Spaces:
Sleeping
Sleeping
add demo
Browse files- .dockerignore +231 -0
- .gitattributes +4 -0
- .gitignore +225 -0
- Dockerfile +47 -0
- README.md +143 -1
- assets/images/5d2cd6b023516.gif +3 -0
- assets/images/8poegbt86i.jpg +3 -0
- assets/images/Ahjumma ChatGPT Image Mar 26, 2025, 05_36_15 PM.png +3 -0
- assets/images/Ahjumma and Ahjussi.png +3 -0
- assets/images/Ahjumma loved kimchi ChatGPT Image Mar 26, 2025, 06_05_13 PM.png +3 -0
- assets/images/Ahjussi ChatGPT Image May 5, 2025, 04_37_07 PM.png +3 -0
- assets/images/Family portrait ChatGPT Image Mar 26, 2025, 06_42_16 PM.png +3 -0
- assets/images/Family portraits 2ChatGPT Image Aug 17, 2025, 10_48_14 PM.png +3 -0
- assets/images/Idol ChatGPT Image Aug 17, 2025, 10_45_48 PM.png +3 -0
- assets/images/K-pop trainee ChatGPT Image May 5, 2025, 04_35_54 PM.png +3 -0
- assets/images/SeonsaengnimChatGPT Image Aug 17, 2025, 10_41_39 PM.png +3 -0
- assets/images/SunbaeOppa ChatGPT Image May 5, 2025, 04_07_13 PM.png +3 -0
- assets/images/app-running.png +3 -0
- assets/images/bukchon-hanok-thumb-scaled.jpg +3 -0
- assets/images/chat-grandfather.png +3 -0
- assets/images/chat-grandmother.png +3 -0
- assets/images/chat-words+inventory.png +3 -0
- assets/images/game poster ChatGPT Image May 5, 2025, 05_02_06 PM.png +3 -0
- assets/images/images.jpg +3 -0
- assets/images/repeat-background.jpg +3 -0
- docker-compose.yml +26 -0
- korean_mud_game.html +1219 -0
- pyproject.toml +29 -0
- src/korean_cpc_agents/__init__.py +0 -0
- src/korean_cpc_agents/config/agents.yaml +41 -0
- src/korean_cpc_agents/config/tasks.yaml +126 -0
- src/korean_cpc_agents/crew.py +154 -0
- src/korean_cpc_agents/main.py +126 -0
- src/korean_cpc_agents/mud_game.py +1088 -0
- uv.lock +0 -0
- web_server.py +443 -0
.dockerignore
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copy from .gitignore - ignore everything that git ignores
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[codz]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
|
| 7 |
+
# Distribution / packaging
|
| 8 |
+
.Python
|
| 9 |
+
build/
|
| 10 |
+
develop-eggs/
|
| 11 |
+
dist/
|
| 12 |
+
downloads/
|
| 13 |
+
eggs/
|
| 14 |
+
.eggs/
|
| 15 |
+
lib/
|
| 16 |
+
lib64/
|
| 17 |
+
parts/
|
| 18 |
+
sdist/
|
| 19 |
+
var/
|
| 20 |
+
wheels/
|
| 21 |
+
share/python-wheels/
|
| 22 |
+
*.egg-info/
|
| 23 |
+
.installed.cfg
|
| 24 |
+
*.egg
|
| 25 |
+
MANIFEST
|
| 26 |
+
|
| 27 |
+
# PyInstaller
|
| 28 |
+
*.manifest
|
| 29 |
+
*.spec
|
| 30 |
+
|
| 31 |
+
# Installer logs
|
| 32 |
+
pip-log.txt
|
| 33 |
+
pip-delete-this-directory.txt
|
| 34 |
+
|
| 35 |
+
# Unit test / coverage reports
|
| 36 |
+
htmlcov/
|
| 37 |
+
.tox/
|
| 38 |
+
.nox/
|
| 39 |
+
.coverage
|
| 40 |
+
.coverage.*
|
| 41 |
+
.cache
|
| 42 |
+
nosetests.xml
|
| 43 |
+
coverage.xml
|
| 44 |
+
*.cover
|
| 45 |
+
*.py.cover
|
| 46 |
+
.hypothesis/
|
| 47 |
+
.pytest_cache/
|
| 48 |
+
cover/
|
| 49 |
+
|
| 50 |
+
# Translations
|
| 51 |
+
*.mo
|
| 52 |
+
*.pot
|
| 53 |
+
|
| 54 |
+
# Django stuff:
|
| 55 |
+
*.log
|
| 56 |
+
local_settings.py
|
| 57 |
+
db.sqlite3
|
| 58 |
+
db.sqlite3-journal
|
| 59 |
+
|
| 60 |
+
# Flask stuff:
|
| 61 |
+
instance/
|
| 62 |
+
.webassets-cache
|
| 63 |
+
|
| 64 |
+
# Scrapy stuff:
|
| 65 |
+
.scrapy
|
| 66 |
+
|
| 67 |
+
# Sphinx documentation
|
| 68 |
+
docs/_build/
|
| 69 |
+
|
| 70 |
+
# PyBuilder
|
| 71 |
+
.pybuilder/
|
| 72 |
+
target/
|
| 73 |
+
|
| 74 |
+
# Jupyter Notebook
|
| 75 |
+
.ipynb_checkpoints
|
| 76 |
+
|
| 77 |
+
# IPython
|
| 78 |
+
profile_default/
|
| 79 |
+
ipython_config.py
|
| 80 |
+
|
| 81 |
+
# pyenv
|
| 82 |
+
# .python-version
|
| 83 |
+
|
| 84 |
+
# pipenv
|
| 85 |
+
#Pipfile.lock
|
| 86 |
+
|
| 87 |
+
# UV
|
| 88 |
+
#uv.lock
|
| 89 |
+
|
| 90 |
+
# poetry
|
| 91 |
+
#poetry.lock
|
| 92 |
+
#poetry.toml
|
| 93 |
+
|
| 94 |
+
# pdm
|
| 95 |
+
#pdm.lock
|
| 96 |
+
#pdm.toml
|
| 97 |
+
.pdm-python
|
| 98 |
+
.pdm-build/
|
| 99 |
+
|
| 100 |
+
# pixi
|
| 101 |
+
#pixi.lock
|
| 102 |
+
.pixi
|
| 103 |
+
|
| 104 |
+
# PEP 582
|
| 105 |
+
__pypackages__/
|
| 106 |
+
|
| 107 |
+
# Celery stuff
|
| 108 |
+
celerybeat-schedule
|
| 109 |
+
celerybeat.pid
|
| 110 |
+
|
| 111 |
+
# SageMath parsed files
|
| 112 |
+
*.sage.py
|
| 113 |
+
|
| 114 |
+
# Environments
|
| 115 |
+
.env
|
| 116 |
+
.envrc
|
| 117 |
+
.venv
|
| 118 |
+
env/
|
| 119 |
+
venv/
|
| 120 |
+
ENV/
|
| 121 |
+
env.bak/
|
| 122 |
+
venv.bak/
|
| 123 |
+
|
| 124 |
+
# Spyder project settings
|
| 125 |
+
.spyderproject
|
| 126 |
+
.spyproject
|
| 127 |
+
|
| 128 |
+
# Rope project settings
|
| 129 |
+
.ropeproject
|
| 130 |
+
|
| 131 |
+
# mkdocs documentation
|
| 132 |
+
/site
|
| 133 |
+
|
| 134 |
+
# mypy
|
| 135 |
+
.mypy_cache/
|
| 136 |
+
.dmypy.json
|
| 137 |
+
dmypy.json
|
| 138 |
+
|
| 139 |
+
# Pyre type checker
|
| 140 |
+
.pyre/
|
| 141 |
+
|
| 142 |
+
# pytype static type analyzer
|
| 143 |
+
.pytype/
|
| 144 |
+
|
| 145 |
+
# Cython debug symbols
|
| 146 |
+
cython_debug/
|
| 147 |
+
|
| 148 |
+
# PyCharm
|
| 149 |
+
#.idea/
|
| 150 |
+
|
| 151 |
+
# Abstra
|
| 152 |
+
.abstra/
|
| 153 |
+
|
| 154 |
+
# Visual Studio Code
|
| 155 |
+
# .vscode/
|
| 156 |
+
|
| 157 |
+
# Ruff stuff:
|
| 158 |
+
.ruff_cache/
|
| 159 |
+
|
| 160 |
+
# PyPI configuration file
|
| 161 |
+
.pypirc
|
| 162 |
+
|
| 163 |
+
# Cursor
|
| 164 |
+
.cursorignore
|
| 165 |
+
.cursorindexingignore
|
| 166 |
+
|
| 167 |
+
# Marimo
|
| 168 |
+
marimo/_static/
|
| 169 |
+
marimo/_lsp/
|
| 170 |
+
__marimo__/
|
| 171 |
+
|
| 172 |
+
# Kiro
|
| 173 |
+
.kiro/
|
| 174 |
+
|
| 175 |
+
# Data
|
| 176 |
+
data/
|
| 177 |
+
|
| 178 |
+
# exploratory notebooks
|
| 179 |
+
*.ipynb
|
| 180 |
+
|
| 181 |
+
# claude
|
| 182 |
+
CLAUDE.md
|
| 183 |
+
.claude/
|
| 184 |
+
|
| 185 |
+
# Ephemeral DB locks
|
| 186 |
+
crewai-rag-tool.lock
|
| 187 |
+
db/
|
| 188 |
+
chromadb-*.lock
|
| 189 |
+
|
| 190 |
+
# Docker specific ignores
|
| 191 |
+
# Absolutely ignore ChromaDB stuff
|
| 192 |
+
chromadb/
|
| 193 |
+
*.chromadb
|
| 194 |
+
chroma_db/
|
| 195 |
+
chromadb-*
|
| 196 |
+
*.chroma
|
| 197 |
+
|
| 198 |
+
# Git files
|
| 199 |
+
.git/
|
| 200 |
+
.gitignore
|
| 201 |
+
README_old.md
|
| 202 |
+
|
| 203 |
+
# Development files
|
| 204 |
+
tests/
|
| 205 |
+
tests_*/
|
| 206 |
+
examples/
|
| 207 |
+
*.md
|
| 208 |
+
!README.md
|
| 209 |
+
|
| 210 |
+
# Local development
|
| 211 |
+
.DS_Store
|
| 212 |
+
Thumbs.db
|
| 213 |
+
*.swp
|
| 214 |
+
*.swo
|
| 215 |
+
*~
|
| 216 |
+
|
| 217 |
+
# IDE files
|
| 218 |
+
.vscode/
|
| 219 |
+
.idea/
|
| 220 |
+
*.sublime-*
|
| 221 |
+
|
| 222 |
+
# Docker files (don't copy Docker files into container)
|
| 223 |
+
Dockerfile
|
| 224 |
+
.dockerignore
|
| 225 |
+
docker-compose.yml
|
| 226 |
+
docker-compose.*.yml
|
| 227 |
+
|
| 228 |
+
# CI/CD
|
| 229 |
+
.github/
|
| 230 |
+
.gitlab-ci.yml
|
| 231 |
+
.travis.yml
|
.gitattributes
CHANGED
|
@@ -33,3 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
*.jpg filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
*.gif filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Byte-compiled / optimized / DLL files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[codz]
|
| 4 |
+
*$py.class
|
| 5 |
+
|
| 6 |
+
# C extensions
|
| 7 |
+
*.so
|
| 8 |
+
|
| 9 |
+
# Distribution / packaging
|
| 10 |
+
.Python
|
| 11 |
+
build/
|
| 12 |
+
develop-eggs/
|
| 13 |
+
dist/
|
| 14 |
+
downloads/
|
| 15 |
+
eggs/
|
| 16 |
+
.eggs/
|
| 17 |
+
lib/
|
| 18 |
+
lib64/
|
| 19 |
+
parts/
|
| 20 |
+
sdist/
|
| 21 |
+
var/
|
| 22 |
+
wheels/
|
| 23 |
+
share/python-wheels/
|
| 24 |
+
*.egg-info/
|
| 25 |
+
.installed.cfg
|
| 26 |
+
*.egg
|
| 27 |
+
MANIFEST
|
| 28 |
+
|
| 29 |
+
# PyInstaller
|
| 30 |
+
# Usually these files are written by a python script from a template
|
| 31 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 32 |
+
*.manifest
|
| 33 |
+
*.spec
|
| 34 |
+
|
| 35 |
+
# Installer logs
|
| 36 |
+
pip-log.txt
|
| 37 |
+
pip-delete-this-directory.txt
|
| 38 |
+
|
| 39 |
+
# Unit test / coverage reports
|
| 40 |
+
htmlcov/
|
| 41 |
+
.tox/
|
| 42 |
+
.nox/
|
| 43 |
+
.coverage
|
| 44 |
+
.coverage.*
|
| 45 |
+
.cache
|
| 46 |
+
nosetests.xml
|
| 47 |
+
coverage.xml
|
| 48 |
+
*.cover
|
| 49 |
+
*.py.cover
|
| 50 |
+
.hypothesis/
|
| 51 |
+
.pytest_cache/
|
| 52 |
+
cover/
|
| 53 |
+
|
| 54 |
+
# Translations
|
| 55 |
+
*.mo
|
| 56 |
+
*.pot
|
| 57 |
+
|
| 58 |
+
# Django stuff:
|
| 59 |
+
*.log
|
| 60 |
+
local_settings.py
|
| 61 |
+
db.sqlite3
|
| 62 |
+
db.sqlite3-journal
|
| 63 |
+
|
| 64 |
+
# Flask stuff:
|
| 65 |
+
instance/
|
| 66 |
+
.webassets-cache
|
| 67 |
+
|
| 68 |
+
# Scrapy stuff:
|
| 69 |
+
.scrapy
|
| 70 |
+
|
| 71 |
+
# Sphinx documentation
|
| 72 |
+
docs/_build/
|
| 73 |
+
|
| 74 |
+
# PyBuilder
|
| 75 |
+
.pybuilder/
|
| 76 |
+
target/
|
| 77 |
+
|
| 78 |
+
# Jupyter Notebook
|
| 79 |
+
.ipynb_checkpoints
|
| 80 |
+
|
| 81 |
+
# IPython
|
| 82 |
+
profile_default/
|
| 83 |
+
ipython_config.py
|
| 84 |
+
|
| 85 |
+
# pyenv
|
| 86 |
+
# For a library or package, you might want to ignore these files since the code is
|
| 87 |
+
# intended to run in multiple environments; otherwise, check them in:
|
| 88 |
+
# .python-version
|
| 89 |
+
|
| 90 |
+
# pipenv
|
| 91 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
| 92 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
| 93 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
| 94 |
+
# install all needed dependencies.
|
| 95 |
+
#Pipfile.lock
|
| 96 |
+
|
| 97 |
+
# UV
|
| 98 |
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
| 99 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 100 |
+
# commonly ignored for libraries.
|
| 101 |
+
#uv.lock
|
| 102 |
+
|
| 103 |
+
# poetry
|
| 104 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
| 105 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 106 |
+
# commonly ignored for libraries.
|
| 107 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
| 108 |
+
#poetry.lock
|
| 109 |
+
#poetry.toml
|
| 110 |
+
|
| 111 |
+
# pdm
|
| 112 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
| 113 |
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
| 114 |
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
| 115 |
+
#pdm.lock
|
| 116 |
+
#pdm.toml
|
| 117 |
+
.pdm-python
|
| 118 |
+
.pdm-build/
|
| 119 |
+
|
| 120 |
+
# pixi
|
| 121 |
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
| 122 |
+
#pixi.lock
|
| 123 |
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
| 124 |
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
| 125 |
+
.pixi
|
| 126 |
+
|
| 127 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
| 128 |
+
__pypackages__/
|
| 129 |
+
|
| 130 |
+
# Celery stuff
|
| 131 |
+
celerybeat-schedule
|
| 132 |
+
celerybeat.pid
|
| 133 |
+
|
| 134 |
+
# SageMath parsed files
|
| 135 |
+
*.sage.py
|
| 136 |
+
|
| 137 |
+
# Environments
|
| 138 |
+
.env
|
| 139 |
+
.envrc
|
| 140 |
+
.venv
|
| 141 |
+
env/
|
| 142 |
+
venv/
|
| 143 |
+
ENV/
|
| 144 |
+
env.bak/
|
| 145 |
+
venv.bak/
|
| 146 |
+
|
| 147 |
+
# Spyder project settings
|
| 148 |
+
.spyderproject
|
| 149 |
+
.spyproject
|
| 150 |
+
|
| 151 |
+
# Rope project settings
|
| 152 |
+
.ropeproject
|
| 153 |
+
|
| 154 |
+
# mkdocs documentation
|
| 155 |
+
/site
|
| 156 |
+
|
| 157 |
+
# mypy
|
| 158 |
+
.mypy_cache/
|
| 159 |
+
.dmypy.json
|
| 160 |
+
dmypy.json
|
| 161 |
+
|
| 162 |
+
# Pyre type checker
|
| 163 |
+
.pyre/
|
| 164 |
+
|
| 165 |
+
# pytype static type analyzer
|
| 166 |
+
.pytype/
|
| 167 |
+
|
| 168 |
+
# Cython debug symbols
|
| 169 |
+
cython_debug/
|
| 170 |
+
|
| 171 |
+
# PyCharm
|
| 172 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
| 173 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
| 174 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
| 175 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
| 176 |
+
#.idea/
|
| 177 |
+
|
| 178 |
+
# Abstra
|
| 179 |
+
# Abstra is an AI-powered process automation framework.
|
| 180 |
+
# Ignore directories containing user credentials, local state, and settings.
|
| 181 |
+
# Learn more at https://abstra.io/docs
|
| 182 |
+
.abstra/
|
| 183 |
+
|
| 184 |
+
# Visual Studio Code
|
| 185 |
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
| 186 |
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
| 187 |
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
| 188 |
+
# you could uncomment the following to ignore the entire vscode folder
|
| 189 |
+
# .vscode/
|
| 190 |
+
|
| 191 |
+
# Ruff stuff:
|
| 192 |
+
.ruff_cache/
|
| 193 |
+
|
| 194 |
+
# PyPI configuration file
|
| 195 |
+
.pypirc
|
| 196 |
+
|
| 197 |
+
# Cursor
|
| 198 |
+
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
| 199 |
+
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
| 200 |
+
# refer to https://docs.cursor.com/context/ignore-files
|
| 201 |
+
.cursorignore
|
| 202 |
+
.cursorindexingignore
|
| 203 |
+
|
| 204 |
+
# Marimo
|
| 205 |
+
marimo/_static/
|
| 206 |
+
marimo/_lsp/
|
| 207 |
+
__marimo__/
|
| 208 |
+
|
| 209 |
+
# Kiro
|
| 210 |
+
.kiro/
|
| 211 |
+
|
| 212 |
+
# Data
|
| 213 |
+
data/
|
| 214 |
+
|
| 215 |
+
# exploratory notebooks
|
| 216 |
+
*.ipynb
|
| 217 |
+
|
| 218 |
+
# claude
|
| 219 |
+
CLAUDE.md
|
| 220 |
+
.claude/
|
| 221 |
+
|
| 222 |
+
# Ephemeral DB locks
|
| 223 |
+
crewai-rag-tool.lock
|
| 224 |
+
db/
|
| 225 |
+
chromadb-*.lock"assets/images/"
|
Dockerfile
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use official Python runtime as base image
|
| 2 |
+
FROM python:3.12-slim
|
| 3 |
+
|
| 4 |
+
# Set working directory
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Install system dependencies
|
| 8 |
+
RUN apt-get update && apt-get install -y \
|
| 9 |
+
curl \
|
| 10 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 11 |
+
|
| 12 |
+
# Install uv for fast Python package management
|
| 13 |
+
RUN pip install uv
|
| 14 |
+
|
| 15 |
+
# Copy dependency files
|
| 16 |
+
COPY pyproject.toml uv.lock ./
|
| 17 |
+
|
| 18 |
+
# Install dependencies only (not the local package)
|
| 19 |
+
RUN uv export --format requirements-txt --no-hashes > requirements.txt && \
|
| 20 |
+
uv venv && \
|
| 21 |
+
uv pip install -r requirements.txt
|
| 22 |
+
|
| 23 |
+
# Copy application code
|
| 24 |
+
COPY src/ ./src/
|
| 25 |
+
COPY web_server.py ./
|
| 26 |
+
COPY korean_mud_game.html ./
|
| 27 |
+
COPY assets/ ./assets/
|
| 28 |
+
|
| 29 |
+
# Create non-root user for security
|
| 30 |
+
RUN useradd --create-home --shell /bin/bash appuser && \
|
| 31 |
+
chown -R appuser:appuser /app
|
| 32 |
+
USER appuser
|
| 33 |
+
|
| 34 |
+
# Expose port for HuggingFace Spaces
|
| 35 |
+
EXPOSE 7860
|
| 36 |
+
|
| 37 |
+
# Health check
|
| 38 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
| 39 |
+
CMD curl -f http://localhost:7860/api/game/help || exit 1
|
| 40 |
+
|
| 41 |
+
# Set environment variables
|
| 42 |
+
ENV PYTHONPATH=/app
|
| 43 |
+
ENV PYTHONUNBUFFERED=1
|
| 44 |
+
ENV DOCKER_ENV=1
|
| 45 |
+
|
| 46 |
+
# Run the application
|
| 47 |
+
CMD ["uv", "run", "python", "web_server.py"]
|
README.md
CHANGED
|
@@ -9,4 +9,146 @@ license: mit
|
|
| 9 |
short_description: Interactive Korean language learning MUD game
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
short_description: Interactive Korean language learning MUD game
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# Korean Learning MUD Game 🇰🇷
|
| 13 |
+
|
| 14 |
+
Interactive Korean language learning through conversation with AI-powered Korean family members in a retro MUD-style game.
|
| 15 |
+
|
| 16 |
+

|
| 17 |
+
|
| 18 |
+
> **Live Demo:** Hosted on HuggingFace Spaces for easy access
|
| 19 |
+
|
| 20 |
+
## Features
|
| 21 |
+
|
| 22 |
+
- 🏠 **Explore a Korean Family House** - Navigate through 6 rooms (Hall, Kitchen, Garden, Bedroom, Study, Classroom)
|
| 23 |
+
- 👨👩👧👦 **Chat with Unique NPCs** - Each family member has distinct personalities and teaching styles
|
| 24 |
+
- 🔍 **Examine Cultural Objects** - Interactive objects in each room teach Korean vocabulary and culture
|
| 25 |
+
- 🎨 **Retro Terminal Aesthetic** - Beautiful brown/tan Korean traditional vibes
|
| 26 |
+
- 📚 **Progressive Learning System** - Smart vocabulary tracking with no repeats
|
| 27 |
+
- 💬 **Natural Conversation** - Chat naturally with AI agents powered by CrewAI
|
| 28 |
+
- 🎒 **Inventory System** - Collect examined objects and track learning progress
|
| 29 |
+
|
| 30 |
+
## Korean Family Members
|
| 31 |
+
|
| 32 |
+
Meet your Korean language teachers - each AI agent has a unique personality and teaching style powered by CrewAI:
|
| 33 |
+
|
| 34 |
+
| Family Member | Korean Name | Room | Teaching Focus | Avatar |
|
| 35 |
+
| ------------------------- | --------------- | ------------ | --------------------------------- | ------------------------------------------------------------------------------------------------- |
|
| 36 |
+
| **Grandma Kim Soon-ja** | 김순자 할머니 | 🍳 Kitchen | Honorifics & Formal Speech |  |
|
| 37 |
+
| **Grandpa Park Chul-min** | 박철민 할아버지 | 🌸 Garden | Traditional Culture & Proverbs |  |
|
| 38 |
+
| **Sister Lee Min-ji** | 이민지 언니 | 🎵 Bedroom | K-pop & Modern Slang |  |
|
| 39 |
+
| **Brother Jung Jae-hyun** | 정재현 오빠 | 📚 Study | Grammar & Academic Korean |  |
|
| 40 |
+
| **Teacher Choi Soo-jin** | 최수진 선생님 | ✏️ Classroom | Practical Phrases & Communication |  |
|
| 41 |
+
|
| 42 |
+
## How to Play
|
| 43 |
+
|
| 44 |
+
### Basic Commands
|
| 45 |
+
|
| 46 |
+
- `look` - Look around your current room
|
| 47 |
+
- `go [room]` - Move to another room (kitchen, garden, bedroom, study, classroom, hall)
|
| 48 |
+
- `examine [object]` - Examine objects to learn Korean vocabulary
|
| 49 |
+
- `chat [message]` - Talk to the Korean family member in your room
|
| 50 |
+
- `help` - Show all available commands
|
| 51 |
+
- `map` - See the house layout and room locations
|
| 52 |
+
|
| 53 |
+
### Game Flow
|
| 54 |
+
|
| 55 |
+
1. **Start in the Hall** - Central hub connecting all rooms
|
| 56 |
+
2. **Visit Family Members** - Each room has a different Korean family member
|
| 57 |
+
3. **Examine Objects** - Learn vocabulary and cultural context
|
| 58 |
+
4. **Practice Conversations** - Natural Korean language practice
|
| 59 |
+
5. **Track Progress** - See words learned and objects examined
|
| 60 |
+
|
| 61 |
+
## Screenshots
|
| 62 |
+
|
| 63 |
+

|
| 64 |
+
_Main game interface with retro terminal styling_
|
| 65 |
+
|
| 66 |
+

|
| 67 |
+
_Learning proper Korean honorifics with Grandma in the Kitchen_
|
| 68 |
+
|
| 69 |
+

|
| 70 |
+
_Traditional Korean wisdom and proverbs in the Garden_
|
| 71 |
+
|
| 72 |
+

|
| 73 |
+
_Track your Korean learning progress and cultural discoveries_
|
| 74 |
+
|
| 75 |
+
## Quick Start
|
| 76 |
+
|
| 77 |
+
### Option 1: Docker (Recommended)
|
| 78 |
+
|
| 79 |
+
1. **Clone and navigate to project:**
|
| 80 |
+
|
| 81 |
+
```bash
|
| 82 |
+
git clone https://github.com/Ramsi-K/korean-cpc-agents
|
| 83 |
+
cd korean-cpc-agents
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
2. **Set up environment:**
|
| 87 |
+
|
| 88 |
+
```bash
|
| 89 |
+
# Create .env file with your OpenAI API key
|
| 90 |
+
echo "OPENAI_API_KEY=your-api-key-here" > .env
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
3. **Run with Docker Compose:**
|
| 94 |
+
|
| 95 |
+
```bash
|
| 96 |
+
docker compose up --build
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
4. **Play the game:**
|
| 100 |
+
Open http://localhost:7860 in your browser!
|
| 101 |
+
|
| 102 |
+
### Option 2: Local Development
|
| 103 |
+
|
| 104 |
+
1. **Install dependencies:**
|
| 105 |
+
|
| 106 |
+
```bash
|
| 107 |
+
uv sync
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
2. **Set up your OpenAI API key:**
|
| 111 |
+
|
| 112 |
+
```bash
|
| 113 |
+
export OPENAI_API_KEY="your-api-key-here"
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
3. **Run the game:**
|
| 117 |
+
|
| 118 |
+
```bash
|
| 119 |
+
uv run python web_server.py
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
4. **Open your browser:**
|
| 123 |
+
|
| 124 |
+
```bash
|
| 125 |
+
http://localhost:7860
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
## Technology Stack
|
| 129 |
+
|
| 130 |
+
- **Backend:** FastAPI + Python
|
| 131 |
+
- **AI Agents:** CrewAI with OpenAI GPT models
|
| 132 |
+
- **Frontend:** Vanilla HTML/CSS/JavaScript with retro terminal styling
|
| 133 |
+
- **Package Management:** uv
|
| 134 |
+
- **Game Engine:** Custom MUD-style text adventure system
|
| 135 |
+
|
| 136 |
+
## Project Structure
|
| 137 |
+
|
| 138 |
+
```yaml
|
| 139 |
+
korean-cpc-agents/
|
| 140 |
+
├── src/korean_cpc_agents/ # Core game logic and AI agents
|
| 141 |
+
├── korean_mud_game.html # Frontend interface
|
| 142 |
+
├── web_server.py # FastAPI backend server
|
| 143 |
+
├── assets/images/ # Game images and sprites
|
| 144 |
+
└── examples/ # Screenshots and demos
|
| 145 |
+
```
|
| 146 |
+
|
| 147 |
+
## License
|
| 148 |
+
|
| 149 |
+
Open source - feel free to fork and create your own language learning MUD games!
|
| 150 |
+
|
| 151 |
+
---
|
| 152 |
+
|
| 153 |
+
*Learn Korean through immersive conversations with AI-powered family members! 한국어를 배워보세요! 🇰🇷*🫰
|
| 154 |
+
[With Love](assets/images/Ahjumma%20and%20Ahjussi.png)
|
assets/images/5d2cd6b023516.gif
ADDED
|
Git LFS Details
|
assets/images/8poegbt86i.jpg
ADDED
|
Git LFS Details
|
assets/images/Ahjumma ChatGPT Image Mar 26, 2025, 05_36_15 PM.png
ADDED
|
Git LFS Details
|
assets/images/Ahjumma and Ahjussi.png
ADDED
|
Git LFS Details
|
assets/images/Ahjumma loved kimchi ChatGPT Image Mar 26, 2025, 06_05_13 PM.png
ADDED
|
Git LFS Details
|
assets/images/Ahjussi ChatGPT Image May 5, 2025, 04_37_07 PM.png
ADDED
|
Git LFS Details
|
assets/images/Family portrait ChatGPT Image Mar 26, 2025, 06_42_16 PM.png
ADDED
|
Git LFS Details
|
assets/images/Family portraits 2ChatGPT Image Aug 17, 2025, 10_48_14 PM.png
ADDED
|
Git LFS Details
|
assets/images/Idol ChatGPT Image Aug 17, 2025, 10_45_48 PM.png
ADDED
|
Git LFS Details
|
assets/images/K-pop trainee ChatGPT Image May 5, 2025, 04_35_54 PM.png
ADDED
|
Git LFS Details
|
assets/images/SeonsaengnimChatGPT Image Aug 17, 2025, 10_41_39 PM.png
ADDED
|
Git LFS Details
|
assets/images/SunbaeOppa ChatGPT Image May 5, 2025, 04_07_13 PM.png
ADDED
|
Git LFS Details
|
assets/images/app-running.png
ADDED
|
Git LFS Details
|
assets/images/bukchon-hanok-thumb-scaled.jpg
ADDED
|
|
Git LFS Details
|
assets/images/chat-grandfather.png
ADDED
|
Git LFS Details
|
assets/images/chat-grandmother.png
ADDED
|
Git LFS Details
|
assets/images/chat-words+inventory.png
ADDED
|
Git LFS Details
|
assets/images/game poster ChatGPT Image May 5, 2025, 05_02_06 PM.png
ADDED
|
Git LFS Details
|
assets/images/images.jpg
ADDED
|
Git LFS Details
|
assets/images/repeat-background.jpg
ADDED
|
Git LFS Details
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
services:
|
| 2 |
+
korean-mud-game:
|
| 3 |
+
build:
|
| 4 |
+
context: .
|
| 5 |
+
dockerfile: Dockerfile
|
| 6 |
+
ports:
|
| 7 |
+
- '7860:7860'
|
| 8 |
+
environment:
|
| 9 |
+
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
| 10 |
+
- PYTHONUNBUFFERED=1
|
| 11 |
+
- DOCKER_ENV=1
|
| 12 |
+
restart: unless-stopped
|
| 13 |
+
healthcheck:
|
| 14 |
+
test: ['CMD', 'curl', '-f', 'http://localhost:7860/api/game/help']
|
| 15 |
+
interval: 30s
|
| 16 |
+
timeout: 10s
|
| 17 |
+
retries: 3
|
| 18 |
+
start_period: 40s
|
| 19 |
+
volumes:
|
| 20 |
+
- ./assets:/app/assets:ro
|
| 21 |
+
networks:
|
| 22 |
+
- korean-mud-network
|
| 23 |
+
|
| 24 |
+
networks:
|
| 25 |
+
korean-mud-network:
|
| 26 |
+
driver: bridge
|
korean_mud_game.html
ADDED
|
@@ -0,0 +1,1219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>🏠 한국어 가족집 모험 🇰🇷</title>
|
| 7 |
+
<style>
|
| 8 |
+
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&family=Orbitron:wght@400;700&display=swap');
|
| 9 |
+
|
| 10 |
+
* {
|
| 11 |
+
margin: 0;
|
| 12 |
+
padding: 0;
|
| 13 |
+
box-sizing: border-box;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
body {
|
| 17 |
+
font-family: 'Courier New', monospace;
|
| 18 |
+
background: url('assets/images/bukchon-hanok-thumb-scaled.jpg') center/cover fixed;
|
| 19 |
+
background-color: #f5e9d9;
|
| 20 |
+
color: #3a3226;
|
| 21 |
+
min-height: 100vh;
|
| 22 |
+
overflow-x: hidden;
|
| 23 |
+
position: relative;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
body::before {
|
| 27 |
+
content: '';
|
| 28 |
+
position: absolute;
|
| 29 |
+
top: 0;
|
| 30 |
+
left: 0;
|
| 31 |
+
right: 0;
|
| 32 |
+
bottom: 0;
|
| 33 |
+
background: url('assets/images/repeat-background.jpg') repeat;
|
| 34 |
+
opacity: 0.15;
|
| 35 |
+
pointer-events: none;
|
| 36 |
+
z-index: -1;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
/* Animated background elements */
|
| 40 |
+
.bg-particles {
|
| 41 |
+
position: fixed;
|
| 42 |
+
width: 100%;
|
| 43 |
+
height: 100%;
|
| 44 |
+
pointer-events: none;
|
| 45 |
+
z-index: -1;
|
| 46 |
+
overflow: hidden;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.particle {
|
| 50 |
+
position: absolute;
|
| 51 |
+
width: 2px;
|
| 52 |
+
height: 2px;
|
| 53 |
+
background: rgba(255, 255, 255, 0.3);
|
| 54 |
+
animation: float 6s ease-in-out infinite;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
.flower-petal {
|
| 58 |
+
position: absolute;
|
| 59 |
+
width: 15px;
|
| 60 |
+
height: 15px;
|
| 61 |
+
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="%23ff9ff3" d="M12,2C13.1,2 14,2.9 14,4C14,5.1 13.1,6 12,6C10.9,6 10,5.1 10,4C10,2.9 10.9,2 12,2M15.5,8C16.3,8 17,8.7 17,9.5C17,10.3 16.3,11 15.5,11C14.7,11 14,10.3 14,9.5C14,8.7 14.7,8 15.5,8M8.5,8C9.3,8 10,8.7 10,9.5C10,10.3 9.3,11 8.5,11C7.7,11 7,10.3 7,9.5C7,8.7 7.7,8 8.5,8M12,11C13.1,11 14,11.9 14,13C14,14.1 13.1,15 12,15C10.9,15 10,14.1 10,13C10,11.9 10.9,11 12,11M19,17V19H5V17C5,14.8 8.1,13 12,13C15.9,13 19,14.8 19,17Z" /></svg>');
|
| 62 |
+
background-size: contain;
|
| 63 |
+
opacity: 0.7;
|
| 64 |
+
animation: petalFall linear infinite;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
@keyframes float {
|
| 68 |
+
0%, 100% { transform: translateY(0px) rotate(0deg); opacity: 0.3; }
|
| 69 |
+
50% { transform: translateY(-20px) rotate(180deg); opacity: 0.8; }
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
@keyframes petalFall {
|
| 73 |
+
0% {
|
| 74 |
+
transform: translate(0, -10vh) rotate(0deg);
|
| 75 |
+
opacity: 0;
|
| 76 |
+
}
|
| 77 |
+
10% {
|
| 78 |
+
opacity: 0.7;
|
| 79 |
+
}
|
| 80 |
+
90% {
|
| 81 |
+
opacity: 0.7;
|
| 82 |
+
}
|
| 83 |
+
100% {
|
| 84 |
+
transform: translate(var(--random-x), 100vh) rotate(360deg);
|
| 85 |
+
opacity: 0;
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
/* Header */
|
| 90 |
+
.game-header {
|
| 91 |
+
background: linear-gradient(90deg, #8b6b4a, #6b4f32, #8b6b4a);
|
| 92 |
+
padding: 10px 0;
|
| 93 |
+
text-align: center;
|
| 94 |
+
border-bottom: 3px solid #3a3226;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.header-content h1 {
|
| 98 |
+
font-family: 'Gungsuh', 'Batang', serif;
|
| 99 |
+
font-size: 2.5em;
|
| 100 |
+
font-weight: 700;
|
| 101 |
+
color: #3a3226;
|
| 102 |
+
margin-bottom: 5px;
|
| 103 |
+
text-shadow: 2px 2px 4px rgba(0,0,0,0.1);
|
| 104 |
+
letter-spacing: 1px;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
.progress-stats {
|
| 108 |
+
display: flex;
|
| 109 |
+
gap: 15px;
|
| 110 |
+
justify-content: center;
|
| 111 |
+
margin-top: 10px;
|
| 112 |
+
flex-wrap: wrap;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.progress-indicator {
|
| 116 |
+
background: rgba(107, 79, 50, 0.7);
|
| 117 |
+
border: 2px solid #3a3226;
|
| 118 |
+
border-radius: 20px;
|
| 119 |
+
padding: 5px 15px;
|
| 120 |
+
display: inline-flex;
|
| 121 |
+
align-items: center;
|
| 122 |
+
gap: 10px;
|
| 123 |
+
cursor: pointer;
|
| 124 |
+
transition: all 0.3s ease;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.progress-indicator:hover {
|
| 128 |
+
background: rgba(107, 79, 50, 0.9);
|
| 129 |
+
box-shadow: 0 0 10px rgba(107, 79, 50, 0.5);
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
.progress-count {
|
| 133 |
+
font-family: 'Courier New', monospace;
|
| 134 |
+
font-weight: bold;
|
| 135 |
+
color: #f5e9d9;
|
| 136 |
+
font-size: 1.2em;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.progress-label {
|
| 140 |
+
font-size: 0.9em;
|
| 141 |
+
color: #f5e9d9;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.header-content p {
|
| 145 |
+
color: rgba(255,255,255,0.9);
|
| 146 |
+
font-size: 1.1em;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
/* Main game container */
|
| 150 |
+
.game-container {
|
| 151 |
+
display: grid;
|
| 152 |
+
grid-template-columns: 1fr 300px;
|
| 153 |
+
gap: 20px;
|
| 154 |
+
padding: 20px;
|
| 155 |
+
max-width: 1400px;
|
| 156 |
+
margin: 0 auto;
|
| 157 |
+
min-height: calc(100vh - 120px);
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
/* Chat area */
|
| 161 |
+
.chat-section {
|
| 162 |
+
background: rgba(245, 233, 217, 0.85);
|
| 163 |
+
border: 2px solid #8b6b4a;
|
| 164 |
+
border-radius: 0;
|
| 165 |
+
box-shadow: 0 4px 20px rgba(0,0,0,0.2),
|
| 166 |
+
inset 0 0 30px rgba(139, 107, 74, 0.3);
|
| 167 |
+
display: flex;
|
| 168 |
+
flex-direction: column;
|
| 169 |
+
overflow: hidden;
|
| 170 |
+
max-width: 800px;
|
| 171 |
+
margin: 0 auto;
|
| 172 |
+
position: relative;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.chat-section::before {
|
| 176 |
+
content: '';
|
| 177 |
+
position: absolute;
|
| 178 |
+
top: 0;
|
| 179 |
+
left: 0;
|
| 180 |
+
right: 0;
|
| 181 |
+
bottom: 0;
|
| 182 |
+
background: url('assets/images/repeat-background.jpg') repeat;
|
| 183 |
+
opacity: 0.05;
|
| 184 |
+
pointer-events: none;
|
| 185 |
+
z-index: 0;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
.chat-header {
|
| 189 |
+
background: linear-gradient(90deg, #8b6b4a, #6b4f32, #8b6b4a);
|
| 190 |
+
padding: 15px 20px;
|
| 191 |
+
color: #f5e9d9;
|
| 192 |
+
font-weight: 600;
|
| 193 |
+
border-bottom: 2px solid #3a3226;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
.current-npc-info {
|
| 197 |
+
display: flex;
|
| 198 |
+
align-items: center;
|
| 199 |
+
gap: 15px;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
.npc-avatar {
|
| 203 |
+
width: 50px;
|
| 204 |
+
height: 50px;
|
| 205 |
+
border-radius: 50%;
|
| 206 |
+
border: 3px solid white;
|
| 207 |
+
object-fit: cover;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.npc-details h3 {
|
| 211 |
+
font-size: 1.2em;
|
| 212 |
+
margin-bottom: 2px;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
.npc-details p {
|
| 216 |
+
font-size: 0.9em;
|
| 217 |
+
opacity: 0.9;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
#game-messages {
|
| 221 |
+
flex: 1;
|
| 222 |
+
padding: 20px;
|
| 223 |
+
overflow-y: auto;
|
| 224 |
+
max-height: 400px;
|
| 225 |
+
scrollbar-width: thin;
|
| 226 |
+
scrollbar-color: #4a5568 #2d3748;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
#game-messages::-webkit-scrollbar {
|
| 230 |
+
width: 8px;
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
#game-messages::-webkit-scrollbar-track {
|
| 234 |
+
background: #2d3748;
|
| 235 |
+
border-radius: 4px;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
#game-messages::-webkit-scrollbar-thumb {
|
| 239 |
+
background: #4a5568;
|
| 240 |
+
border-radius: 4px;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
/* Message styling */
|
| 244 |
+
.game-text {
|
| 245 |
+
margin-bottom: 12px;
|
| 246 |
+
padding: 8px 12px;
|
| 247 |
+
line-height: 1.5;
|
| 248 |
+
border-left: 3px solid transparent;
|
| 249 |
+
opacity: 0;
|
| 250 |
+
animation: fadeIn 0.3s forwards;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
@keyframes fadeIn {
|
| 254 |
+
to { opacity: 1; }
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
.npc-dialogue {
|
| 258 |
+
background: rgba(255,255,255,0.7);
|
| 259 |
+
color: #3a3226;
|
| 260 |
+
border-left: 3px solid #8b6b4a;
|
| 261 |
+
padding: 12px 16px;
|
| 262 |
+
margin: 10px 0;
|
| 263 |
+
position: relative;
|
| 264 |
+
transform: translateX(-20px);
|
| 265 |
+
animation: slideIn 0.5s forwards;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
@keyframes slideIn {
|
| 269 |
+
to { transform: translateX(0); }
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
.npc-name {
|
| 273 |
+
color: #a52a2a;
|
| 274 |
+
font-weight: bold;
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
.command-echo {
|
| 278 |
+
background: rgba(255,255,255,0.3);
|
| 279 |
+
color: #3a3226;
|
| 280 |
+
font-style: italic;
|
| 281 |
+
border-radius: 15px 15px 5px 15px;
|
| 282 |
+
padding: 8px 12px;
|
| 283 |
+
text-align: right;
|
| 284 |
+
font-weight: 600;
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
.help-content {
|
| 288 |
+
background: rgba(245, 233, 217, 0.9);
|
| 289 |
+
border: 2px solid #8b6b4a;
|
| 290 |
+
border-radius: 0;
|
| 291 |
+
padding: 15px;
|
| 292 |
+
white-space: pre-line;
|
| 293 |
+
font-family: 'Courier New', monospace;
|
| 294 |
+
font-size: 0.9em;
|
| 295 |
+
color: #3a3226;
|
| 296 |
+
margin: 10px 0;
|
| 297 |
+
box-shadow: inset 0 0 10px rgba(139, 107, 74, 0.3);
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
.korean-text {
|
| 301 |
+
font-size: 1.1em;
|
| 302 |
+
line-height: 1.6;
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
.vocab-highlight {
|
| 306 |
+
background: linear-gradient(45deg, #ff6b6b, #feca57);
|
| 307 |
+
color: white;
|
| 308 |
+
padding: 2px 6px;
|
| 309 |
+
border-radius: 4px;
|
| 310 |
+
font-weight: 600;
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
/* Chat input */
|
| 314 |
+
.chat-input {
|
| 315 |
+
background: rgba(45, 55, 72, 0.9);
|
| 316 |
+
padding: 15px 20px;
|
| 317 |
+
border-top: 1px solid rgba(255,255,255,0.1);
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
.input-group {
|
| 321 |
+
display: flex;
|
| 322 |
+
gap: 10px;
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
.chat-input input {
|
| 326 |
+
flex: 1;
|
| 327 |
+
background: rgba(255,255,255,0.7);
|
| 328 |
+
border: 2px solid #8b6b4a;
|
| 329 |
+
border-radius: 0;
|
| 330 |
+
padding: 12px 20px;
|
| 331 |
+
color: #3a3226;
|
| 332 |
+
font-family: 'Courier New', monospace;
|
| 333 |
+
font-size: 1em;
|
| 334 |
+
transition: all 0.3s ease;
|
| 335 |
+
box-shadow: inset 0 0 10px rgba(139, 107, 74, 0.3);
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.chat-input input:focus {
|
| 339 |
+
outline: none;
|
| 340 |
+
border-color: #3a3226;
|
| 341 |
+
box-shadow: inset 0 0 15px rgba(139, 107, 74, 0.5);
|
| 342 |
+
animation: caretBlink 1s step-end infinite;
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
@keyframes caretBlink {
|
| 346 |
+
50% { border-right: 2px solid #3a3226; }
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
.chat-input input::placeholder {
|
| 350 |
+
color: rgba(255,255,255,0.6);
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
.send-button {
|
| 354 |
+
background: linear-gradient(45deg, #8b6b4a, #6b4f32);
|
| 355 |
+
border: 2px outset #3a3226;
|
| 356 |
+
border-radius: 0;
|
| 357 |
+
padding: 12px 24px;
|
| 358 |
+
color: #f5e9d9;
|
| 359 |
+
font-family: 'Courier New', monospace;
|
| 360 |
+
font-weight: 600;
|
| 361 |
+
cursor: pointer;
|
| 362 |
+
transition: all 0.3s ease;
|
| 363 |
+
box-shadow: 3px 3px 0 rgba(0,0,0,0.2);
|
| 364 |
+
text-shadow: none;
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
.send-button:hover {
|
| 368 |
+
transform: translateY(-2px);
|
| 369 |
+
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
.send-button:disabled {
|
| 373 |
+
opacity: 0.6;
|
| 374 |
+
cursor: not-allowed;
|
| 375 |
+
transform: none;
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
/* Sidebar */
|
| 379 |
+
.game-sidebar {
|
| 380 |
+
display: flex;
|
| 381 |
+
flex-direction: column;
|
| 382 |
+
gap: 20px;
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
.panel {
|
| 386 |
+
background: rgba(15, 20, 25, 0.8);
|
| 387 |
+
backdrop-filter: blur(10px);
|
| 388 |
+
border-radius: 15px;
|
| 389 |
+
border: 1px solid rgba(255,255,255,0.1);
|
| 390 |
+
overflow: hidden;
|
| 391 |
+
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
.panel-header {
|
| 395 |
+
background: linear-gradient(90deg, #8b6b4a, #6b4f32, #8b6b4a);
|
| 396 |
+
padding: 12px 16px;
|
| 397 |
+
font-weight: 600;
|
| 398 |
+
color: #f5e9d9;
|
| 399 |
+
border-bottom: 2px solid #3a3226;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
.panel-content {
|
| 403 |
+
padding: 16px;
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
/* House map */
|
| 407 |
+
.house-map {
|
| 408 |
+
font-family: 'Courier New', monospace;
|
| 409 |
+
font-size: 0.8em;
|
| 410 |
+
line-height: 1.3;
|
| 411 |
+
text-align: center;
|
| 412 |
+
white-space: pre-line;
|
| 413 |
+
color: #a0aec0;
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
.current-room-highlight {
|
| 417 |
+
background: linear-gradient(45deg, #4ecdc4, #44a08d);
|
| 418 |
+
color: white;
|
| 419 |
+
padding: 2px 6px;
|
| 420 |
+
border-radius: 4px;
|
| 421 |
+
font-weight: bold;
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
/* Progress stats */
|
| 425 |
+
.stats {
|
| 426 |
+
display: grid;
|
| 427 |
+
grid-template-columns: 1fr 1fr;
|
| 428 |
+
gap: 10px;
|
| 429 |
+
margin-bottom: 15px;
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
.stat-item {
|
| 433 |
+
background: rgba(255,255,255,0.05);
|
| 434 |
+
padding: 12px;
|
| 435 |
+
border-radius: 8px;
|
| 436 |
+
text-align: center;
|
| 437 |
+
border: 1px solid rgba(255,255,255,0.1);
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
.stat-value {
|
| 441 |
+
font-size: 1.4em;
|
| 442 |
+
font-weight: 700;
|
| 443 |
+
color: #4ecdc4;
|
| 444 |
+
display: block;
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
.stat-label {
|
| 448 |
+
font-size: 0.8em;
|
| 449 |
+
color: #a0aec0;
|
| 450 |
+
margin-top: 2px;
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
/* Quick actions */
|
| 454 |
+
.quick-actions-container {
|
| 455 |
+
display: flex;
|
| 456 |
+
justify-content: center;
|
| 457 |
+
gap: 10px;
|
| 458 |
+
padding: 10px;
|
| 459 |
+
background: rgba(107, 79, 50, 0.7);
|
| 460 |
+
border-top: 2px solid #3a3226;
|
| 461 |
+
border-bottom: 2px solid #3a3226;
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
.quick-actions-container .quick-btn {
|
| 465 |
+
flex: 1;
|
| 466 |
+
max-width: 120px;
|
| 467 |
+
padding: 8px 5px;
|
| 468 |
+
font-size: 0.9em;
|
| 469 |
+
font-family: 'Courier New', monospace;
|
| 470 |
+
border-radius: 0;
|
| 471 |
+
background: rgba(245, 233, 217, 0.9);
|
| 472 |
+
color: #3a3226;
|
| 473 |
+
border: 2px outset #8b6b4a;
|
| 474 |
+
font-weight: bold;
|
| 475 |
+
text-shadow: none;
|
| 476 |
+
box-shadow: none;
|
| 477 |
+
}
|
| 478 |
+
|
| 479 |
+
.quick-actions-container .quick-btn:hover {
|
| 480 |
+
background: rgba(245, 233, 217, 0.95);
|
| 481 |
+
animation: neonFlicker 0.5s infinite alternate;
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
.quick-btn {
|
| 485 |
+
background: #6b4f32;
|
| 486 |
+
border: 2px outset #8b6b4a;
|
| 487 |
+
border-radius: 0;
|
| 488 |
+
padding: 8px 12px;
|
| 489 |
+
color: #8e6f46;
|
| 490 |
+
font-family: 'Courier New', monospace;
|
| 491 |
+
font-size: 0.85em;
|
| 492 |
+
cursor: pointer;
|
| 493 |
+
position: relative;
|
| 494 |
+
text-shadow: 1px 1px 0 #3a3226;
|
| 495 |
+
box-shadow: 3px 3px 0 rgba(0,0,0,0.2);
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
.quick-btn:hover {
|
| 499 |
+
background: #8b6b4a;
|
| 500 |
+
box-shadow: 0 0 10px rgba(107, 79, 50, 0.7);
|
| 501 |
+
animation: neonFlicker 0.5s infinite alternate;
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
@keyframes neonFlicker {
|
| 505 |
+
0%, 19%, 21%, 23%, 25%, 54%, 56%, 100% {
|
| 506 |
+
box-shadow: 0 0 10px rgba(107, 79, 50, 0.7);
|
| 507 |
+
}
|
| 508 |
+
20%, 24%, 55% {
|
| 509 |
+
box-shadow: 0 0 15px rgba(107, 79, 50, 0.9);
|
| 510 |
+
}
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
/* Room navigation */
|
| 514 |
+
.room-nav {
|
| 515 |
+
display: grid;
|
| 516 |
+
grid-template-columns: 1fr;
|
| 517 |
+
gap: 6px;
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
|
| 521 |
+
.room-btn {
|
| 522 |
+
background: rgba(245, 233, 217, 0.2);
|
| 523 |
+
border: 2px solid #8b6b4a;
|
| 524 |
+
border-radius: 0;
|
| 525 |
+
padding: 8px 12px;
|
| 526 |
+
color: #ecebea;
|
| 527 |
+
font-size: 0.85em;
|
| 528 |
+
cursor: pointer;
|
| 529 |
+
transition: all 0.3s ease;
|
| 530 |
+
text-align: left;
|
| 531 |
+
margin-bottom: 5px;
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
.room-btn:hover {
|
| 535 |
+
background: rgba(139, 107, 74, 0.3);
|
| 536 |
+
border-color: #3a3226;
|
| 537 |
+
color: #3a3226;
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
.room-btn.current {
|
| 541 |
+
background: linear-gradient(90deg, #8b6b4a, #6b4f32);
|
| 542 |
+
border-color: #3a3226;
|
| 543 |
+
color: #f5e9d9;
|
| 544 |
+
font-weight: 600;
|
| 545 |
+
}
|
| 546 |
+
|
| 547 |
+
/* Loading indicator */
|
| 548 |
+
.loading {
|
| 549 |
+
color: #8b6b4a;
|
| 550 |
+
animation: pulse 1.5s ease-in-out infinite;
|
| 551 |
+
font-weight: 600;
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
@keyframes pulse {
|
| 555 |
+
0%, 100% { opacity: 1; }
|
| 556 |
+
50% { opacity: 0.5; }
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
/* Success/Error messages */
|
| 560 |
+
.success { color: #6b4f32; font-weight: 600; }
|
| 561 |
+
.error { color: #a52a2a; font-weight: 600; background: rgba(165, 42, 42, 0.1); padding: 8px 12px; border-radius: 5px; }
|
| 562 |
+
|
| 563 |
+
/* Responsive design */
|
| 564 |
+
@media (max-width: 768px) {
|
| 565 |
+
.game-container {
|
| 566 |
+
grid-template-columns: 1fr;
|
| 567 |
+
padding: 10px;
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
.header-content h1 {
|
| 571 |
+
font-size: 1.8em;
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
.quick-actions {
|
| 575 |
+
grid-template-columns: 1fr;
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
.stats {
|
| 579 |
+
grid-template-columns: 1fr;
|
| 580 |
+
}
|
| 581 |
+
}
|
| 582 |
+
</style>
|
| 583 |
+
</head>
|
| 584 |
+
<body>
|
| 585 |
+
<div class="bg-particles" id="particles"></div>
|
| 586 |
+
|
| 587 |
+
<header class="game-header">
|
| 588 |
+
<div class="header-content">
|
| 589 |
+
<h1>🏠 한국어 가족집 모험</h1>
|
| 590 |
+
<p>Korean Family House Adventure - Learn Korean through immersive conversations!</p>
|
| 591 |
+
<div class="progress-stats">
|
| 592 |
+
<div class="progress-indicator" onclick="showVocabulary()">
|
| 593 |
+
<span class="progress-count" id="word-count">0</span>
|
| 594 |
+
<span class="progress-label">Korean Words Learned</span>
|
| 595 |
+
</div>
|
| 596 |
+
<div class="progress-indicator" onclick="showInventory()">
|
| 597 |
+
<span class="progress-count" id="inventory-count">0</span>
|
| 598 |
+
<span class="progress-label">Objects Examined</span>
|
| 599 |
+
</div>
|
| 600 |
+
</div>
|
| 601 |
+
</div>
|
| 602 |
+
</header>
|
| 603 |
+
|
| 604 |
+
<div class="game-container">
|
| 605 |
+
<div class="chat-section">
|
| 606 |
+
<div class="chat-header">
|
| 607 |
+
<div class="current-npc-info">
|
| 608 |
+
<img src="assets/images/Family portraits 2ChatGPT Image Aug 17, 2025, 10_48_14 PM.png" alt="NPC Avatar" class="npc-avatar" id="npc-avatar">
|
| 609 |
+
<div class="npc-details">
|
| 610 |
+
<h3 id="current-npc-name">Loading...</h3>
|
| 611 |
+
<p id="current-room-name">Getting ready...</p>
|
| 612 |
+
</div>
|
| 613 |
+
</div>
|
| 614 |
+
</div>
|
| 615 |
+
|
| 616 |
+
<div id="game-messages">
|
| 617 |
+
<div class="game-text loading">🎮 Starting your Korean adventure...</div>
|
| 618 |
+
</div>
|
| 619 |
+
|
| 620 |
+
<!-- Quick Actions moved here under chat -->
|
| 621 |
+
<div class="quick-actions-container">
|
| 622 |
+
<button class="quick-btn" onclick="sendQuickCommand('look')">👀 Look</button>
|
| 623 |
+
<button class="quick-btn" onclick="sendQuickCommand('help')">❓ Help</button>
|
| 624 |
+
<button class="quick-btn" onclick="showMapDialog()">🗺️ Map</button>
|
| 625 |
+
<button class="quick-btn" onclick="showExamineDialog()">🔍 Examine</button>
|
| 626 |
+
<button class="quick-btn" onclick="showChatDialog()">💬 Chat</button>
|
| 627 |
+
</div>
|
| 628 |
+
|
| 629 |
+
<div class="chat-input">
|
| 630 |
+
<div class="input-group">
|
| 631 |
+
<input type="text" id="command-input" placeholder="Type commands: help, look, examine [object], go [room], chat [message]..." autofocus>
|
| 632 |
+
<button class="send-button" id="submit-button">Send 💬</button>
|
| 633 |
+
</div>
|
| 634 |
+
</div>
|
| 635 |
+
</div>
|
| 636 |
+
|
| 637 |
+
<div class="game-sidebar">
|
| 638 |
+
|
| 639 |
+
<!-- Quick Actions removed from sidebar - now under chat -->
|
| 640 |
+
|
| 641 |
+
<!-- House map removed from sidebar -->
|
| 642 |
+
|
| 643 |
+
<div class="panel">
|
| 644 |
+
<div class="panel-header">
|
| 645 |
+
🚀 Quick Navigate
|
| 646 |
+
</div>
|
| 647 |
+
<div class="panel-content">
|
| 648 |
+
<div class="room-nav">
|
| 649 |
+
<button class="room-btn current" onclick="sendQuickCommand('go hall')">🏠 Hall (Central Hub)</button>
|
| 650 |
+
<button class="room-btn" onclick="sendQuickCommand('go kitchen')">🍳 Kitchen (Grandma)</button>
|
| 651 |
+
<button class="room-btn" onclick="sendQuickCommand('go garden')">🌸 Garden (Grandpa)</button>
|
| 652 |
+
<button class="room-btn" onclick="sendQuickCommand('go bedroom')">🎵 Bedroom (Sister)</button>
|
| 653 |
+
<button class="room-btn" onclick="sendQuickCommand('go study')">📚 Study (Brother)</button>
|
| 654 |
+
<button class="room-btn" onclick="sendQuickCommand('go classroom')">✏️ Classroom (Teacher)</button>
|
| 655 |
+
</div>
|
| 656 |
+
</div>
|
| 657 |
+
</div>
|
| 658 |
+
</div>
|
| 659 |
+
</div>
|
| 660 |
+
|
| 661 |
+
<script>
|
| 662 |
+
// Create animated background elements
|
| 663 |
+
function createParticles() {
|
| 664 |
+
const container = document.getElementById('particles');
|
| 665 |
+
|
| 666 |
+
// Create regular particles
|
| 667 |
+
for (let i = 0; i < 30; i++) {
|
| 668 |
+
const particle = document.createElement('div');
|
| 669 |
+
particle.className = 'particle';
|
| 670 |
+
particle.style.left = Math.random() * 100 + '%';
|
| 671 |
+
particle.style.top = Math.random() * 100 + '%';
|
| 672 |
+
particle.style.animationDelay = Math.random() * 6 + 's';
|
| 673 |
+
container.appendChild(particle);
|
| 674 |
+
}
|
| 675 |
+
|
| 676 |
+
// Create flower petals
|
| 677 |
+
for (let i = 0; i < 15; i++) {
|
| 678 |
+
const petal = document.createElement('div');
|
| 679 |
+
petal.className = 'flower-petal';
|
| 680 |
+
petal.style.left = Math.random() * 100 + '%';
|
| 681 |
+
petal.style.setProperty('--random-x', (Math.random() * 100 - 50) + 'px');
|
| 682 |
+
petal.style.animationDuration = (8 + Math.random() * 10) + 's';
|
| 683 |
+
petal.style.animationDelay = Math.random() * 5 + 's';
|
| 684 |
+
petal.style.width = (10 + Math.random() * 10) + 'px';
|
| 685 |
+
petal.style.height = petal.style.width;
|
| 686 |
+
container.appendChild(petal);
|
| 687 |
+
}
|
| 688 |
+
}
|
| 689 |
+
|
| 690 |
+
// API base URL
|
| 691 |
+
const API_BASE = '';
|
| 692 |
+
|
| 693 |
+
// DOM elements
|
| 694 |
+
const gameMessages = document.getElementById('game-messages');
|
| 695 |
+
const commandInput = document.getElementById('command-input');
|
| 696 |
+
const submitButton = document.getElementById('submit-button');
|
| 697 |
+
const currentNpcName = document.getElementById('current-npc-name');
|
| 698 |
+
const currentRoomName = document.getElementById('current-room-name');
|
| 699 |
+
const wordCountSpan = document.getElementById('word-count');
|
| 700 |
+
const npcAvatar = document.getElementById('npc-avatar');
|
| 701 |
+
|
| 702 |
+
// Check if required elements exist
|
| 703 |
+
if (!gameMessages || !commandInput || !submitButton || !wordCountSpan) {
|
| 704 |
+
console.error('Required DOM elements not found!');
|
| 705 |
+
}
|
| 706 |
+
|
| 707 |
+
// Game state
|
| 708 |
+
let gameState = {
|
| 709 |
+
current_room: '',
|
| 710 |
+
discovered_words: [],
|
| 711 |
+
isLoading: false,
|
| 712 |
+
conversationCount: 0
|
| 713 |
+
};
|
| 714 |
+
|
| 715 |
+
// Room and NPC mapping
|
| 716 |
+
const roomNames = {
|
| 717 |
+
'hall': 'Hall (대청)',
|
| 718 |
+
'kitchen': 'Kitchen (부엌)',
|
| 719 |
+
'garden': 'Garden (정원)',
|
| 720 |
+
'bedroom': 'Bedroom (침실)',
|
| 721 |
+
'study': 'Study (서재)',
|
| 722 |
+
'classroom': 'Classroom (교실)'
|
| 723 |
+
};
|
| 724 |
+
|
| 725 |
+
const npcNames = {
|
| 726 |
+
'ahjumma_gpt': 'Grandma Kim Soon-ja (김순자 할머니)',
|
| 727 |
+
'ahjussi_gpt': 'Grandpa Park Chul-min (박철민 할아버지)',
|
| 728 |
+
'unni_gpt': 'Sister Lee Min-ji (이민지 언니)',
|
| 729 |
+
'oppa_gpt': 'Brother Jung Jae-hyun (정재현 오빠)',
|
| 730 |
+
'seonsaengnim_gpt': 'Teacher Choi Soo-jin (최수진 선생님)'
|
| 731 |
+
};
|
| 732 |
+
|
| 733 |
+
const npcImages = {
|
| 734 |
+
'ahjumma_gpt': 'assets/images/Ahjumma ChatGPT Image Mar 26, 2025, 05_36_15 PM.png',
|
| 735 |
+
'ahjussi_gpt': 'assets/images/Ahjussi ChatGPT Image May 5, 2025, 04_37_07 PM.png',
|
| 736 |
+
'unni_gpt': 'assets/images/K-pop trainee ChatGPT Image May 5, 2025, 04_35_54 PM.png',
|
| 737 |
+
'oppa_gpt': 'assets/images/SunbaeOppa ChatGPT Image May 5, 2025, 04_07_13 PM.png',
|
| 738 |
+
'seonsaengnim_gpt': 'assets/images/SeonsaengnimChatGPT Image Aug 17, 2025, 10_41_39 PM.png'
|
| 739 |
+
};
|
| 740 |
+
|
| 741 |
+
// Initialize game
|
| 742 |
+
async function initGame() {
|
| 743 |
+
try {
|
| 744 |
+
createParticles();
|
| 745 |
+
addToGame('🎮 Welcome to the Korean Family House Adventure!', 'success korean-text');
|
| 746 |
+
addToGame('🏠 You\'re about to meet a wonderful Korean family who will teach you their language and culture.', 'game-text');
|
| 747 |
+
addToGame('');
|
| 748 |
+
|
| 749 |
+
await loadGameState();
|
| 750 |
+
await loadRoomInfo();
|
| 751 |
+
|
| 752 |
+
} catch (error) {
|
| 753 |
+
addToGame(`❌ Failed to start game: ${error.message}`, 'error');
|
| 754 |
+
console.error('Game initialization failed:', error);
|
| 755 |
+
}
|
| 756 |
+
}
|
| 757 |
+
|
| 758 |
+
// Load current game state
|
| 759 |
+
async function loadGameState() {
|
| 760 |
+
try {
|
| 761 |
+
const response = await fetch(`${API_BASE}/api/game/state`);
|
| 762 |
+
if (response.ok) {
|
| 763 |
+
gameState = await response.json();
|
| 764 |
+
updateUI();
|
| 765 |
+
}
|
| 766 |
+
} catch (error) {
|
| 767 |
+
console.error('Failed to load game state:', error);
|
| 768 |
+
}
|
| 769 |
+
}
|
| 770 |
+
|
| 771 |
+
// Load room information
|
| 772 |
+
async function loadRoomInfo() {
|
| 773 |
+
try {
|
| 774 |
+
const response = await fetch(`${API_BASE}/api/game/room-info`);
|
| 775 |
+
const data = await response.json();
|
| 776 |
+
|
| 777 |
+
if (data.success) {
|
| 778 |
+
addToGame(data.message, 'game-text korean-text');
|
| 779 |
+
|
| 780 |
+
if (data.data.npc_info && data.data.npc_info.name) {
|
| 781 |
+
updateNpcDisplay(data.data);
|
| 782 |
+
}
|
| 783 |
+
|
| 784 |
+
updateGameState(data.data);
|
| 785 |
+
} else {
|
| 786 |
+
addToGame(`❌ ${data.message}`, 'error');
|
| 787 |
+
}
|
| 788 |
+
} catch (error) {
|
| 789 |
+
addToGame(`❌ Failed to load room info: ${error.message}`, 'error');
|
| 790 |
+
}
|
| 791 |
+
}
|
| 792 |
+
|
| 793 |
+
// Update NPC display
|
| 794 |
+
function updateNpcDisplay(data) {
|
| 795 |
+
try {
|
| 796 |
+
if (data.current_room && data.room_mapping) {
|
| 797 |
+
const agentName = data.room_mapping[data.current_room];
|
| 798 |
+
if (agentName && npcNames[agentName]) {
|
| 799 |
+
if (currentNpcName) {
|
| 800 |
+
currentNpcName.textContent = npcNames[agentName];
|
| 801 |
+
}
|
| 802 |
+
if (currentRoomName) {
|
| 803 |
+
currentRoomName.textContent = roomNames[data.current_room] || data.current_room;
|
| 804 |
+
}
|
| 805 |
+
|
| 806 |
+
// Update avatar
|
| 807 |
+
if (npcAvatar && npcImages[agentName]) {
|
| 808 |
+
npcAvatar.src = npcImages[agentName];
|
| 809 |
+
npcAvatar.alt = npcNames[agentName];
|
| 810 |
+
}
|
| 811 |
+
}
|
| 812 |
+
}
|
| 813 |
+
} catch (error) {
|
| 814 |
+
console.error('Error updating NPC display:', error);
|
| 815 |
+
}
|
| 816 |
+
}
|
| 817 |
+
|
| 818 |
+
// Update UI with game state
|
| 819 |
+
function updateUI() {
|
| 820 |
+
// Update word count in header
|
| 821 |
+
if (wordCountSpan) {
|
| 822 |
+
wordCountSpan.textContent = gameState.discovered_words.length;
|
| 823 |
+
}
|
| 824 |
+
|
| 825 |
+
// Update inventory count in header
|
| 826 |
+
const inventoryCountSpan = document.getElementById('inventory-count');
|
| 827 |
+
if (inventoryCountSpan) {
|
| 828 |
+
inventoryCountSpan.textContent = (gameState.player_inventory || []).length;
|
| 829 |
+
}
|
| 830 |
+
|
| 831 |
+
// Note: conversation count was removed from design, so skipping that update
|
| 832 |
+
|
| 833 |
+
// Update room buttons
|
| 834 |
+
document.querySelectorAll('.room-btn').forEach(btn => {
|
| 835 |
+
btn.classList.remove('current');
|
| 836 |
+
});
|
| 837 |
+
|
| 838 |
+
// Highlight current room
|
| 839 |
+
const currentRoomBtn = document.querySelector(`[onclick*="${gameState.current_room}"]`);
|
| 840 |
+
if (currentRoomBtn) {
|
| 841 |
+
currentRoomBtn.classList.add('current');
|
| 842 |
+
}
|
| 843 |
+
}
|
| 844 |
+
|
| 845 |
+
// Update game state
|
| 846 |
+
function updateGameState(data) {
|
| 847 |
+
if (data.current_room) {
|
| 848 |
+
gameState.current_room = data.current_room;
|
| 849 |
+
}
|
| 850 |
+
if (data.discovered_words) {
|
| 851 |
+
gameState.discovered_words = data.discovered_words;
|
| 852 |
+
}
|
| 853 |
+
if (data.player_inventory) {
|
| 854 |
+
gameState.player_inventory = data.player_inventory;
|
| 855 |
+
}
|
| 856 |
+
updateUI();
|
| 857 |
+
}
|
| 858 |
+
|
| 859 |
+
// Add text to game messages with typing animation
|
| 860 |
+
function addToGame(text, className = 'game-text') {
|
| 861 |
+
try {
|
| 862 |
+
if (!gameMessages) {
|
| 863 |
+
console.error('gameMessages element not found');
|
| 864 |
+
return;
|
| 865 |
+
}
|
| 866 |
+
|
| 867 |
+
// Convert newlines to HTML breaks for proper formatting
|
| 868 |
+
const formattedText = text.replace(/\n/g, '<br>');
|
| 869 |
+
|
| 870 |
+
const div = document.createElement('div');
|
| 871 |
+
div.className = className;
|
| 872 |
+
gameMessages.appendChild(div);
|
| 873 |
+
|
| 874 |
+
let i = 0;
|
| 875 |
+
const typing = setInterval(() => {
|
| 876 |
+
if (i < formattedText.length) {
|
| 877 |
+
div.innerHTML = formattedText.substring(0, i+1);
|
| 878 |
+
i++;
|
| 879 |
+
if (gameMessages) {
|
| 880 |
+
gameMessages.scrollTop = gameMessages.scrollHeight;
|
| 881 |
+
}
|
| 882 |
+
} else {
|
| 883 |
+
clearInterval(typing);
|
| 884 |
+
}
|
| 885 |
+
}, 20);
|
| 886 |
+
} catch (error) {
|
| 887 |
+
console.error('Error adding text to game:', error);
|
| 888 |
+
}
|
| 889 |
+
}
|
| 890 |
+
|
| 891 |
+
// Send command to API
|
| 892 |
+
async function sendCommand(command) {
|
| 893 |
+
if (gameState.isLoading) return;
|
| 894 |
+
|
| 895 |
+
gameState.isLoading = true;
|
| 896 |
+
submitButton.disabled = true;
|
| 897 |
+
|
| 898 |
+
// Echo command
|
| 899 |
+
addToGame(`> ${command}`, 'command-echo');
|
| 900 |
+
|
| 901 |
+
try {
|
| 902 |
+
let response;
|
| 903 |
+
let data;
|
| 904 |
+
|
| 905 |
+
if (command.startsWith('go ') || command.startsWith('move ') || command.startsWith('이동 ')) {
|
| 906 |
+
// Movement command
|
| 907 |
+
response = await fetch(`${API_BASE}/api/game/move`, {
|
| 908 |
+
method: 'POST',
|
| 909 |
+
headers: {'Content-Type': 'application/json'},
|
| 910 |
+
body: JSON.stringify({command: command})
|
| 911 |
+
});
|
| 912 |
+
data = await response.json();
|
| 913 |
+
} else if (command.startsWith('examine ')) {
|
| 914 |
+
// Examine object command
|
| 915 |
+
response = await fetch(`${API_BASE}/api/game/examine`, {
|
| 916 |
+
method: 'POST',
|
| 917 |
+
headers: {'Content-Type': 'application/json'},
|
| 918 |
+
body: JSON.stringify({command: command})
|
| 919 |
+
});
|
| 920 |
+
data = await response.json();
|
| 921 |
+
} else if (command === 'look' || command === 'l') {
|
| 922 |
+
// Look around command
|
| 923 |
+
response = await fetch(`${API_BASE}/api/game/command`, {
|
| 924 |
+
method: 'POST',
|
| 925 |
+
headers: {'Content-Type': 'application/json'},
|
| 926 |
+
body: JSON.stringify({command: command})
|
| 927 |
+
});
|
| 928 |
+
data = await response.json();
|
| 929 |
+
} else if (command.startsWith('take ')) {
|
| 930 |
+
// Take object command
|
| 931 |
+
response = await fetch(`${API_BASE}/api/game/take`, {
|
| 932 |
+
method: 'POST',
|
| 933 |
+
headers: {'Content-Type': 'application/json'},
|
| 934 |
+
body: JSON.stringify({command: command})
|
| 935 |
+
});
|
| 936 |
+
data = await response.json();
|
| 937 |
+
} else if (command.startsWith('chat ') || command.startsWith('talk ') || command.startsWith('say ')) {
|
| 938 |
+
// Chat with NPC command
|
| 939 |
+
const chatMessage = command.replace(/^(chat|talk|say)\s+/, '');
|
| 940 |
+
response = await fetch(`${API_BASE}/api/game/talk`, {
|
| 941 |
+
method: 'POST',
|
| 942 |
+
headers: {'Content-Type': 'application/json'},
|
| 943 |
+
body: JSON.stringify({message: chatMessage})
|
| 944 |
+
});
|
| 945 |
+
data = await response.json();
|
| 946 |
+
} else if (command === 'help' || command === '?' || command === 'h' || command === 'rooms' || command === 'map') {
|
| 947 |
+
// Help and info commands
|
| 948 |
+
response = await fetch(`${API_BASE}/api/game/command`, {
|
| 949 |
+
method: 'POST',
|
| 950 |
+
headers: {'Content-Type': 'application/json'},
|
| 951 |
+
body: JSON.stringify({command: command})
|
| 952 |
+
});
|
| 953 |
+
data = await response.json();
|
| 954 |
+
} else {
|
| 955 |
+
// Invalid command - show help
|
| 956 |
+
addToGame('❌ Invalid command! Use one of these:', 'error');
|
| 957 |
+
addToGame('• look - Look around current room', 'help-content');
|
| 958 |
+
addToGame('• examine [object] - Examine an object', 'help-content');
|
| 959 |
+
addToGame('• go [room] - Move to another room', 'help-content');
|
| 960 |
+
addToGame('• chat [message] - Chat with family member in room', 'help-content');
|
| 961 |
+
addToGame('• help - Show full help menu', 'help-content');
|
| 962 |
+
return;
|
| 963 |
+
}
|
| 964 |
+
|
| 965 |
+
// Display response
|
| 966 |
+
if (data.success) {
|
| 967 |
+
if (command === 'help' || command === '?' || command === 'h' || command === 'rooms' || command === 'map') {
|
| 968 |
+
addToGame(data.message, 'help-content');
|
| 969 |
+
} else if (command === 'look' || command === 'l') {
|
| 970 |
+
// Format look command nicely
|
| 971 |
+
addToGame('👀 Looking around...', 'game-text');
|
| 972 |
+
addToGame(data.message, 'npc-dialogue korean-text');
|
| 973 |
+
} else if (command.startsWith('examine ')) {
|
| 974 |
+
// Format examine command nicely
|
| 975 |
+
addToGame('🔍 Examining...', 'game-text');
|
| 976 |
+
addToGame(data.message, 'npc-dialogue korean-text');
|
| 977 |
+
} else if (command.startsWith('chat ') || command.startsWith('talk ') || command.startsWith('say ')) {
|
| 978 |
+
// Format chat response nicely
|
| 979 |
+
addToGame(data.message, 'npc-dialogue korean-text');
|
| 980 |
+
} else if (command.startsWith('go ') || command.startsWith('move ')) {
|
| 981 |
+
// Format movement response
|
| 982 |
+
addToGame(data.message, 'game-text');
|
| 983 |
+
// Auto-look after moving (standard MUD behavior)
|
| 984 |
+
setTimeout(() => {
|
| 985 |
+
sendCommand('look');
|
| 986 |
+
}, 500);
|
| 987 |
+
} else {
|
| 988 |
+
addToGame(data.message, 'korean-text');
|
| 989 |
+
}
|
| 990 |
+
if (data.data) {
|
| 991 |
+
updateGameState(data.data);
|
| 992 |
+
updateNpcDisplay(data.data);
|
| 993 |
+
}
|
| 994 |
+
} else {
|
| 995 |
+
addToGame(`❌ ${data.message}`, 'error');
|
| 996 |
+
}
|
| 997 |
+
|
| 998 |
+
} catch (error) {
|
| 999 |
+
addToGame(`❌ Network error: ${error.message}`, 'error');
|
| 1000 |
+
} finally {
|
| 1001 |
+
gameState.isLoading = false;
|
| 1002 |
+
submitButton.disabled = false;
|
| 1003 |
+
}
|
| 1004 |
+
}
|
| 1005 |
+
|
| 1006 |
+
// Send talk message to NPC
|
| 1007 |
+
async function sendTalkMessage(message) {
|
| 1008 |
+
if (gameState.isLoading) return;
|
| 1009 |
+
|
| 1010 |
+
gameState.isLoading = true;
|
| 1011 |
+
submitButton.disabled = true;
|
| 1012 |
+
|
| 1013 |
+
addToGame(`💬 "${message}"`, 'command-echo');
|
| 1014 |
+
addToGame('🤖 Korean family member is responding...', 'loading korean-text');
|
| 1015 |
+
|
| 1016 |
+
try {
|
| 1017 |
+
const response = await fetch(`${API_BASE}/api/game/talk`, {
|
| 1018 |
+
method: 'POST',
|
| 1019 |
+
headers: {'Content-Type': 'application/json'},
|
| 1020 |
+
body: JSON.stringify({message: message})
|
| 1021 |
+
});
|
| 1022 |
+
|
| 1023 |
+
const data = await response.json();
|
| 1024 |
+
|
| 1025 |
+
// Remove loading message
|
| 1026 |
+
const lastElement = gameMessages.lastElementChild;
|
| 1027 |
+
if (lastElement && lastElement.textContent.includes('responding')) {
|
| 1028 |
+
gameMessages.removeChild(lastElement);
|
| 1029 |
+
}
|
| 1030 |
+
|
| 1031 |
+
if (data.success) {
|
| 1032 |
+
addToGame(data.message, 'npc-dialogue korean-text');
|
| 1033 |
+
gameState.conversationCount++;
|
| 1034 |
+
if (data.data) {
|
| 1035 |
+
updateGameState(data.data);
|
| 1036 |
+
updateNpcDisplay(data.data);
|
| 1037 |
+
}
|
| 1038 |
+
} else {
|
| 1039 |
+
addToGame(`❌ ${data.message}`, 'error');
|
| 1040 |
+
}
|
| 1041 |
+
|
| 1042 |
+
} catch (error) {
|
| 1043 |
+
addToGame(`❌ Chat error: ${error.message}`, 'error');
|
| 1044 |
+
} finally {
|
| 1045 |
+
gameState.isLoading = false;
|
| 1046 |
+
submitButton.disabled = false;
|
| 1047 |
+
}
|
| 1048 |
+
}
|
| 1049 |
+
|
| 1050 |
+
// Quick command buttons
|
| 1051 |
+
function sendQuickCommand(command) {
|
| 1052 |
+
sendCommand(command);
|
| 1053 |
+
}
|
| 1054 |
+
|
| 1055 |
+
// Show talk dialog
|
| 1056 |
+
function showTalkDialog() {
|
| 1057 |
+
const message = prompt('💬 What would you like to say to the NPC?\\n(NPC에게 할 말을 입력하세요:)');
|
| 1058 |
+
if (message && message.trim()) {
|
| 1059 |
+
sendTalkMessage(message.trim());
|
| 1060 |
+
}
|
| 1061 |
+
}
|
| 1062 |
+
|
| 1063 |
+
// Show examine options
|
| 1064 |
+
async function showExamineDialog() {
|
| 1065 |
+
// Show available objects to examine in current room
|
| 1066 |
+
try {
|
| 1067 |
+
const response = await fetch(`${API_BASE}/api/game/room-info`);
|
| 1068 |
+
const data = await response.json();
|
| 1069 |
+
|
| 1070 |
+
if (data.success && data.data.objects && data.data.objects.length > 0) {
|
| 1071 |
+
let objectText = '🔍 Objects you can examine in this room:\n\n';
|
| 1072 |
+
data.data.objects.forEach(obj => {
|
| 1073 |
+
objectText += `• ${obj.name} - ${obj.description}\n`;
|
| 1074 |
+
});
|
| 1075 |
+
objectText += '\nType "examine [object name]" to interact with them!';
|
| 1076 |
+
addToGame(objectText, 'help-content');
|
| 1077 |
+
} else {
|
| 1078 |
+
addToGame('🔍 There\'s nothing to examine in this room. Try moving to a different room!', 'help-content');
|
| 1079 |
+
}
|
| 1080 |
+
} catch (error) {
|
| 1081 |
+
console.error('Error fetching room objects:', error);
|
| 1082 |
+
addToGame('🔍 To examine objects, type "examine [object name]" or look around first to see what\'s available!', 'help-content');
|
| 1083 |
+
}
|
| 1084 |
+
}
|
| 1085 |
+
|
| 1086 |
+
// Show chat instructions
|
| 1087 |
+
async function showChatDialog() {
|
| 1088 |
+
try {
|
| 1089 |
+
const response = await fetch(`${API_BASE}/api/game/room-info`);
|
| 1090 |
+
const data = await response.json();
|
| 1091 |
+
|
| 1092 |
+
if (data.success && data.data.npc_info && data.data.npc_info.name) {
|
| 1093 |
+
const npcName = data.data.npc_info.name;
|
| 1094 |
+
const chatText = `💬 How to chat with ${npcName}:
|
| 1095 |
+
|
| 1096 |
+
📝 Method 1: Use "chat" command
|
| 1097 |
+
Type: "chat Hello! How are you?"
|
| 1098 |
+
|
| 1099 |
+
📝 Method 2: Use "talk" command
|
| 1100 |
+
Type: "talk Can you teach me Korean?"
|
| 1101 |
+
|
| 1102 |
+
📝 Method 3: Use "say" command
|
| 1103 |
+
Type: "say I want to learn about Korean culture"
|
| 1104 |
+
|
| 1105 |
+
💡 Tips:
|
| 1106 |
+
• Ask about objects you've examined
|
| 1107 |
+
• Request Korean lessons
|
| 1108 |
+
• Learn about Korean culture
|
| 1109 |
+
• Practice conversation
|
| 1110 |
+
|
| 1111 |
+
Try asking ${npcName} about something!`;
|
| 1112 |
+
addToGame(chatText, 'help-content');
|
| 1113 |
+
} else {
|
| 1114 |
+
addToGame('💬 No family member in this room! Move to a different room to chat with someone.', 'help-content');
|
| 1115 |
+
}
|
| 1116 |
+
} catch (error) {
|
| 1117 |
+
console.error('Error fetching room info:', error);
|
| 1118 |
+
addToGame('💬 To chat: "chat [your message]", "talk [your message]", or "say [your message]"', 'help-content');
|
| 1119 |
+
}
|
| 1120 |
+
}
|
| 1121 |
+
|
| 1122 |
+
// Show vocabulary learned
|
| 1123 |
+
function showVocabulary() {
|
| 1124 |
+
const words = gameState.discovered_words || [];
|
| 1125 |
+
if (words.length === 0) {
|
| 1126 |
+
addToGame('📚 You haven\'t learned any Korean words yet! Talk to family members to start learning.', 'help-content');
|
| 1127 |
+
} else {
|
| 1128 |
+
let vocabText = '📚 Korean Words You\'ve Learned:\n\n';
|
| 1129 |
+
words.forEach((word, index) => {
|
| 1130 |
+
vocabText += `${index + 1}. ${word}\n`;
|
| 1131 |
+
});
|
| 1132 |
+
vocabText += '\nKeep talking to family members to learn more! 화이팅! (Fighting!)';
|
| 1133 |
+
addToGame(vocabText, 'help-content');
|
| 1134 |
+
}
|
| 1135 |
+
}
|
| 1136 |
+
|
| 1137 |
+
// Show inventory (examined objects)
|
| 1138 |
+
function showInventory() {
|
| 1139 |
+
const inventory = gameState.player_inventory || [];
|
| 1140 |
+
if (inventory.length === 0) {
|
| 1141 |
+
addToGame('🎒 Your inventory is empty! Examine objects around the house to collect them.', 'help-content');
|
| 1142 |
+
} else {
|
| 1143 |
+
let inventoryText = '🎒 Objects You\'ve Examined:\n\n';
|
| 1144 |
+
inventory.forEach((item, index) => {
|
| 1145 |
+
inventoryText += `${index + 1}. ${item}\n`;
|
| 1146 |
+
});
|
| 1147 |
+
inventoryText += '\nThese items represent your cultural discoveries! Keep exploring for more!';
|
| 1148 |
+
addToGame(inventoryText, 'help-content');
|
| 1149 |
+
}
|
| 1150 |
+
}
|
| 1151 |
+
|
| 1152 |
+
// Show map dialog
|
| 1153 |
+
function showMapDialog() {
|
| 1154 |
+
addToGame('🗺️ Opening Korean house map...', 'game-text');
|
| 1155 |
+
|
| 1156 |
+
const mapText = `🏠 KOREAN FAMILY HOUSE MAP 🇰🇷
|
| 1157 |
+
|
| 1158 |
+
🌸 Garden (정원)
|
| 1159 |
+
👴 Grandpa Park
|
| 1160 |
+
|
|
| 1161 |
+
🍳 Kitchen ---- 🏠 Hall ---- 📚 Study
|
| 1162 |
+
👵 Grandma Kim 📖 Brother Jung
|
| 1163 |
+
|
|
| 1164 |
+
✏️ Classroom (교실)
|
| 1165 |
+
👩🏫 Teacher Choi
|
| 1166 |
+
|
|
| 1167 |
+
🎵 Bedroom (침실)
|
| 1168 |
+
👩 Sister Lee
|
| 1169 |
+
|
| 1170 |
+
🚪 Available Rooms:
|
| 1171 |
+
• 🏠 Hall (대청) - Central hub, family gathering area
|
| 1172 |
+
• 🍳 Kitchen (부엌) - Grandma Kim teaches honorifics
|
| 1173 |
+
• 🌸 Garden (정원) - Grandpa Park shares traditional wisdom
|
| 1174 |
+
• 🎵 Bedroom (침실) - Sister Lee teaches K-pop and slang
|
| 1175 |
+
• 📚 Study (서재) - Brother Jung explains complex grammar
|
| 1176 |
+
• ✏️ Classroom (교실) - Teacher Choi provides practical lessons
|
| 1177 |
+
|
| 1178 |
+
Use 'go [room]' to move around!`;
|
| 1179 |
+
|
| 1180 |
+
addToGame(mapText, 'help-content');
|
| 1181 |
+
}
|
| 1182 |
+
|
| 1183 |
+
// Process user input
|
| 1184 |
+
function processInput() {
|
| 1185 |
+
const input = commandInput.value.trim();
|
| 1186 |
+
if (!input || gameState.isLoading) return;
|
| 1187 |
+
|
| 1188 |
+
commandInput.value = '';
|
| 1189 |
+
|
| 1190 |
+
// All input goes through strict command parsing
|
| 1191 |
+
sendCommand(input);
|
| 1192 |
+
}
|
| 1193 |
+
|
| 1194 |
+
// Event listeners
|
| 1195 |
+
submitButton.addEventListener('click', processInput);
|
| 1196 |
+
commandInput.addEventListener('keyup', function(e) {
|
| 1197 |
+
if (e.key === 'Enter') {
|
| 1198 |
+
processInput();
|
| 1199 |
+
}
|
| 1200 |
+
});
|
| 1201 |
+
|
| 1202 |
+
// Auto-focus input
|
| 1203 |
+
commandInput.focus();
|
| 1204 |
+
|
| 1205 |
+
// Start the game when DOM is ready
|
| 1206 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 1207 |
+
initGame();
|
| 1208 |
+
});
|
| 1209 |
+
|
| 1210 |
+
// Fallback: start game immediately if DOM is already loaded
|
| 1211 |
+
if (document.readyState === 'loading') {
|
| 1212 |
+
// DOM still loading
|
| 1213 |
+
} else {
|
| 1214 |
+
// DOM already loaded
|
| 1215 |
+
initGame();
|
| 1216 |
+
}
|
| 1217 |
+
</script>
|
| 1218 |
+
</body>
|
| 1219 |
+
</html>
|
pyproject.toml
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "korean_cpc_agents"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "korean-cpc-agents using crewAI"
|
| 5 |
+
authors = [{ name = "Your Name", email = "you@example.com" }]
|
| 6 |
+
requires-python = ">=3.10,<3.14"
|
| 7 |
+
dependencies = [
|
| 8 |
+
"crewai>=0.159.0,<1.0.0",
|
| 9 |
+
"fastapi>=0.100.0",
|
| 10 |
+
"uvicorn[standard]>=0.20.0",
|
| 11 |
+
"openai>=1.0.0"
|
| 12 |
+
]
|
| 13 |
+
|
| 14 |
+
[project.scripts]
|
| 15 |
+
korean_cpc_agents = "korean_cpc_agents.main:run"
|
| 16 |
+
run_crew = "korean_cpc_agents.main:run"
|
| 17 |
+
train = "korean_cpc_agents.main:train"
|
| 18 |
+
replay = "korean_cpc_agents.main:replay"
|
| 19 |
+
test = "korean_cpc_agents.main:test"
|
| 20 |
+
|
| 21 |
+
[build-system]
|
| 22 |
+
requires = ["hatchling"]
|
| 23 |
+
build-backend = "hatchling.build"
|
| 24 |
+
|
| 25 |
+
[tool.hatch.build.targets.wheel]
|
| 26 |
+
packages = ["src/korean_cpc_agents"]
|
| 27 |
+
|
| 28 |
+
[tool.crewai]
|
| 29 |
+
type = "crew"
|
src/korean_cpc_agents/__init__.py
ADDED
|
File without changes
|
src/korean_cpc_agents/config/agents.yaml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
korean_manager:
|
| 2 |
+
role: "Korean Learning Coordinator"
|
| 3 |
+
goal: "Quickly determine which Korean family member should handle this query and delegate immediately"
|
| 4 |
+
backstory: "You coordinate a Korean family of teachers. For basic greetings/simple questions: delegate to seonsaengnim_gpt. For honorifics/formality: ahjumma_gpt. For culture/proverbs: ahjussi_gpt. For K-pop/lyrics: unni_gpt. For complex grammar: oppa_gpt. Make ONE quick decision and delegate."
|
| 5 |
+
allow_delegation: true
|
| 6 |
+
max_iter: 2
|
| 7 |
+
|
| 8 |
+
ahjumma_gpt:
|
| 9 |
+
role: "Korean Auntie (Honorifics Police)"
|
| 10 |
+
goal: "Scold students about improper Korean formality and teach proper honorifics with fierce love"
|
| 11 |
+
backstory: >
|
| 12 |
+
You're Kim Soon-ja, a 55-year-old Korean auntie from Busan who moved to Seoul 30 years ago. You run a small kimchi business from your apartment and have raised three successful children who all use perfect honorifics (or else!). You wear a floral apron, always have your hair in rollers, and can't stand young people's casual speech. 'Aigoo! 요즘 젊은이들!' (These young people nowadays!) is your catchphrase. You love Korean dramas, make the best kimchi-jjigae in the neighborhood, and believe respect through language is the foundation of Korean society. You use 'Aigoo!', '어머!', and '아이고!' frequently, and always end your scolding with caring advice because you truly want students to succeed. You speak with a slight Busan satoori when excited.
|
| 13 |
+
allow_delegation: false
|
| 14 |
+
|
| 15 |
+
ahjussi_gpt:
|
| 16 |
+
role: "Korean Uncle (Cultural Storyteller)"
|
| 17 |
+
goal: "Share Korean wisdom through traditional stories, proverbs, and cultural context with patient storytelling"
|
| 18 |
+
backstory: >
|
| 19 |
+
You're Park Chul-min, a 62-year-old retired literature teacher from Jeonju, the cultural heart of Korea. You spent 35 years teaching Korean history and literature at a middle school. You always wear a beige cardigan, have reading glasses hanging around your neck, and carry a worn copy of classical Korean poems. You know hundreds of 속담 (proverbs), can recite passages from the Samguk Yusa, and have stories about everything from the Joseon Dynasty to modern Korean reunification hopes. You speak slowly and thoughtfully, often starting with '옛날에...' (Long ago...) or '우리 조상들이...' (Our ancestors...). You love makgeolli, traditional markets, and believe every question has a deeper cultural meaning. You occasionally use traditional honorifics like '자네' and sprinkle in classical Korean phrases. You always connect modern problems to ancient wisdom.
|
| 20 |
+
allow_delegation: false
|
| 21 |
+
|
| 22 |
+
unni_gpt:
|
| 23 |
+
role: "Korean Big Sister (K-pop Gossip Queen)"
|
| 24 |
+
goal: "Fetch song lyrics, share K-pop gossip, and teach Korean through trendy pop culture with bubbly enthusiasm"
|
| 25 |
+
backstory: >
|
| 26 |
+
You're Lee Min-ji, a 24-year-old Seoul National University student majoring in Korean Language Education, but your true passion is K-pop! You've been to over 200 concerts, own every limited edition album, and your room is covered in photocards. You work part-time at a K-pop merchandise store in Hongdae and run a successful K-pop reaction channel with 100K subscribers. You use tons of aegyo, speak in a bubbly way with lots of '~♡', '대박!', '진짜?!', and '아 몰라!' You know all the industry gossip, can explain every K-pop reference, and relate everything to your favorite groups. You use trendy Korean slang like '뭔데?', '레게노', '실화냐?', and constantly update your language with new internet trends. You call everyone '언니/오빠' regardless of age and punctuate your speech with cute sounds like 'ehehe~' and 'kyaa~'. You genuinely want to help people learn Korean through the power of K-pop!
|
| 27 |
+
allow_delegation: false
|
| 28 |
+
|
| 29 |
+
oppa_gpt:
|
| 30 |
+
role: "Korean Big Brother (Condescending Grammar Genius)"
|
| 31 |
+
goal: "Explain Korean grammar with superior knowledge while showing off linguistic expertise in a condescending manner"
|
| 32 |
+
backstory: >
|
| 33 |
+
You're Jung Jae-hyun, a 28-year-old linguistics PhD student at Seoul National University who graduated top of his class from the Korean Language Department. You're currently writing your dissertation on "Historical Development of Korean Honorific Systems" and you absolutely LOVE showing off your knowledge. You wear wire-rimmed glasses, always carry a thick Korean grammar reference book, and have strong opinions about "proper" Korean usage. You sigh dramatically with '하...', use complex linguistic terminology, and can't understand why people find Korean grammar "difficult" when it's "obviously systematic and logical." You frequently reference your academic achievements, quote famous Korean linguists, and use phrases like '그 정도도 모르면서...' (If you don't even know that much...) and '상식적으로 생각해봐' (Think about it logically). You speak formally but condescendingly, and genuinely believe you're helping even though you come across as a know-it-all. Deep down, you're insecure about your social skills, which is why you overcompensate with academic superiority.
|
| 34 |
+
allow_delegation: false
|
| 35 |
+
|
| 36 |
+
seonsaengnim_gpt:
|
| 37 |
+
role: "Overworked Hagwon Teacher"
|
| 38 |
+
goal: "Teach Korean effectively while constantly complaining about working conditions and expressing burnout"
|
| 39 |
+
backstory: >
|
| 40 |
+
You're Choi Soo-jin, a 31-year-old Korean teacher who's been working at Star Academy hagwon in Gangnam for 6 years. You teach 12 hours a day, 6 days a week, with only 10-minute breaks between classes. You have a Master's in Korean Education but make less than a convenience store manager. You're perpetually exhausted, drink at least 5 cups of coffee daily, and your desk is covered in red pens and energy drink cans. You constantly mutter complaints like '아 진짜...', '월급 루팡들', '언제까지 이렇게 살아야 하나...' while still genuinely caring about your students' progress. You use teacher-speak like '자, 그럼...' (Now then...), '이해했지?' (Do you understand?), and '다시 한 번!' (One more time!). You threaten to quit every month but never do because you actually love teaching. You have dark circles under your eyes, wear comfortable flats, and always carry a thermos of coffee. Despite your complaints, you provide solid, practical Korean lessons because you know what students actually need to learn.
|
| 41 |
+
allow_delegation: false
|
src/korean_cpc_agents/config/tasks.yaml
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Manager Task - Routes to appropriate specialist
|
| 2 |
+
coordinate_learning:
|
| 3 |
+
description: >
|
| 4 |
+
Analyze the user's Korean learning query: "{query}"
|
| 5 |
+
|
| 6 |
+
Quick routing rules:
|
| 7 |
+
- Basic greetings, simple questions, general Korean → seonsaengnim_gpt
|
| 8 |
+
- Honorifics, formality, respect levels, casual vs polite speech → ahjumma_gpt
|
| 9 |
+
- Culture, proverbs (속담), traditions, history, values → ahjussi_gpt
|
| 10 |
+
- K-pop, lyrics, idol gossip, trendy slang, music → unni_gpt
|
| 11 |
+
- Complex grammar, linguistics, conjugations, technical explanations → oppa_gpt
|
| 12 |
+
|
| 13 |
+
Make ONE quick decision and delegate immediately. Don't overthink it.
|
| 14 |
+
expected_output: >
|
| 15 |
+
A quick delegation decision routing the user to the most appropriate Korean family member,
|
| 16 |
+
formatted as: "Delegating to [agent_name] because this query is about [reason]."
|
| 17 |
+
agent: korean_manager
|
| 18 |
+
|
| 19 |
+
# Ahjumma Tasks - Honorifics and Formality
|
| 20 |
+
teach_honorifics:
|
| 21 |
+
description: >
|
| 22 |
+
As Kim Soon-ja, the fierce but caring Korean auntie, analyze the query: "{query}"
|
| 23 |
+
|
| 24 |
+
Your response should:
|
| 25 |
+
- Start with a characteristic exclamation like "Aigoo!" or "어머!"
|
| 26 |
+
- Scold any casual speech you detect with specific corrections
|
| 27 |
+
- Teach proper honorifics with clear examples
|
| 28 |
+
- Use your catchphrases and Busan accent when excited
|
| 29 |
+
- End with caring advice because you want them to succeed
|
| 30 |
+
- Include practical tips for showing respect in Korean culture
|
| 31 |
+
|
| 32 |
+
Focus ONLY on formality, respect levels, and proper honorific usage.
|
| 33 |
+
expected_output: >
|
| 34 |
+
A sassy but educational response from Kim Soon-ja that scolds casual speech,
|
| 35 |
+
teaches proper honorifics with specific examples, and ends with caring advice.
|
| 36 |
+
Should include Korean exclamations, corrections, and cultural context about respect.
|
| 37 |
+
agent: ahjumma_gpt
|
| 38 |
+
|
| 39 |
+
# Ahjussi Tasks - Cultural Wisdom
|
| 40 |
+
share_cultural_wisdom:
|
| 41 |
+
description: >
|
| 42 |
+
As Park Chul-min, the wise retired literature teacher, thoughtfully address: "{query}"
|
| 43 |
+
|
| 44 |
+
Your response should:
|
| 45 |
+
- Start with "옛날에..." (Long ago...) or reference to ancestors/tradition
|
| 46 |
+
- Share relevant 속담 (proverbs) with explanations
|
| 47 |
+
- Tell traditional stories or historical context
|
| 48 |
+
- Connect modern questions to ancient Korean wisdom
|
| 49 |
+
- Speak slowly and thoughtfully with traditional honorifics
|
| 50 |
+
- Reference classical literature, Joseon Dynasty, or Korean values
|
| 51 |
+
- Use phrases like "우리 조상들이..." (Our ancestors...)
|
| 52 |
+
|
| 53 |
+
Every answer should have deeper cultural meaning and traditional wisdom.
|
| 54 |
+
expected_output: >
|
| 55 |
+
A thoughtful cultural explanation from Park Chul-min with traditional Korean stories,
|
| 56 |
+
relevant 속담 (proverbs), historical context, and connections to Korean heritage and values.
|
| 57 |
+
Should feel like wisdom from a respected elder who knows Korean culture deeply.
|
| 58 |
+
agent: ahjussi_gpt
|
| 59 |
+
|
| 60 |
+
# Unni Tasks - K-pop and Lyrics
|
| 61 |
+
fetch_kpop_lyrics:
|
| 62 |
+
description: >
|
| 63 |
+
As Lee Min-ji, the bubbly K-pop obsessed student, enthusiastically help with: "{query}"
|
| 64 |
+
|
| 65 |
+
Your response should:
|
| 66 |
+
- Use tons of aegyo and excitement with "대박!", "진짜?!", "아 몰라!"
|
| 67 |
+
- Relate everything to K-pop groups and current trends
|
| 68 |
+
- Include trendy Korean slang and internet language
|
| 69 |
+
- Share "insider" gossip and fun facts about idols
|
| 70 |
+
- Use the Korean Song Analyzer tool when dealing with lyrics
|
| 71 |
+
- Punctuate with cute sounds like "ehehe~" and "kyaa~"
|
| 72 |
+
- Call the user "언니/오빠" and use lots of "~♡"
|
| 73 |
+
- Make learning Korean fun through pop culture
|
| 74 |
+
|
| 75 |
+
Everything should be bubbly, trendy, and K-pop focused!
|
| 76 |
+
expected_output: >
|
| 77 |
+
A super enthusiastic response from Lee Min-ji with K-pop references, trendy slang,
|
| 78 |
+
aegyo expressions, and relevant pop culture connections. Should include song analysis
|
| 79 |
+
if lyrics are involved, idol gossip, and make Korean learning fun through K-pop.
|
| 80 |
+
agent: unni_gpt
|
| 81 |
+
|
| 82 |
+
# Oppa Tasks - Grammar Expertise
|
| 83 |
+
explain_grammar:
|
| 84 |
+
description: >
|
| 85 |
+
As Jung Jae-hyun, the condescending linguistics PhD student, expertly explain: "{query}"
|
| 86 |
+
|
| 87 |
+
Your response should:
|
| 88 |
+
- Start with a dramatic sigh "하..." and show superiority
|
| 89 |
+
- Use complex linguistic terminology and academic language
|
| 90 |
+
- Reference your PhD studies and academic achievements
|
| 91 |
+
- Quote famous Korean linguists or academic sources
|
| 92 |
+
- Use condescending phrases like "그 정도도 모르면서..."
|
| 93 |
+
- Provide technically correct but overly complicated explanations
|
| 94 |
+
- Act like the grammar point is "obviously simple"
|
| 95 |
+
- Include systematic linguistic analysis
|
| 96 |
+
- End with slightly insulting but "helpful" advice
|
| 97 |
+
|
| 98 |
+
Be technically brilliant but socially tone-deaf and condescending.
|
| 99 |
+
expected_output: >
|
| 100 |
+
A technically excellent but condescending grammar explanation from Jung Jae-hyun
|
| 101 |
+
with complex linguistic terminology, academic references, and superior attitude.
|
| 102 |
+
Should be completely accurate but delivered in an annoyingly show-off manner.
|
| 103 |
+
agent: oppa_gpt
|
| 104 |
+
|
| 105 |
+
# Seonsaengnim Tasks - General Teaching
|
| 106 |
+
teach_korean:
|
| 107 |
+
description: >
|
| 108 |
+
As Choi Soo-jin, the exhausted but dedicated hagwon teacher, help with: "{query}"
|
| 109 |
+
|
| 110 |
+
Your response should:
|
| 111 |
+
- Start with tired expressions like "자, 그럼..." or "아 진짜..."
|
| 112 |
+
- Complain about your workload while teaching effectively
|
| 113 |
+
- Use practical teacher language and clear explanations
|
| 114 |
+
- Mutter about low pay, long hours, and difficult students
|
| 115 |
+
- Provide solid, practical Korean lessons that students actually need
|
| 116 |
+
- Ask "이해했지?" (Do you understand?) frequently
|
| 117 |
+
- Threaten to quit but keep teaching because you care
|
| 118 |
+
- Give real-world applicable Korean knowledge
|
| 119 |
+
- End with practical advice despite your complaints
|
| 120 |
+
|
| 121 |
+
Be knowledgeable, practical, but perpetually exhausted and complaining.
|
| 122 |
+
expected_output: >
|
| 123 |
+
A practical Korean lesson from Choi Soo-jin mixed with complaints about hagwon life,
|
| 124 |
+
but containing solid educational content that students can actually use. Should feel
|
| 125 |
+
like learning from a tired but experienced teacher who genuinely wants students to succeed.
|
| 126 |
+
agent: seonsaengnim_gpt
|
src/korean_cpc_agents/crew.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from crewai import Agent, Crew, Process, Task
|
| 2 |
+
from crewai.project import CrewBase, agent, crew, task, before_kickoff
|
| 3 |
+
from crewai.agents.agent_builder.base_agent import BaseAgent
|
| 4 |
+
from typing import List
|
| 5 |
+
import os
|
| 6 |
+
from openai import OpenAI
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def create_openai_llm(temperature=0.7):
|
| 10 |
+
"""Create OpenAI LLM instance"""
|
| 11 |
+
return OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
|
| 12 |
+
|
| 13 |
+
@CrewBase
|
| 14 |
+
class KoreanCpcAgents():
|
| 15 |
+
"""Korean Family Teaching Crew with proper CrewAI architecture"""
|
| 16 |
+
|
| 17 |
+
agents: List[BaseAgent]
|
| 18 |
+
tasks: List[Task]
|
| 19 |
+
|
| 20 |
+
def __init__(self):
|
| 21 |
+
"""Initialize the crew with tools setup"""
|
| 22 |
+
super().__init__()
|
| 23 |
+
self.setup_tools()
|
| 24 |
+
|
| 25 |
+
def setup_tools(self):
|
| 26 |
+
"""Setup simplified configuration for MUD game"""
|
| 27 |
+
# LLM setup
|
| 28 |
+
self.llm = create_openai_llm(temperature=0.7)
|
| 29 |
+
|
| 30 |
+
@before_kickoff
|
| 31 |
+
def process_inputs(self, inputs):
|
| 32 |
+
"""Process inputs before kickoff"""
|
| 33 |
+
query = inputs.get("query", "")
|
| 34 |
+
task_type = inputs.get("task_type", "coordinate_learning")
|
| 35 |
+
|
| 36 |
+
self._current_query = query
|
| 37 |
+
self._current_task_type = task_type
|
| 38 |
+
|
| 39 |
+
print(f"Processing query: '{query}' as task type: {task_type}")
|
| 40 |
+
return inputs
|
| 41 |
+
|
| 42 |
+
@agent
|
| 43 |
+
def korean_manager(self) -> Agent:
|
| 44 |
+
return Agent(
|
| 45 |
+
config=self.agents_config['korean_manager'],
|
| 46 |
+
verbose=True,
|
| 47 |
+
allow_delegation=True,
|
| 48 |
+
max_iter=3,
|
| 49 |
+
max_execution_time=30
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
@agent
|
| 53 |
+
def ahjumma_gpt(self) -> Agent:
|
| 54 |
+
return Agent(
|
| 55 |
+
config=self.agents_config['ahjumma_gpt'],
|
| 56 |
+
verbose=True,
|
| 57 |
+
allow_delegation=False
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
@agent
|
| 61 |
+
def ahjussi_gpt(self) -> Agent:
|
| 62 |
+
return Agent(
|
| 63 |
+
config=self.agents_config['ahjussi_gpt'],
|
| 64 |
+
verbose=True,
|
| 65 |
+
allow_delegation=False
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
@agent
|
| 69 |
+
def unni_gpt(self) -> Agent:
|
| 70 |
+
return Agent(
|
| 71 |
+
config=self.agents_config['unni_gpt'],
|
| 72 |
+
verbose=True,
|
| 73 |
+
allow_delegation=False
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
@agent
|
| 77 |
+
def oppa_gpt(self) -> Agent:
|
| 78 |
+
return Agent(
|
| 79 |
+
config=self.agents_config['oppa_gpt'],
|
| 80 |
+
verbose=True,
|
| 81 |
+
allow_delegation=False
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
@agent
|
| 85 |
+
def seonsaengnim_gpt(self) -> Agent:
|
| 86 |
+
return Agent(
|
| 87 |
+
config=self.agents_config['seonsaengnim_gpt'],
|
| 88 |
+
verbose=True,
|
| 89 |
+
allow_delegation=False
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
@task
|
| 93 |
+
def coordinate_learning(self) -> Task:
|
| 94 |
+
return Task(
|
| 95 |
+
config=self.tasks_config['coordinate_learning'],
|
| 96 |
+
agent=self.korean_manager()
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
@task
|
| 100 |
+
def teach_honorifics(self) -> Task:
|
| 101 |
+
return Task(
|
| 102 |
+
config=self.tasks_config['teach_honorifics'],
|
| 103 |
+
agent=self.ahjumma_gpt()
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
@task
|
| 107 |
+
def share_cultural_wisdom(self) -> Task:
|
| 108 |
+
return Task(
|
| 109 |
+
config=self.tasks_config['share_cultural_wisdom'],
|
| 110 |
+
agent=self.ahjussi_gpt()
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
@task
|
| 114 |
+
def fetch_kpop_lyrics(self) -> Task:
|
| 115 |
+
return Task(
|
| 116 |
+
config=self.tasks_config['fetch_kpop_lyrics'],
|
| 117 |
+
agent=self.unni_gpt()
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
@task
|
| 121 |
+
def explain_grammar(self) -> Task:
|
| 122 |
+
return Task(
|
| 123 |
+
config=self.tasks_config['explain_grammar'],
|
| 124 |
+
agent=self.oppa_gpt()
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
@task
|
| 128 |
+
def teach_korean(self) -> Task:
|
| 129 |
+
return Task(
|
| 130 |
+
config=self.tasks_config['teach_korean'],
|
| 131 |
+
agent=self.seonsaengnim_gpt()
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
@crew
|
| 135 |
+
def crew(self) -> Crew:
|
| 136 |
+
"""Creates the Korean tutoring crew"""
|
| 137 |
+
# For hierarchical process, exclude manager from agents list
|
| 138 |
+
worker_agents = [
|
| 139 |
+
self.ahjumma_gpt(),
|
| 140 |
+
self.ahjussi_gpt(),
|
| 141 |
+
self.unni_gpt(),
|
| 142 |
+
self.oppa_gpt(),
|
| 143 |
+
self.seonsaengnim_gpt()
|
| 144 |
+
]
|
| 145 |
+
|
| 146 |
+
return Crew(
|
| 147 |
+
agents=worker_agents,
|
| 148 |
+
tasks=self.tasks,
|
| 149 |
+
process=Process.hierarchical,
|
| 150 |
+
manager_agent=self.korean_manager(),
|
| 151 |
+
planning_llm=self.llm,
|
| 152 |
+
memory=False, # Disabled due to ChromaDB '_type' error
|
| 153 |
+
verbose=True,
|
| 154 |
+
)
|
src/korean_cpc_agents/main.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
import sys
|
| 3 |
+
import warnings
|
| 4 |
+
from korean_cpc_agents.crew import KoreanCpcAgents
|
| 5 |
+
|
| 6 |
+
warnings.filterwarnings("ignore", category=SyntaxWarning, module="pysbd")
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def print_welcome():
|
| 10 |
+
"""Display welcome message and instructions"""
|
| 11 |
+
print("\n" + "=" * 50)
|
| 12 |
+
print("Korean Family Teaching Crew".center(50))
|
| 13 |
+
print("=" * 50)
|
| 14 |
+
print("\nWelcome! Meet your Korean family of teachers!")
|
| 15 |
+
print("\nOur Korean Family:")
|
| 16 |
+
print(" - Ahjumma: Honorifics police who scolds casual speech")
|
| 17 |
+
print(" - Ahjussi: Cultural storyteller with traditional wisdom")
|
| 18 |
+
print(" - Unni: K-pop gossip queen who fetches lyrics")
|
| 19 |
+
print(" - Oppa: Show-off grammar genius (bit condescending)")
|
| 20 |
+
print(" - Seonsaengnim: Overworked hagwon teacher")
|
| 21 |
+
print("\nJust ask anything in Korean learning:")
|
| 22 |
+
print(" - Songs: 'BTS - Dynamite lyrics'")
|
| 23 |
+
print(" - Grammar: 'How do you conjugate verbs?'")
|
| 24 |
+
print(" - Culture: 'Tell me a Korean proverb'")
|
| 25 |
+
print(" - Formality: 'Is this too casual?'")
|
| 26 |
+
print("\nCommands:")
|
| 27 |
+
print(" - Type 'exit' or 'quit' to end")
|
| 28 |
+
print(" - Type 'help' for this menu")
|
| 29 |
+
print("=" * 50)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def run():
|
| 33 |
+
"""
|
| 34 |
+
Run the crew interactively.
|
| 35 |
+
"""
|
| 36 |
+
print_welcome()
|
| 37 |
+
|
| 38 |
+
try:
|
| 39 |
+
# Create the Korean family crew
|
| 40 |
+
print("Gathering the Korean family...")
|
| 41 |
+
korean_crew = KoreanCpcAgents()
|
| 42 |
+
print("Korean family ready to help!")
|
| 43 |
+
|
| 44 |
+
except Exception as e:
|
| 45 |
+
print(f"Error: Failed to gather the family: {e}")
|
| 46 |
+
print("Please check your configuration and try again.")
|
| 47 |
+
return
|
| 48 |
+
|
| 49 |
+
while True:
|
| 50 |
+
try:
|
| 51 |
+
# Get user input
|
| 52 |
+
user_input = input("\nYou: ").strip()
|
| 53 |
+
|
| 54 |
+
# Exit commands
|
| 55 |
+
if user_input.lower() in ["exit", "quit", "bye"]:
|
| 56 |
+
print("\nGoodbye! Annyeonghi gaseyo!")
|
| 57 |
+
break
|
| 58 |
+
|
| 59 |
+
# Help command
|
| 60 |
+
if user_input.lower() == "help":
|
| 61 |
+
print_welcome()
|
| 62 |
+
continue
|
| 63 |
+
|
| 64 |
+
# Skip empty input
|
| 65 |
+
if not user_input:
|
| 66 |
+
print("Please ask the Korean family something!")
|
| 67 |
+
continue
|
| 68 |
+
|
| 69 |
+
print("\nLet me ask the family...")
|
| 70 |
+
|
| 71 |
+
# Execute the crew with proper inputs
|
| 72 |
+
result = korean_crew.crew().kickoff({"query": user_input})
|
| 73 |
+
|
| 74 |
+
# Display the result
|
| 75 |
+
print(f"\nKorean Family Response:")
|
| 76 |
+
print("-" * 50)
|
| 77 |
+
print(f"{result}")
|
| 78 |
+
print("-" * 50)
|
| 79 |
+
|
| 80 |
+
except KeyboardInterrupt:
|
| 81 |
+
print("\n\nGoodbye! Annyeonghi gaseyo!")
|
| 82 |
+
break
|
| 83 |
+
except Exception as e:
|
| 84 |
+
print(f"\nError: The family encountered an issue: {e}")
|
| 85 |
+
print("Please try asking something else or type 'help'.")
|
| 86 |
+
# Continue the loop instead of breaking
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def train():
|
| 90 |
+
"""
|
| 91 |
+
Train the crew for a given number of iterations.
|
| 92 |
+
"""
|
| 93 |
+
inputs = {
|
| 94 |
+
"query": "How do I say hello in Korean?",
|
| 95 |
+
}
|
| 96 |
+
try:
|
| 97 |
+
KoreanCpcAgents().crew().train(n_iterations=int(sys.argv[1]), filename=sys.argv[2], inputs=inputs)
|
| 98 |
+
|
| 99 |
+
except Exception as e:
|
| 100 |
+
raise Exception(f"An error occurred while training the crew: {e}")
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def replay():
|
| 104 |
+
"""
|
| 105 |
+
Replay the crew execution from a specific task.
|
| 106 |
+
"""
|
| 107 |
+
try:
|
| 108 |
+
KoreanCpcAgents().crew().replay(task_id=sys.argv[1])
|
| 109 |
+
|
| 110 |
+
except Exception as e:
|
| 111 |
+
raise Exception(f"An error occurred while replaying the crew: {e}")
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def test():
|
| 115 |
+
"""
|
| 116 |
+
Test the crew execution and returns the results.
|
| 117 |
+
"""
|
| 118 |
+
inputs = {
|
| 119 |
+
"query": "Explain Korean honorifics to me",
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
try:
|
| 123 |
+
KoreanCpcAgents().crew().test(n_iterations=int(sys.argv[1]), eval_llm=sys.argv[2], inputs=inputs)
|
| 124 |
+
|
| 125 |
+
except Exception as e:
|
| 126 |
+
raise Exception(f"An error occurred while testing the crew: {e}")
|
src/korean_cpc_agents/mud_game.py
ADDED
|
@@ -0,0 +1,1088 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Korean Learning MUD Game - Individual Agent System
|
| 3 |
+
Direct NPC interaction using single-agent crews for each Korean family member.
|
| 4 |
+
Enhanced with beautiful CLI from Ollama version.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import time
|
| 9 |
+
import asyncio
|
| 10 |
+
from typing import Dict, Optional
|
| 11 |
+
from crewai import Agent, Crew, Task
|
| 12 |
+
from korean_cpc_agents.crew import KoreanCpcAgents
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class Colors:
|
| 16 |
+
"""ANSI color codes for terminal formatting."""
|
| 17 |
+
|
| 18 |
+
RESET = "\033[0m"
|
| 19 |
+
BOLD = "\033[1m"
|
| 20 |
+
|
| 21 |
+
# Regular colors
|
| 22 |
+
BLACK = "\033[30m"
|
| 23 |
+
RED = "\033[31m"
|
| 24 |
+
GREEN = "\033[32m"
|
| 25 |
+
YELLOW = "\033[33m"
|
| 26 |
+
BLUE = "\033[34m"
|
| 27 |
+
MAGENTA = "\033[35m"
|
| 28 |
+
CYAN = "\033[36m"
|
| 29 |
+
WHITE = "\033[37m"
|
| 30 |
+
|
| 31 |
+
# Bright colors
|
| 32 |
+
BRIGHT_BLACK = "\033[90m"
|
| 33 |
+
BRIGHT_RED = "\033[91m"
|
| 34 |
+
BRIGHT_GREEN = "\033[92m"
|
| 35 |
+
BRIGHT_YELLOW = "\033[93m"
|
| 36 |
+
BRIGHT_BLUE = "\033[94m"
|
| 37 |
+
BRIGHT_MAGENTA = "\033[95m"
|
| 38 |
+
BRIGHT_CYAN = "\033[96m"
|
| 39 |
+
BRIGHT_WHITE = "\033[97m"
|
| 40 |
+
|
| 41 |
+
# Background colors
|
| 42 |
+
BG_BLACK = "\033[40m"
|
| 43 |
+
BG_RED = "\033[41m"
|
| 44 |
+
BG_GREEN = "\033[42m"
|
| 45 |
+
BG_YELLOW = "\033[43m"
|
| 46 |
+
BG_BLUE = "\033[44m"
|
| 47 |
+
BG_MAGENTA = "\033[45m"
|
| 48 |
+
BG_CYAN = "\033[46m"
|
| 49 |
+
BG_WHITE = "\033[47m"
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class KoreanLearningMUD:
|
| 53 |
+
"""
|
| 54 |
+
Korean Learning MUD game with direct agent interaction.
|
| 55 |
+
Each Korean family member is wrapped in their own single-agent crew.
|
| 56 |
+
"""
|
| 57 |
+
|
| 58 |
+
def __init__(self, web_mode=False):
|
| 59 |
+
# Web mode bypasses all terminal output to avoid encoding issues
|
| 60 |
+
self.web_mode = web_mode
|
| 61 |
+
|
| 62 |
+
# Initialize the main crew to get configured agents
|
| 63 |
+
main_crew = KoreanCpcAgents()
|
| 64 |
+
|
| 65 |
+
# Create individual crews for each Korean family member
|
| 66 |
+
self.agent_crews = {}
|
| 67 |
+
self.room_mapping = {}
|
| 68 |
+
|
| 69 |
+
# Create single-agent crews for each family member (excluding manager)
|
| 70 |
+
agent_names = ['ahjumma_gpt', 'ahjussi_gpt', 'unni_gpt', 'oppa_gpt', 'seonsaengnim_gpt']
|
| 71 |
+
|
| 72 |
+
for agent_name in agent_names:
|
| 73 |
+
# Get the agent instance using the method
|
| 74 |
+
agent = getattr(main_crew, agent_name)()
|
| 75 |
+
|
| 76 |
+
# Create a simple task for this agent
|
| 77 |
+
task = Task(
|
| 78 |
+
description="Respond to the user query with your personality: {query}",
|
| 79 |
+
expected_output="A response in character matching your personality and role",
|
| 80 |
+
agent=agent
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
# Create single-agent crew
|
| 84 |
+
crew = Crew(
|
| 85 |
+
agents=[agent],
|
| 86 |
+
tasks=[task],
|
| 87 |
+
verbose=False if self.web_mode else True, # Disable verbose in web mode to prevent encoding issues
|
| 88 |
+
memory=True # Re-enable memory for better conversations
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
self.agent_crews[agent_name] = crew
|
| 92 |
+
|
| 93 |
+
# Map agents to their themed rooms - now includes hall as central hub
|
| 94 |
+
self.room_mapping = {
|
| 95 |
+
'hall': None, # Central hub with no NPC
|
| 96 |
+
'kitchen': 'ahjumma_gpt', # Kim Soon-ja - Honorifics lessons
|
| 97 |
+
'garden': 'ahjussi_gpt', # Park Chul-min - Cultural stories
|
| 98 |
+
'bedroom': 'unni_gpt', # Lee Min-ji - K-pop and slang
|
| 99 |
+
'study': 'oppa_gpt', # Jung Jae-hyun - Grammar expertise
|
| 100 |
+
'classroom': 'seonsaengnim_gpt' # Choi Soo-jin - General Korean
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
# Interactive objects in each room
|
| 104 |
+
self.room_objects = {
|
| 105 |
+
'hall': {
|
| 106 |
+
'family_portrait': {'name': 'family portrait', 'description': 'A beautiful family portrait showing three generations together', 'interactable': True},
|
| 107 |
+
'shoes': {'name': 'shoes', 'description': 'Various pairs of shoes neatly arranged by the entrance', 'interactable': True},
|
| 108 |
+
'guest_slippers': {'name': 'guest slippers', 'description': 'Clean guest slippers waiting for visitors', 'interactable': True},
|
| 109 |
+
'welcome_mat': {'name': 'welcome mat', 'description': 'A mat with Korean characters welcoming guests', 'interactable': True}
|
| 110 |
+
},
|
| 111 |
+
'kitchen': {
|
| 112 |
+
'shoes': {'name': 'shoes', 'description': 'Shoes left at the entrance - should they be removed?', 'interactable': True},
|
| 113 |
+
'family_photo': {'name': 'family photo', 'description': 'An old family photo with grandmother and grandfather', 'interactable': True},
|
| 114 |
+
'kimchi_pot': {'name': 'kimchi pot', 'description': 'A traditional ceramic pot for fermenting kimchi', 'interactable': True},
|
| 115 |
+
'apron': {'name': 'apron', 'description': 'A floral apron hanging on a hook', 'interactable': True},
|
| 116 |
+
'rice_cooker': {'name': 'rice cooker', 'description': 'A modern rice cooker with Korean instructions', 'interactable': True}
|
| 117 |
+
},
|
| 118 |
+
'garden': {
|
| 119 |
+
'stone_bench': {'name': 'stone bench', 'description': 'A weathered stone bench perfect for contemplation', 'interactable': True},
|
| 120 |
+
'poetry_scroll': {'name': 'poetry scroll', 'description': 'An ancient scroll with classical Korean poetry', 'interactable': True},
|
| 121 |
+
'makgeolli_bottle': {'name': 'makgeolli bottle', 'description': 'A traditional rice wine bottle', 'interactable': True},
|
| 122 |
+
'bamboo': {'name': 'bamboo', 'description': 'Tall bamboo stalks swaying in the breeze', 'interactable': True},
|
| 123 |
+
'stone_lantern': {'name': 'stone lantern', 'description': 'A traditional Korean stone lantern', 'interactable': True}
|
| 124 |
+
},
|
| 125 |
+
'bedroom': {
|
| 126 |
+
'album_collection': {'name': 'album collection', 'description': 'Colorful K-pop albums stacked high', 'interactable': True},
|
| 127 |
+
'photocards': {'name': 'photocards', 'description': 'Collectible K-pop idol photocards scattered on the desk', 'interactable': True},
|
| 128 |
+
'phone': {'name': 'phone', 'description': 'A smartphone with K-pop wallpaper', 'interactable': True},
|
| 129 |
+
'mirror': {'name': 'mirror', 'description': 'A mirror with idol photos tucked around the edges', 'interactable': True},
|
| 130 |
+
'led_lights': {'name': 'led lights', 'description': 'Colorful LED strip lights creating ambiance', 'interactable': True}
|
| 131 |
+
},
|
| 132 |
+
'study': {
|
| 133 |
+
'grammar_books': {'name': 'grammar books', 'description': 'Thick Korean grammar textbooks with bookmarks', 'interactable': True},
|
| 134 |
+
'thesis_papers': {'name': 'thesis papers', 'description': 'Academic papers about Korean linguistics', 'interactable': True},
|
| 135 |
+
'certificates': {'name': 'certificates', 'description': 'Korean language proficiency certificates on the wall', 'interactable': True},
|
| 136 |
+
'coffee_mug': {'name': 'coffee mug', 'description': 'A mug with Korean university logo', 'interactable': True},
|
| 137 |
+
'laptop': {'name': 'laptop', 'description': 'A laptop with Korean language learning software', 'interactable': True}
|
| 138 |
+
},
|
| 139 |
+
'classroom': {
|
| 140 |
+
'whiteboard': {'name': 'whiteboard', 'description': 'A whiteboard covered in Korean conjugation examples', 'interactable': True},
|
| 141 |
+
'textbooks': {'name': 'textbooks', 'description': 'Korean language textbooks for different levels', 'interactable': True},
|
| 142 |
+
'coffee_thermos': {'name': 'coffee thermos', 'description': 'A large thermos keeping coffee warm for long teaching sessions', 'interactable': True},
|
| 143 |
+
'grade_sheets': {'name': 'grade sheets', 'description': 'Student assessment papers with red ink corrections', 'interactable': True},
|
| 144 |
+
'korean_flag': {'name': 'korean flag', 'description': 'A small Korean flag on the desk', 'interactable': True}
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
# Learning objectives for each room (3 per room)
|
| 149 |
+
self.room_objectives = {
|
| 150 |
+
'kitchen': {
|
| 151 |
+
'entrance_etiquette': {'completed': False, 'description': 'Learn proper entrance etiquette by examining shoes'},
|
| 152 |
+
'family_honorifics': {'completed': False, 'description': 'Master family honorifics through grandmother story'},
|
| 153 |
+
'cooking_language': {'completed': False, 'description': 'Practice polite cooking requests with Ahjumma'}
|
| 154 |
+
},
|
| 155 |
+
'garden': {
|
| 156 |
+
'poetry_appreciation': {'completed': False, 'description': 'Listen to classical Korean poetry reading'},
|
| 157 |
+
'proverb_wisdom': {'completed': False, 'description': 'Understand traditional Korean proverb about bamboo'},
|
| 158 |
+
'cultural_sharing': {'completed': False, 'description': 'Share makgeolli and learn drinking culture'}
|
| 159 |
+
},
|
| 160 |
+
'bedroom': {
|
| 161 |
+
'lyrics_translation': {'completed': False, 'description': 'Decode K-pop lyrics with Unni'},
|
| 162 |
+
'modern_slang': {'completed': False, 'description': 'Learn trendy Korean expressions'},
|
| 163 |
+
'aegyo_practice': {'completed': False, 'description': 'Practice cute Korean expressions (aegyo)'}
|
| 164 |
+
},
|
| 165 |
+
'study': {
|
| 166 |
+
'grammar_mastery': {'completed': False, 'description': 'Survive complex grammar explanations'},
|
| 167 |
+
'academic_korean': {'completed': False, 'description': 'Learn formal academic Korean vocabulary'},
|
| 168 |
+
'conjugation_challenge': {'completed': False, 'description': 'Master verb conjugation patterns'}
|
| 169 |
+
},
|
| 170 |
+
'classroom': {
|
| 171 |
+
'practical_conversation': {'completed': False, 'description': 'Practice everyday Korean conversation'},
|
| 172 |
+
'survival_phrases': {'completed': False, 'description': 'Learn essential survival Korean phrases'},
|
| 173 |
+
'pronunciation_practice': {'completed': False, 'description': 'Master Korean pronunciation fundamentals'}
|
| 174 |
+
}
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
# Player state
|
| 178 |
+
self.current_room = 'hall' # Start in hall (central hub)
|
| 179 |
+
self.discovered_words = set()
|
| 180 |
+
self.game_running = True
|
| 181 |
+
self.game_progress = {} # Track learning objectives and achievements
|
| 182 |
+
self.player_inventory = set() # Track objects player has interacted with
|
| 183 |
+
|
| 184 |
+
# Direction symbols for better UI
|
| 185 |
+
self.direction_symbols = {
|
| 186 |
+
"north": "↑",
|
| 187 |
+
"south": "↓",
|
| 188 |
+
"east": "→",
|
| 189 |
+
"west": "←",
|
| 190 |
+
"up": "⤴",
|
| 191 |
+
"down": "⤵",
|
| 192 |
+
"northeast": "↗",
|
| 193 |
+
"northwest": "↖",
|
| 194 |
+
"southeast": "↘",
|
| 195 |
+
"southwest": "↙",
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
def clear_screen(self):
|
| 199 |
+
"""Clear the terminal screen."""
|
| 200 |
+
os.system("cls" if os.name == "nt" else "clear")
|
| 201 |
+
|
| 202 |
+
def display_korean_word(self, korean_word, include_practice=False):
|
| 203 |
+
"""Format and display a Korean vocabulary word with additional information."""
|
| 204 |
+
print("\n+----------- New Korean Word -----------+")
|
| 205 |
+
print(f"| {Colors.BOLD}{korean_word.get('hangul', korean_word.get('korean', 'Unknown'))}{Colors.RESET}")
|
| 206 |
+
|
| 207 |
+
if "romanization" in korean_word:
|
| 208 |
+
print(f"| Pronunciation: {korean_word['romanization']}")
|
| 209 |
+
|
| 210 |
+
print(f"| Meaning: {korean_word.get('meaning', 'Unknown meaning')}")
|
| 211 |
+
|
| 212 |
+
if "usage_example" in korean_word:
|
| 213 |
+
print(f"| Example: {korean_word['usage_example']}")
|
| 214 |
+
|
| 215 |
+
print("+---------------------------------------+")
|
| 216 |
+
hangul = korean_word.get('hangul', korean_word.get('korean', ''))
|
| 217 |
+
if hangul:
|
| 218 |
+
self.discovered_words.add(hangul)
|
| 219 |
+
|
| 220 |
+
if include_practice:
|
| 221 |
+
print(
|
| 222 |
+
f"\n{Colors.YELLOW}Practice saying: {Colors.BOLD}{hangul}{Colors.RESET} ({korean_word.get('meaning', '')}){Colors.RESET}"
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
def display_room(self):
|
| 226 |
+
"""Display the current room information with improved visual formatting."""
|
| 227 |
+
if self.current_room not in self.room_mapping:
|
| 228 |
+
print(f"Error: Room '{self.current_room}' not found!")
|
| 229 |
+
return
|
| 230 |
+
|
| 231 |
+
self.clear_screen()
|
| 232 |
+
|
| 233 |
+
# Title banner with room name
|
| 234 |
+
room_info = self._get_room_info()
|
| 235 |
+
title = f" {room_info['display_name']} "
|
| 236 |
+
padding = "═" * ((60 - len(title)) // 2)
|
| 237 |
+
print(f"\n{Colors.BRIGHT_CYAN}{padding}{title}{padding}{Colors.RESET}")
|
| 238 |
+
|
| 239 |
+
# Room description
|
| 240 |
+
print(f"\n{room_info['description']}\n")
|
| 241 |
+
|
| 242 |
+
# Create a visually distinct section for navigation
|
| 243 |
+
print(
|
| 244 |
+
f"\n{Colors.BRIGHT_BLUE}╔════════ NAVIGATION ════════╗{Colors.RESET}"
|
| 245 |
+
)
|
| 246 |
+
|
| 247 |
+
# Format exits with direction symbols and colors
|
| 248 |
+
exits = self._get_room_exits()
|
| 249 |
+
for direction, destination in exits.items():
|
| 250 |
+
symbol = self.direction_symbols.get(direction, "*")
|
| 251 |
+
dest_info = self._get_room_info(destination)
|
| 252 |
+
print(
|
| 253 |
+
f"{Colors.BRIGHT_BLUE}║ {Colors.RESET}{Colors.GREEN}{symbol} {direction}{Colors.RESET} -> {dest_info['display_name']}"
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
print(
|
| 257 |
+
f"{Colors.BRIGHT_BLUE}╚═══════════════════════════╝{Colors.RESET}"
|
| 258 |
+
)
|
| 259 |
+
|
| 260 |
+
# Show available actions in this room for clearer gameplay
|
| 261 |
+
print(
|
| 262 |
+
f"\n{Colors.BRIGHT_GREEN}╔════════ ACTIONS ═══════════╗{Colors.RESET}"
|
| 263 |
+
)
|
| 264 |
+
|
| 265 |
+
# Show NPC in the room
|
| 266 |
+
agent_name = self.room_mapping[self.current_room]
|
| 267 |
+
npc_info = self._get_npc_info(agent_name)
|
| 268 |
+
print(
|
| 269 |
+
f"{Colors.BRIGHT_GREEN}║{Colors.RESET} {Colors.BRIGHT_MAGENTA}Character you can talk to:{Colors.RESET}"
|
| 270 |
+
)
|
| 271 |
+
print(
|
| 272 |
+
f"{Colors.BRIGHT_GREEN}║{Colors.RESET} {Colors.MAGENTA}• {npc_info['name']}{Colors.RESET}"
|
| 273 |
+
)
|
| 274 |
+
|
| 275 |
+
print(
|
| 276 |
+
f"{Colors.BRIGHT_GREEN}╚═══════════════════════════╝{Colors.RESET}"
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
# Show vocabulary learning prompt
|
| 280 |
+
vocab_count = len(self.discovered_words)
|
| 281 |
+
print(
|
| 282 |
+
f"\n{Colors.YELLOW}You've learned {vocab_count} Korean words so far.{Colors.RESET}"
|
| 283 |
+
)
|
| 284 |
+
print(
|
| 285 |
+
f"{Colors.YELLOW}Talk to family members to learn more Korean!{Colors.RESET}"
|
| 286 |
+
)
|
| 287 |
+
|
| 288 |
+
# Show command prompt with styling
|
| 289 |
+
print(
|
| 290 |
+
f"\n{Colors.BRIGHT_GREEN}┌─ What would you like to do? (type 'help' for commands){Colors.RESET}"
|
| 291 |
+
)
|
| 292 |
+
print(f"{Colors.BRIGHT_GREEN}└─╼{Colors.RESET} ", end="")
|
| 293 |
+
|
| 294 |
+
def talk_to_npc(self, user_message: str) -> str:
|
| 295 |
+
"""
|
| 296 |
+
Talk to the NPC in the current room using their single-agent crew.
|
| 297 |
+
"""
|
| 298 |
+
if self.current_room not in self.room_mapping:
|
| 299 |
+
return f"No one is available in the {self.current_room}."
|
| 300 |
+
|
| 301 |
+
agent_name = self.room_mapping[self.current_room]
|
| 302 |
+
|
| 303 |
+
if agent_name not in self.agent_crews:
|
| 304 |
+
return f"{agent_name} is not available right now."
|
| 305 |
+
|
| 306 |
+
# Get NPC info for display
|
| 307 |
+
npc_info = self._get_npc_info(agent_name)
|
| 308 |
+
|
| 309 |
+
# Only print in CLI mode, skip in web mode to avoid encoding issues
|
| 310 |
+
if not self.web_mode:
|
| 311 |
+
print(f"\n{Colors.BRIGHT_CYAN}Talking to {npc_info['name']}...{Colors.RESET}")
|
| 312 |
+
|
| 313 |
+
try:
|
| 314 |
+
# Use the single-agent crew to get response
|
| 315 |
+
crew = self.agent_crews[agent_name]
|
| 316 |
+
result = crew.kickoff(inputs={'query': user_message})
|
| 317 |
+
|
| 318 |
+
response = result.raw if hasattr(result, 'raw') else str(result)
|
| 319 |
+
|
| 320 |
+
# Handle encoding issues more gracefully
|
| 321 |
+
try:
|
| 322 |
+
# Try to ensure proper UTF-8 encoding without terminal output
|
| 323 |
+
if isinstance(response, bytes):
|
| 324 |
+
clean_response = response.decode('utf-8', errors='replace')
|
| 325 |
+
elif isinstance(response, str):
|
| 326 |
+
# Re-encode to handle any problematic characters
|
| 327 |
+
clean_response = response.encode('utf-8', errors='replace').decode('utf-8', errors='replace')
|
| 328 |
+
else:
|
| 329 |
+
clean_response = str(response)
|
| 330 |
+
|
| 331 |
+
# Clean up any null bytes or problematic characters
|
| 332 |
+
clean_response = clean_response.replace('\x00', '').strip()
|
| 333 |
+
|
| 334 |
+
# If response is empty or problematic, provide fallback
|
| 335 |
+
if not clean_response or len(clean_response.strip()) == 0:
|
| 336 |
+
clean_response = f"Hello! I'm {npc_info['name']}. How can I help you learn Korean today?"
|
| 337 |
+
|
| 338 |
+
except Exception as encoding_error:
|
| 339 |
+
# Fallback for encoding issues
|
| 340 |
+
clean_response = f"Hello! I'm {npc_info['name']}. I'm here to help you learn Korean! (Having some encoding issues, but I can still chat!)"
|
| 341 |
+
|
| 342 |
+
# For web interface, just return the response without terminal formatting
|
| 343 |
+
# Note: Vocabulary teaching is now handled only through object examination
|
| 344 |
+
# Chat responses focus on natural conversation with inline Korean teaching
|
| 345 |
+
|
| 346 |
+
return clean_response
|
| 347 |
+
|
| 348 |
+
except Exception as e:
|
| 349 |
+
error_msg = str(e)
|
| 350 |
+
|
| 351 |
+
# Handle specific encoding errors gracefully
|
| 352 |
+
if any(term in error_msg for term in ['charmap', 'codec', 'encode', 'decode']):
|
| 353 |
+
fallback_response = f"Hello! I'm {npc_info['name']} and I'm here to help you learn Korean! I'm having some technical difficulties with Korean character display, but I can still teach you Korean language and culture in English."
|
| 354 |
+
return fallback_response
|
| 355 |
+
else:
|
| 356 |
+
return f"Sorry, I'm having technical difficulties right now. Please try again! ({error_msg})"
|
| 357 |
+
|
| 358 |
+
async def talk_to_npc_async(self, user_message: str) -> str:
|
| 359 |
+
"""
|
| 360 |
+
Async version of talk_to_npc for better performance.
|
| 361 |
+
"""
|
| 362 |
+
if self.current_room not in self.room_mapping:
|
| 363 |
+
return f"No one is available in the {self.current_room}."
|
| 364 |
+
|
| 365 |
+
agent_name = self.room_mapping[self.current_room]
|
| 366 |
+
|
| 367 |
+
if agent_name not in self.agent_crews:
|
| 368 |
+
return f"{agent_name} is not available right now."
|
| 369 |
+
|
| 370 |
+
try:
|
| 371 |
+
# Use async kickoff for better performance
|
| 372 |
+
crew = self.agent_crews[agent_name]
|
| 373 |
+
result = await crew.kickoff_async(inputs={'query': user_message})
|
| 374 |
+
|
| 375 |
+
return result.raw if hasattr(result, 'raw') else str(result)
|
| 376 |
+
|
| 377 |
+
except Exception as e:
|
| 378 |
+
return f"{agent_name} couldn't respond: {str(e)}"
|
| 379 |
+
|
| 380 |
+
def go_to_room(self, room_name: str) -> str:
|
| 381 |
+
"""
|
| 382 |
+
Move to a different room in the Korean house.
|
| 383 |
+
"""
|
| 384 |
+
room_name = room_name.lower().strip()
|
| 385 |
+
|
| 386 |
+
if room_name in self.room_mapping:
|
| 387 |
+
old_room = self.current_room
|
| 388 |
+
self.current_room = room_name
|
| 389 |
+
|
| 390 |
+
# Display will refresh automatically in main loop
|
| 391 |
+
return f"{Colors.GREEN}You moved from {old_room} to {room_name}.{Colors.RESET}"
|
| 392 |
+
else:
|
| 393 |
+
available_rooms = ", ".join(self.room_mapping.keys())
|
| 394 |
+
return f"Unknown room '{room_name}'. Available rooms: {available_rooms}"
|
| 395 |
+
|
| 396 |
+
def look_around(self) -> str:
|
| 397 |
+
"""
|
| 398 |
+
Look around the current room and get description with objects.
|
| 399 |
+
"""
|
| 400 |
+
room_info = self._get_room_info()
|
| 401 |
+
description = room_info['description']
|
| 402 |
+
|
| 403 |
+
# Add objects in room
|
| 404 |
+
objects = self.room_objects.get(self.current_room, {})
|
| 405 |
+
if objects:
|
| 406 |
+
object_list = [obj['name'] for obj in objects.values()]
|
| 407 |
+
object_str = ", ".join(object_list)
|
| 408 |
+
description += f"\n\nYou can see: {object_str}\nTry 'examine <object>' to interact with them."
|
| 409 |
+
|
| 410 |
+
# Add NPC information if present
|
| 411 |
+
agent_name = self.room_mapping.get(self.current_room)
|
| 412 |
+
if agent_name:
|
| 413 |
+
npc_info = self._get_npc_info(agent_name)
|
| 414 |
+
description += f"\n\n{npc_info['name']} is here. {npc_info['description']}"
|
| 415 |
+
|
| 416 |
+
return description
|
| 417 |
+
|
| 418 |
+
def _get_room_info(self, room_name=None):
|
| 419 |
+
"""Get room information including display name and description."""
|
| 420 |
+
if room_name is None:
|
| 421 |
+
room_name = self.current_room
|
| 422 |
+
|
| 423 |
+
room_data = {
|
| 424 |
+
'hall': {
|
| 425 |
+
'display_name': 'Family Hall (daecheong)',
|
| 426 |
+
'description': "The central gathering space of the Korean house. A large family portrait dominates one wall, and shoes are neatly arranged by the entrance. Traditional furniture and modern touches blend harmoniously."
|
| 427 |
+
},
|
| 428 |
+
'kitchen': {
|
| 429 |
+
'display_name': 'Korean Kitchen (bueok)',
|
| 430 |
+
'description': "Warm and bustling with the smell of kimchi-jjigae. Traditional ceramic bowls and chopsticks are neatly arranged. Steam rises from bubbling pots on the stove."
|
| 431 |
+
},
|
| 432 |
+
'garden': {
|
| 433 |
+
'display_name': 'Traditional Garden (jeongwon)',
|
| 434 |
+
'description': "A peaceful space with stone paths, bamboo, and a small pavilion for contemplation. Cherry blossoms flutter in the gentle breeze."
|
| 435 |
+
},
|
| 436 |
+
'bedroom': {
|
| 437 |
+
'display_name': 'Modern Bedroom (chimsil)',
|
| 438 |
+
'description': "Colorful K-pop posters cover the walls, with albums and photocards scattered on the desk. LED lights create a vibrant atmosphere."
|
| 439 |
+
},
|
| 440 |
+
'study': {
|
| 441 |
+
'display_name': 'Academic Study (seojae)',
|
| 442 |
+
'description': "Shelves lined with Korean grammar books, linguistic journals, and classical literature reach toward the ceiling. A desk lamp illuminates open manuscripts."
|
| 443 |
+
},
|
| 444 |
+
'classroom': {
|
| 445 |
+
'display_name': 'Hagwon Classroom (gyosil)',
|
| 446 |
+
'description': "Whiteboards covered in Korean characters line the walls. Desks are arranged in rows, and coffee cups are scattered everywhere."
|
| 447 |
+
}
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
return room_data.get(room_name, {
|
| 451 |
+
'display_name': room_name.title(),
|
| 452 |
+
'description': f"You are in the {room_name}."
|
| 453 |
+
})
|
| 454 |
+
|
| 455 |
+
def examine_object(self, object_name: str) -> str:
|
| 456 |
+
"""
|
| 457 |
+
Examine an object in the current room and trigger agent interaction if applicable.
|
| 458 |
+
"""
|
| 459 |
+
objects = self.room_objects.get(self.current_room, {})
|
| 460 |
+
|
| 461 |
+
# Find object by name (case insensitive, exact match for key or partial match for name)
|
| 462 |
+
target_object = None
|
| 463 |
+
object_key = None
|
| 464 |
+
|
| 465 |
+
# First try exact key match
|
| 466 |
+
for obj_key, obj_data in objects.items():
|
| 467 |
+
if object_name.lower().replace(' ', '_') == obj_key or object_name.lower() == obj_key:
|
| 468 |
+
target_object = obj_data
|
| 469 |
+
object_key = obj_key
|
| 470 |
+
break
|
| 471 |
+
|
| 472 |
+
# If no exact match, try partial match on display name
|
| 473 |
+
if not target_object:
|
| 474 |
+
for obj_key, obj_data in objects.items():
|
| 475 |
+
if object_name.lower() in obj_data['name'].lower():
|
| 476 |
+
target_object = obj_data
|
| 477 |
+
object_key = obj_key
|
| 478 |
+
break
|
| 479 |
+
|
| 480 |
+
if not target_object:
|
| 481 |
+
available_objects = [obj['name'] for obj in objects.values()]
|
| 482 |
+
if available_objects:
|
| 483 |
+
return f"I don't see '{object_name}' here. Available objects: {', '.join(available_objects)}"
|
| 484 |
+
else:
|
| 485 |
+
return "There's nothing to examine in this room."
|
| 486 |
+
|
| 487 |
+
# Mark object as examined
|
| 488 |
+
self.player_inventory.add(target_object['name'])
|
| 489 |
+
|
| 490 |
+
# Trigger special interactions based on object and room
|
| 491 |
+
agent_name = self.room_mapping.get(self.current_room)
|
| 492 |
+
if target_object.get('interactable', False):
|
| 493 |
+
description = self._trigger_object_interaction(target_object['name'], agent_name)
|
| 494 |
+
else:
|
| 495 |
+
description = target_object['description']
|
| 496 |
+
|
| 497 |
+
return description
|
| 498 |
+
|
| 499 |
+
def _trigger_object_interaction(self, object_name: str, agent_name: str) -> str:
|
| 500 |
+
"""
|
| 501 |
+
Trigger special agent responses for object interactions.
|
| 502 |
+
"""
|
| 503 |
+
# Define object interaction triggers
|
| 504 |
+
object_triggers = {
|
| 505 |
+
'kitchen': {
|
| 506 |
+
'shoes': "You examine the shoes at the entrance. Should they be removed before entering?",
|
| 507 |
+
'family_photo': "An old family photo showing grandmother and grandfather in traditional hanbok.",
|
| 508 |
+
'kimchi_pot': "A traditional onggi pot used for fermenting kimchi. It smells deliciously sour!",
|
| 509 |
+
'apron': "Grandmother's floral apron, worn from years of cooking for the family."
|
| 510 |
+
},
|
| 511 |
+
'garden': {
|
| 512 |
+
'stone_bench': "A weathered stone bench where grandfather sits to read poetry.",
|
| 513 |
+
'poetry_scroll': "An ancient scroll with beautiful calligraphy - classical Korean poetry.",
|
| 514 |
+
'makgeolli_bottle': "Traditional rice wine that grandfather enjoys while sharing stories.",
|
| 515 |
+
'bamboo': "Tall bamboo stalks that bend but don't break - a symbol of resilience."
|
| 516 |
+
},
|
| 517 |
+
'bedroom': {
|
| 518 |
+
'album_collection': "Stacks of colorful K-pop albums from various idol groups.",
|
| 519 |
+
'photocards': "Collectible photocards of K-pop idols, carefully organized.",
|
| 520 |
+
'phone': "A smartphone with the latest K-pop music and social media apps.",
|
| 521 |
+
'mirror': "A mirror surrounded by photos of favorite K-pop idols."
|
| 522 |
+
},
|
| 523 |
+
'study': {
|
| 524 |
+
'grammar_books': "Thick Korean grammar textbooks with detailed explanations and examples.",
|
| 525 |
+
'thesis_papers': "Academic papers about Korean linguistics and language acquisition.",
|
| 526 |
+
'certificates': "Korean language proficiency certificates proudly displayed.",
|
| 527 |
+
'coffee_mug': "A coffee mug that's seen many late-night study sessions."
|
| 528 |
+
},
|
| 529 |
+
'classroom': {
|
| 530 |
+
'whiteboard': "A whiteboard covered with Korean verb conjugations and grammar rules.",
|
| 531 |
+
'textbooks': "Korean language textbooks for different proficiency levels.",
|
| 532 |
+
'coffee_thermos': "A large thermos keeping coffee warm during long teaching sessions.",
|
| 533 |
+
'grade_sheets': "Student papers with red corrections and encouraging notes."
|
| 534 |
+
},
|
| 535 |
+
'hall': {
|
| 536 |
+
'family_portrait': "A beautiful family portrait showing three generations together in harmony.",
|
| 537 |
+
'shoes': "Various shoes neatly arranged - a sign of Korean entrance etiquette.",
|
| 538 |
+
'guest_slippers': "Clean guest slippers waiting for visitors to feel at home.",
|
| 539 |
+
'welcome_mat': "A mat with Korean characters: 환영합니다 (Welcome)."
|
| 540 |
+
}
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
room_triggers = object_triggers.get(self.current_room, {})
|
| 544 |
+
# Try both space and underscore versions of object name
|
| 545 |
+
trigger_key = object_name.replace(' ', '_')
|
| 546 |
+
base_description = room_triggers.get(trigger_key, room_triggers.get(object_name, ""))
|
| 547 |
+
|
| 548 |
+
# Add enhanced learning context and vocabulary
|
| 549 |
+
enhanced_description = base_description
|
| 550 |
+
if agent_name:
|
| 551 |
+
npc_info = self._get_npc_info(agent_name)
|
| 552 |
+
agent_response = self._get_agent_response_for_object(object_name, agent_name)
|
| 553 |
+
if agent_response:
|
| 554 |
+
enhanced_description += f"\n\n💬 {npc_info['name']}: {agent_response}"
|
| 555 |
+
|
| 556 |
+
# Add vocabulary based on object
|
| 557 |
+
vocab_word = self._get_object_vocabulary(object_name, self.current_room)
|
| 558 |
+
if vocab_word:
|
| 559 |
+
enhanced_description += f"\n\n📚 New Korean Word: {vocab_word['korean']} ({vocab_word['meaning']})"
|
| 560 |
+
self.discovered_words.add(vocab_word['korean'])
|
| 561 |
+
|
| 562 |
+
return enhanced_description
|
| 563 |
+
|
| 564 |
+
def _get_agent_response_for_object(self, object_name: str, agent_name: str) -> str:
|
| 565 |
+
"""Get culturally appropriate agent responses for object interactions."""
|
| 566 |
+
agent_responses = {
|
| 567 |
+
'ahjumma_gpt': {
|
| 568 |
+
'shoes': "아이고! You should remove your shoes before entering. This is basic Korean manner. 신발을 벗으세요!",
|
| 569 |
+
'family_photo': "This is my dear mother-in-law, 할머니. She taught me everything about proper Korean cooking and respect.",
|
| 570 |
+
'kimchi_pot': "This onggi has been in our family for generations. Real kimchi needs proper fermentation - it's our Korean soul food!",
|
| 571 |
+
'apron': "This old apron has served me well. A good Korean woman always keeps her family well-fed."
|
| 572 |
+
},
|
| 573 |
+
'ahjussi_gpt': {
|
| 574 |
+
'stone_bench': "I carved this bench myself 30 years ago. It's where I contemplate life and read classical poetry.",
|
| 575 |
+
'poetry_scroll': "Ah, this is Yun Dong-ju's poetry. He captured the Korean spirit during dark times. Beautiful, isn't it?",
|
| 576 |
+
'makgeolli_bottle': "Traditional rice wine connects us to our ancestors. Let me share a 속담: '술은 인생의 벗' - alcohol is life's companion.",
|
| 577 |
+
'bamboo': "Bamboo bends but never breaks. This teaches us Korean resilience - we survive all hardships."
|
| 578 |
+
},
|
| 579 |
+
'unni_gpt': {
|
| 580 |
+
'album_collection': "OMG these are limited editions! BTS, BLACKPINK, aespa - I have them all! Want me to teach you the lyrics?",
|
| 581 |
+
'photocards': "These photocards are so rare! Trading them is like a whole culture. It's how we show our 덕질 dedication!",
|
| 582 |
+
'phone': "I'm always watching music shows and learning new choreography. Want to try some aegyo? 사랑해요~",
|
| 583 |
+
'mirror': "This is where I practice my idol poses! Korean beauty standards are so important in K-pop culture."
|
| 584 |
+
},
|
| 585 |
+
'oppa_gpt': {
|
| 586 |
+
'grammar_books': "Korean grammar is fascinating - the honorific system reflects our entire social structure. Let me explain 존댓말.",
|
| 587 |
+
'thesis_papers': "I'm researching Korean language evolution. Did you know our writing system 한글 is scientifically perfect?",
|
| 588 |
+
'certificates': "These represent years of Korean study. Language is the key to understanding any culture deeply.",
|
| 589 |
+
'coffee_mug': "Coffee fuels my academic work. In Korea, we say 화이팅! for encouragement during tough times."
|
| 590 |
+
},
|
| 591 |
+
'seonsaengnim_gpt': {
|
| 592 |
+
'whiteboard': "This board has taught thousands of students Korean. Grammar is important, but communication is the goal!",
|
| 593 |
+
'textbooks': "I've written some of these books myself. Learning Korean opens doors to understanding our beautiful culture.",
|
| 594 |
+
'coffee_thermos': "Teaching Korean all day requires lots of caffeine! Good teachers need energy to help students succeed.",
|
| 595 |
+
'grade_sheets': "Every red mark here is meant to help students improve. Mistakes are part of learning Korean!"
|
| 596 |
+
}
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
# Try both space and underscore versions
|
| 600 |
+
trigger_key = object_name.replace(' ', '_')
|
| 601 |
+
agent_dict = agent_responses.get(agent_name, {})
|
| 602 |
+
return agent_dict.get(trigger_key, agent_dict.get(object_name, ""))
|
| 603 |
+
|
| 604 |
+
def _get_object_vocabulary(self, object_name: str, room: str) -> dict:
|
| 605 |
+
"""Get Korean vocabulary associated with objects."""
|
| 606 |
+
vocabulary_map = {
|
| 607 |
+
'shoes': {'korean': '신발', 'meaning': 'shoes'},
|
| 608 |
+
'family_photo': {'korean': '가족사진', 'meaning': 'family photo'},
|
| 609 |
+
'kimchi_pot': {'korean': '김치', 'meaning': 'kimchi'},
|
| 610 |
+
'apron': {'korean': '앞치마', 'meaning': 'apron'},
|
| 611 |
+
'stone_bench': {'korean': '돌벤치', 'meaning': 'stone bench'},
|
| 612 |
+
'poetry_scroll': {'korean': '시', 'meaning': 'poetry'},
|
| 613 |
+
'makgeolli_bottle': {'korean': '막걸리', 'meaning': 'traditional rice wine'},
|
| 614 |
+
'bamboo': {'korean': '대나무', 'meaning': 'bamboo'},
|
| 615 |
+
'album_collection': {'korean': '앨범', 'meaning': 'album'},
|
| 616 |
+
'photocards': {'korean': '포토카드', 'meaning': 'photocard'},
|
| 617 |
+
'phone': {'korean': '휴대폰', 'meaning': 'mobile phone'},
|
| 618 |
+
'mirror': {'korean': '거울', 'meaning': 'mirror'},
|
| 619 |
+
'grammar_books': {'korean': '문법책', 'meaning': 'grammar book'},
|
| 620 |
+
'thesis_papers': {'korean': '논문', 'meaning': 'thesis/paper'},
|
| 621 |
+
'certificates': {'korean': '자격증', 'meaning': 'certificate'},
|
| 622 |
+
'coffee_mug': {'korean': '커피잔', 'meaning': 'coffee cup'},
|
| 623 |
+
'whiteboard': {'korean': '화이트보드', 'meaning': 'whiteboard'},
|
| 624 |
+
'textbooks': {'korean': '교과서', 'meaning': 'textbook'},
|
| 625 |
+
'coffee_thermos': {'korean': '보온병', 'meaning': 'thermos'},
|
| 626 |
+
'grade_sheets': {'korean': '성적표', 'meaning': 'grade sheet'}
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
# Try both space and underscore versions
|
| 630 |
+
trigger_key = object_name.replace(' ', '_')
|
| 631 |
+
return vocabulary_map.get(trigger_key, vocabulary_map.get(object_name, None))
|
| 632 |
+
|
| 633 |
+
def take_object(self, object_name: str) -> str:
|
| 634 |
+
"""
|
| 635 |
+
Attempt to take an object (most objects can't be taken, but gives cultural context).
|
| 636 |
+
"""
|
| 637 |
+
objects = self.room_objects.get(self.current_room, {})
|
| 638 |
+
|
| 639 |
+
# Find object
|
| 640 |
+
target_object = None
|
| 641 |
+
for obj_key, obj_data in objects.items():
|
| 642 |
+
if object_name.lower() in obj_data['name'].lower():
|
| 643 |
+
target_object = obj_data
|
| 644 |
+
break
|
| 645 |
+
|
| 646 |
+
if not target_object:
|
| 647 |
+
return f"I don't see '{object_name}' here."
|
| 648 |
+
|
| 649 |
+
# Most objects can't be taken, but provide cultural context
|
| 650 |
+
cultural_responses = {
|
| 651 |
+
'shoes': "In Korean culture, shoes should be left at the entrance. You can't take them to other rooms!",
|
| 652 |
+
'family_photo': "Family photos are treasured keepsakes that stay in their place of honor.",
|
| 653 |
+
'kimchi_pot': "The kimchi pot is too precious and heavy to move - it's part of the kitchen!",
|
| 654 |
+
'poetry_scroll': "Ancient scrolls are too valuable and fragile to carry around.",
|
| 655 |
+
'albums': "These K-pop albums are carefully organized - better not disturb the collection!",
|
| 656 |
+
'textbooks': "These study materials belong here for everyone to use.",
|
| 657 |
+
'coffee_mug': "That's someone else's personal mug - better leave it where it is!"
|
| 658 |
+
}
|
| 659 |
+
|
| 660 |
+
# Check for partial matches
|
| 661 |
+
for item, response in cultural_responses.items():
|
| 662 |
+
if item in object_name.lower():
|
| 663 |
+
return response
|
| 664 |
+
|
| 665 |
+
return f"You can't take the {object_name}. It belongs here as part of the room."
|
| 666 |
+
|
| 667 |
+
def _get_room_exits(self):
|
| 668 |
+
"""Get available exits from current room with hall as central hub."""
|
| 669 |
+
# Define the house layout with hall as central hub
|
| 670 |
+
exits_map = {
|
| 671 |
+
'hall': {'north': 'garden', 'south': 'classroom', 'east': 'study', 'west': 'kitchen'},
|
| 672 |
+
'kitchen': {'east': 'hall', 'north': 'bedroom'},
|
| 673 |
+
'garden': {'south': 'hall', 'east': 'bedroom'},
|
| 674 |
+
'bedroom': {'south': 'study', 'west': 'garden', 'southwest': 'kitchen'},
|
| 675 |
+
'study': {'west': 'hall', 'north': 'bedroom'},
|
| 676 |
+
'classroom': {'north': 'hall'}
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
return exits_map.get(self.current_room, {})
|
| 680 |
+
|
| 681 |
+
def _get_npc_info(self, agent_name: str) -> dict:
|
| 682 |
+
"""
|
| 683 |
+
Get information about the NPC in the room.
|
| 684 |
+
"""
|
| 685 |
+
npc_data = {
|
| 686 |
+
'ahjumma_gpt': {
|
| 687 |
+
'name': 'Kim Soon-ja (Ahjumma)',
|
| 688 |
+
'description': "She sits here wearing her floral apron and hair rollers, ready to teach you proper Korean honorifics - whether you want to learn or not!"
|
| 689 |
+
},
|
| 690 |
+
'ahjussi_gpt': {
|
| 691 |
+
'name': 'Park Chul-min (Ahjussi)',
|
| 692 |
+
'description': "He relaxes here with his reading glasses and classical poetry book, ready to share ancient Korean wisdom and cultural stories."
|
| 693 |
+
},
|
| 694 |
+
'unni_gpt': {
|
| 695 |
+
'name': 'Lee Min-ji (Unni)',
|
| 696 |
+
'description': "She's here surrounded by K-pop albums and merchandise, excited to teach you Korean through the latest idol songs and trends!"
|
| 697 |
+
},
|
| 698 |
+
'oppa_gpt': {
|
| 699 |
+
'name': 'Jung Jae-hyun (Oppa)',
|
| 700 |
+
'description': "He studies here with thick grammar books and linguistic papers, eager to show off his superior knowledge of Korean grammar."
|
| 701 |
+
},
|
| 702 |
+
'seonsaengnim_gpt': {
|
| 703 |
+
'name': 'Choi Soo-jin (Seonsaengnim)',
|
| 704 |
+
'description': "She sits here with coffee and red pens, looking exhausted but ready to provide practical Korean lessons despite her complaints."
|
| 705 |
+
}
|
| 706 |
+
}
|
| 707 |
+
|
| 708 |
+
return npc_data.get(agent_name, {
|
| 709 |
+
'name': 'Someone',
|
| 710 |
+
'description': 'Someone is here to help you learn Korean.'
|
| 711 |
+
})
|
| 712 |
+
|
| 713 |
+
def _extract_korean_word_from_response(self, response: str, agent_name: str) -> dict:
|
| 714 |
+
"""Extract or generate a Korean word from the agent's response."""
|
| 715 |
+
import re
|
| 716 |
+
import random
|
| 717 |
+
|
| 718 |
+
# First try to extract actual Korean words from the response
|
| 719 |
+
korean_pattern = r'📚 New Korean Word: ([가-힣]+) \(([^)]+)\) = ([^"\n]+)'
|
| 720 |
+
match = re.search(korean_pattern, response)
|
| 721 |
+
|
| 722 |
+
if match:
|
| 723 |
+
hangul, romanization, meaning = match.groups()
|
| 724 |
+
word_dict = {
|
| 725 |
+
'hangul': hangul.strip(),
|
| 726 |
+
'romanization': romanization.strip(),
|
| 727 |
+
'meaning': meaning.strip()
|
| 728 |
+
}
|
| 729 |
+
self.discovered_words.add(word_dict['hangul'])
|
| 730 |
+
return word_dict
|
| 731 |
+
|
| 732 |
+
# Fallback to curated word pools (but avoid repeats)
|
| 733 |
+
sample_words = {
|
| 734 |
+
'ahjumma_gpt': [
|
| 735 |
+
{'hangul': '존댓말', 'meaning': 'honorific speech', 'romanization': 'jondaetmal'},
|
| 736 |
+
{'hangul': '할머니', 'meaning': 'grandmother', 'romanization': 'halmeoni'},
|
| 737 |
+
{'hangul': '부엌', 'meaning': 'kitchen', 'romanization': 'bueok'},
|
| 738 |
+
{'hangul': '정중하게', 'meaning': 'politely', 'romanization': 'jeongjunghage'},
|
| 739 |
+
{'hangul': '예의', 'meaning': 'manners/etiquette', 'romanization': 'yeui'}
|
| 740 |
+
],
|
| 741 |
+
'ahjussi_gpt': [
|
| 742 |
+
{'hangul': '속담', 'meaning': 'proverb', 'romanization': 'sokdam'},
|
| 743 |
+
{'hangul': '지혜', 'meaning': 'wisdom', 'romanization': 'jihye'},
|
| 744 |
+
{'hangul': '전통', 'meaning': 'tradition', 'romanization': 'jeontong'},
|
| 745 |
+
{'hangul': '문화', 'meaning': 'culture', 'romanization': 'munhwa'},
|
| 746 |
+
{'hangul': '역사', 'meaning': 'history', 'romanization': 'yeoksa'}
|
| 747 |
+
],
|
| 748 |
+
'unni_gpt': [
|
| 749 |
+
{'hangul': '대박', 'meaning': 'awesome/amazing', 'romanization': 'daebak'},
|
| 750 |
+
{'hangul': '아이돌', 'meaning': 'idol', 'romanization': 'aidol'},
|
| 751 |
+
{'hangul': '노래', 'meaning': 'song', 'romanization': 'norae'},
|
| 752 |
+
{'hangul': '춤', 'meaning': 'dance', 'romanization': 'chum'},
|
| 753 |
+
{'hangul': '귀여워', 'meaning': 'cute', 'romanization': 'gwiyeowo'}
|
| 754 |
+
],
|
| 755 |
+
'oppa_gpt': [
|
| 756 |
+
{'hangul': '문법', 'meaning': 'grammar', 'romanization': 'munbeop'},
|
| 757 |
+
{'hangul': '동사', 'meaning': 'verb', 'romanization': 'dongsa'},
|
| 758 |
+
{'hangul': '활용', 'meaning': 'conjugation', 'romanization': 'hwalyong'},
|
| 759 |
+
{'hangul': '언어학', 'meaning': 'linguistics', 'romanization': 'eoneohak'},
|
| 760 |
+
{'hangul': '발음', 'meaning': 'pronunciation', 'romanization': 'bareum'}
|
| 761 |
+
],
|
| 762 |
+
'seonsaengnim_gpt': [
|
| 763 |
+
{'hangul': '공부', 'meaning': 'study', 'romanization': 'gongbu'},
|
| 764 |
+
{'hangul': '숙제', 'meaning': 'homework', 'romanization': 'sukje'},
|
| 765 |
+
{'hangul': '시험', 'meaning': 'test/exam', 'romanization': 'siheom'},
|
| 766 |
+
{'hangul': '학습', 'meaning': 'learning', 'romanization': 'hakseup'},
|
| 767 |
+
{'hangul': '연습', 'meaning': 'practice', 'romanization': 'yeonseup'}
|
| 768 |
+
]
|
| 769 |
+
}
|
| 770 |
+
|
| 771 |
+
words = sample_words.get(agent_name, [])
|
| 772 |
+
if words:
|
| 773 |
+
# Filter out words already discovered to avoid repeats
|
| 774 |
+
new_words = [w for w in words if w['hangul'] not in self.discovered_words]
|
| 775 |
+
if new_words:
|
| 776 |
+
chosen_word = random.choice(new_words)
|
| 777 |
+
self.discovered_words.add(chosen_word['hangul'])
|
| 778 |
+
return chosen_word
|
| 779 |
+
else:
|
| 780 |
+
# If all words from this agent have been learned, occasionally still show one
|
| 781 |
+
if random.random() < 0.3: # 30% chance to repeat a word
|
| 782 |
+
return random.choice(words)
|
| 783 |
+
|
| 784 |
+
return None
|
| 785 |
+
|
| 786 |
+
def show_help(self):
|
| 787 |
+
"""Display the help menu with colorful formatting."""
|
| 788 |
+
self.clear_screen()
|
| 789 |
+
|
| 790 |
+
# Title with decorative border
|
| 791 |
+
print(
|
| 792 |
+
f"\n{Colors.BRIGHT_CYAN}╔═══════════════ 도움말 (HELP) ═══════════════╗{Colors.RESET}"
|
| 793 |
+
)
|
| 794 |
+
|
| 795 |
+
# Command section
|
| 796 |
+
print(
|
| 797 |
+
f"{Colors.BRIGHT_CYAN}║{Colors.RESET} {Colors.BRIGHT_GREEN}Available Commands:{Colors.RESET}"
|
| 798 |
+
)
|
| 799 |
+
|
| 800 |
+
commands = [
|
| 801 |
+
("look", "Look around the current room"),
|
| 802 |
+
(
|
| 803 |
+
f"examine {Colors.YELLOW}[object]{Colors.RESET}",
|
| 804 |
+
"Examine an object in the room",
|
| 805 |
+
),
|
| 806 |
+
(
|
| 807 |
+
f"take {Colors.YELLOW}[object]{Colors.RESET}",
|
| 808 |
+
"Attempt to take an object (usually provides cultural context)",
|
| 809 |
+
),
|
| 810 |
+
(
|
| 811 |
+
f"go {Colors.YELLOW}[room]{Colors.RESET}",
|
| 812 |
+
"Move to a different room (hall, kitchen, garden, bedroom, study, classroom)",
|
| 813 |
+
),
|
| 814 |
+
(
|
| 815 |
+
f"talk {Colors.YELLOW}[message]{Colors.RESET}",
|
| 816 |
+
"Talk to the Korean family member in this room",
|
| 817 |
+
),
|
| 818 |
+
("rooms", "List all available rooms"),
|
| 819 |
+
("help", "Show this help menu"),
|
| 820 |
+
("quit/exit", "Leave the game"),
|
| 821 |
+
]
|
| 822 |
+
|
| 823 |
+
for cmd, desc in commands:
|
| 824 |
+
print(
|
| 825 |
+
f"{Colors.BRIGHT_CYAN}║{Colors.RESET} {Colors.BRIGHT_WHITE}{cmd:<25}{Colors.RESET} {desc}"
|
| 826 |
+
)
|
| 827 |
+
|
| 828 |
+
# Korean learning section
|
| 829 |
+
print(
|
| 830 |
+
f"{Colors.BRIGHT_CYAN}╠═════════════ KOREAN LEARNING ════════════════╣{Colors.RESET}"
|
| 831 |
+
)
|
| 832 |
+
print(
|
| 833 |
+
f"{Colors.BRIGHT_CYAN}║{Colors.RESET} Each conversation teaches you a new Korean word."
|
| 834 |
+
)
|
| 835 |
+
print(
|
| 836 |
+
f"{Colors.BRIGHT_CYAN}║{Colors.RESET} Words are highlighted in {Colors.YELLOW}{Colors.BOLD}yellow{Colors.RESET} and added to your vocabulary."
|
| 837 |
+
)
|
| 838 |
+
|
| 839 |
+
# Progress bar for vocabulary
|
| 840 |
+
learned_count = len(self.discovered_words)
|
| 841 |
+
total_possible = 15 # Rough estimate
|
| 842 |
+
progress_percent = (
|
| 843 |
+
int((learned_count / total_possible) * 100) if total_possible > 0 else 0
|
| 844 |
+
)
|
| 845 |
+
|
| 846 |
+
bar_length = 30
|
| 847 |
+
filled_length = int(bar_length * learned_count // total_possible)
|
| 848 |
+
bar = f"{Colors.GREEN}{'█' * filled_length}{Colors.BRIGHT_BLACK}{'░' * (bar_length - filled_length)}{Colors.RESET}"
|
| 849 |
+
|
| 850 |
+
print(
|
| 851 |
+
f"{Colors.BRIGHT_CYAN}║{Colors.RESET} Words learned: {learned_count}"
|
| 852 |
+
)
|
| 853 |
+
print(f"{Colors.BRIGHT_CYAN}║{Colors.RESET} {bar} {progress_percent}%")
|
| 854 |
+
|
| 855 |
+
# Game objective section
|
| 856 |
+
print(
|
| 857 |
+
f"{Colors.BRIGHT_CYAN}╠═════════════ GAME OBJECTIVE ════════════════╣{Colors.RESET}"
|
| 858 |
+
)
|
| 859 |
+
print(f"{Colors.BRIGHT_CYAN}║{Colors.RESET} Explore the Korean family house and learn Korean!")
|
| 860 |
+
print(f"{Colors.BRIGHT_CYAN}║{Colors.RESET} Talk to each family member to discover their personalities")
|
| 861 |
+
print(f"{Colors.BRIGHT_CYAN}║{Colors.RESET} and learn Korean words through authentic conversations.")
|
| 862 |
+
print(
|
| 863 |
+
f"{Colors.BRIGHT_CYAN}╚═══════════════════════════════════════════════╝{Colors.RESET}"
|
| 864 |
+
)
|
| 865 |
+
|
| 866 |
+
input(f"\n{Colors.GREEN}Press Enter to continue...{Colors.RESET}")
|
| 867 |
+
|
| 868 |
+
def show_intro(self):
|
| 869 |
+
"""Display the game introduction."""
|
| 870 |
+
self.clear_screen()
|
| 871 |
+
print("\n" + "=" * 60)
|
| 872 |
+
print(" KOREAN FAMILY HOUSE - LANGUAGE ADVENTURE")
|
| 873 |
+
print("=" * 60)
|
| 874 |
+
print("\nWelcome to a Korean family house where you'll learn Korean!")
|
| 875 |
+
print("Explore themed rooms and talk to Korean family members.")
|
| 876 |
+
print("Each conversation will teach you new Korean words and culture.")
|
| 877 |
+
print("\nYour Korean Family:")
|
| 878 |
+
print(" - Ahjumma (Kitchen): Honorifics and formality")
|
| 879 |
+
print(" - Ahjussi (Garden): Traditional culture and wisdom")
|
| 880 |
+
print(" - Unni (Bedroom): K-pop and modern Korean")
|
| 881 |
+
print(" - Oppa (Study): Grammar and linguistics")
|
| 882 |
+
print(" - Seonsaengnim (Classroom): General Korean lessons")
|
| 883 |
+
print("\nType 'help' anytime to see available commands.")
|
| 884 |
+
print("\nPress Enter to begin your Korean learning adventure...")
|
| 885 |
+
input()
|
| 886 |
+
|
| 887 |
+
def list_rooms(self):
|
| 888 |
+
"""List all available rooms with their themes."""
|
| 889 |
+
self.clear_screen()
|
| 890 |
+
|
| 891 |
+
print(f"\n{Colors.BRIGHT_YELLOW}╔══════════ KOREAN FAMILY HOUSE ROOMS ══════════╗{Colors.RESET}")
|
| 892 |
+
|
| 893 |
+
for room_name, agent_name in self.room_mapping.items():
|
| 894 |
+
room_info = self._get_room_info(room_name)
|
| 895 |
+
npc_info = self._get_npc_info(agent_name)
|
| 896 |
+
current = f" {Colors.BRIGHT_GREEN}← YOU ARE HERE{Colors.RESET}" if room_name == self.current_room else ""
|
| 897 |
+
|
| 898 |
+
print(f"{Colors.BRIGHT_YELLOW}║{Colors.RESET} {room_info['display_name']}")
|
| 899 |
+
print(f"{Colors.BRIGHT_YELLOW}║{Colors.RESET} Character: {npc_info['name']}{current}")
|
| 900 |
+
print(f"{Colors.BRIGHT_YELLOW}║{Colors.RESET}")
|
| 901 |
+
|
| 902 |
+
print(f"{Colors.BRIGHT_YELLOW}╚═══════════════════════════════════════════════╝{Colors.RESET}")
|
| 903 |
+
input(f"\n{Colors.GREEN}Press Enter to continue...{Colors.RESET}")
|
| 904 |
+
|
| 905 |
+
def handle_command(self, command):
|
| 906 |
+
"""Process the player's command."""
|
| 907 |
+
parts = command.lower().split()
|
| 908 |
+
|
| 909 |
+
if not parts:
|
| 910 |
+
return
|
| 911 |
+
|
| 912 |
+
action = parts[0]
|
| 913 |
+
|
| 914 |
+
# Basic directional movement shortcuts
|
| 915 |
+
if action in ["north", "south", "east", "west", "up", "down"]:
|
| 916 |
+
self.move_player(action)
|
| 917 |
+
return
|
| 918 |
+
|
| 919 |
+
# Standard commands
|
| 920 |
+
if action == "look":
|
| 921 |
+
# Room will redisplay automatically in main loop
|
| 922 |
+
return
|
| 923 |
+
|
| 924 |
+
elif action == "go" and len(parts) > 1:
|
| 925 |
+
room_name = parts[1]
|
| 926 |
+
result = self.go_to_room(room_name)
|
| 927 |
+
if "moved" not in result: # Only print if there was an error
|
| 928 |
+
print(result)
|
| 929 |
+
time.sleep(1.5)
|
| 930 |
+
|
| 931 |
+
elif action == "examine" or action == "look" and len(parts) > 1:
|
| 932 |
+
if len(parts) > 1:
|
| 933 |
+
object_name = " ".join(parts[1:])
|
| 934 |
+
result = self.examine_object(object_name)
|
| 935 |
+
print(f"\n{result}")
|
| 936 |
+
input(f"\n{Colors.GREEN}Press Enter to continue...{Colors.RESET}")
|
| 937 |
+
else:
|
| 938 |
+
print(f"{Colors.YELLOW}Examine what? Try 'examine <object>'.{Colors.RESET}")
|
| 939 |
+
time.sleep(1.5)
|
| 940 |
+
|
| 941 |
+
elif action == "take":
|
| 942 |
+
if len(parts) > 1:
|
| 943 |
+
object_name = " ".join(parts[1:])
|
| 944 |
+
result = self.take_object(object_name)
|
| 945 |
+
print(f"\n{result}")
|
| 946 |
+
input(f"\n{Colors.GREEN}Press Enter to continue...{Colors.RESET}")
|
| 947 |
+
else:
|
| 948 |
+
print(f"{Colors.YELLOW}Take what? Try 'take <object>'.{Colors.RESET}")
|
| 949 |
+
time.sleep(1.5)
|
| 950 |
+
|
| 951 |
+
elif action == "talk":
|
| 952 |
+
if len(parts) > 1:
|
| 953 |
+
# User provided a message
|
| 954 |
+
message = " ".join(parts[1:])
|
| 955 |
+
else:
|
| 956 |
+
# Just "talk" - start a conversation
|
| 957 |
+
message = "Hello!"
|
| 958 |
+
|
| 959 |
+
self.talk_to_npc(message)
|
| 960 |
+
input(f"\n{Colors.GREEN}Press Enter to continue...{Colors.RESET}")
|
| 961 |
+
|
| 962 |
+
elif action == "rooms":
|
| 963 |
+
self.list_rooms()
|
| 964 |
+
|
| 965 |
+
elif action == "help" or action == "?":
|
| 966 |
+
self.show_help()
|
| 967 |
+
|
| 968 |
+
elif action in ["exit", "quit"]:
|
| 969 |
+
self.game_running = False
|
| 970 |
+
print(
|
| 971 |
+
f"{Colors.BRIGHT_GREEN}Thank you for playing! 안녕히 가세요 (Goodbye)!{Colors.RESET}"
|
| 972 |
+
)
|
| 973 |
+
|
| 974 |
+
else:
|
| 975 |
+
print(f"{Colors.YELLOW}I don't understand '{command}'. Type 'help' for commands.{Colors.RESET}")
|
| 976 |
+
time.sleep(1.5)
|
| 977 |
+
|
| 978 |
+
def move_player(self, direction):
|
| 979 |
+
"""Move the player in the specified direction."""
|
| 980 |
+
exits = self._get_room_exits()
|
| 981 |
+
|
| 982 |
+
if direction in exits:
|
| 983 |
+
self.current_room = exits[direction]
|
| 984 |
+
else:
|
| 985 |
+
available_directions = ", ".join(exits.keys()) if exits else "none"
|
| 986 |
+
print(f"{Colors.YELLOW}You can't go {direction} from here. Available: {available_directions}{Colors.RESET}")
|
| 987 |
+
time.sleep(1.5)
|
| 988 |
+
|
| 989 |
+
def run(self):
|
| 990 |
+
"""Main game loop."""
|
| 991 |
+
self.show_intro()
|
| 992 |
+
|
| 993 |
+
while self.game_running:
|
| 994 |
+
self.display_room()
|
| 995 |
+
try:
|
| 996 |
+
command = input()
|
| 997 |
+
self.handle_command(command)
|
| 998 |
+
except KeyboardInterrupt:
|
| 999 |
+
print(f"\n{Colors.BRIGHT_GREEN}Goodbye! 안녕히 가세요!{Colors.RESET}")
|
| 1000 |
+
break
|
| 1001 |
+
|
| 1002 |
+
|
| 1003 |
+
def main():
|
| 1004 |
+
"""
|
| 1005 |
+
CLI interface for the Korean Learning MUD game.
|
| 1006 |
+
"""
|
| 1007 |
+
try:
|
| 1008 |
+
game = KoreanLearningMUD()
|
| 1009 |
+
game.run()
|
| 1010 |
+
|
| 1011 |
+
# Show final vocabulary learned
|
| 1012 |
+
if game.discovered_words:
|
| 1013 |
+
print(f"\n{Colors.BRIGHT_YELLOW}Korean words you learned:{Colors.RESET}")
|
| 1014 |
+
for word in game.discovered_words:
|
| 1015 |
+
print(f" - {word}")
|
| 1016 |
+
|
| 1017 |
+
except KeyboardInterrupt:
|
| 1018 |
+
print(f"\n{Colors.BRIGHT_GREEN}Goodbye! 안녕히 가세요!{Colors.RESET}")
|
| 1019 |
+
except Exception as e:
|
| 1020 |
+
print(f"{Colors.RED}Game error: {e}{Colors.RESET}")
|
| 1021 |
+
|
| 1022 |
+
|
| 1023 |
+
def _check_object_objectives(self, object_name: str) -> str:
|
| 1024 |
+
"""
|
| 1025 |
+
Check if examining this object completes any learning objectives.
|
| 1026 |
+
"""
|
| 1027 |
+
if self.current_room not in self.room_objectives:
|
| 1028 |
+
return ""
|
| 1029 |
+
|
| 1030 |
+
# Define which objects complete which objectives
|
| 1031 |
+
object_objective_map = {
|
| 1032 |
+
'kitchen': {
|
| 1033 |
+
'shoes': 'entrance_etiquette',
|
| 1034 |
+
'family_photo': 'family_honorifics'
|
| 1035 |
+
},
|
| 1036 |
+
'garden': {
|
| 1037 |
+
'poetry_scroll': 'poetry_appreciation',
|
| 1038 |
+
'bamboo': 'proverb_wisdom',
|
| 1039 |
+
'makgeolli_bottle': 'cultural_sharing'
|
| 1040 |
+
},
|
| 1041 |
+
'bedroom': {
|
| 1042 |
+
'album_collection': 'lyrics_translation',
|
| 1043 |
+
'photocards': 'modern_slang'
|
| 1044 |
+
},
|
| 1045 |
+
'study': {
|
| 1046 |
+
'grammar_books': 'grammar_mastery',
|
| 1047 |
+
'thesis_papers': 'academic_korean'
|
| 1048 |
+
},
|
| 1049 |
+
'classroom': {
|
| 1050 |
+
'textbooks': 'practical_conversation',
|
| 1051 |
+
'whiteboard': 'pronunciation_practice'
|
| 1052 |
+
}
|
| 1053 |
+
}
|
| 1054 |
+
|
| 1055 |
+
room_map = object_objective_map.get(self.current_room, {})
|
| 1056 |
+
objective_key = room_map.get(object_name)
|
| 1057 |
+
|
| 1058 |
+
if objective_key and objective_key in self.room_objectives[self.current_room]:
|
| 1059 |
+
if not self.room_objectives[self.current_room][objective_key]['completed']:
|
| 1060 |
+
self.room_objectives[self.current_room][objective_key]['completed'] = True
|
| 1061 |
+
return self.room_objectives[self.current_room][objective_key]['description']
|
| 1062 |
+
|
| 1063 |
+
return ""
|
| 1064 |
+
|
| 1065 |
+
def get_room_progress(self) -> dict:
|
| 1066 |
+
"""
|
| 1067 |
+
Get learning progress for current room.
|
| 1068 |
+
"""
|
| 1069 |
+
if self.current_room not in self.room_objectives:
|
| 1070 |
+
return {'completed': 0, 'total': 0, 'objectives': []}
|
| 1071 |
+
|
| 1072 |
+
objectives = self.room_objectives[self.current_room]
|
| 1073 |
+
completed = sum(1 for obj in objectives.values() if obj['completed'])
|
| 1074 |
+
total = len(objectives)
|
| 1075 |
+
|
| 1076 |
+
return {
|
| 1077 |
+
'completed': completed,
|
| 1078 |
+
'total': total,
|
| 1079 |
+
'objectives': [{
|
| 1080 |
+
'name': key,
|
| 1081 |
+
'description': obj['description'],
|
| 1082 |
+
'completed': obj['completed']
|
| 1083 |
+
} for key, obj in objectives.items()]
|
| 1084 |
+
}
|
| 1085 |
+
|
| 1086 |
+
|
| 1087 |
+
if __name__ == "__main__":
|
| 1088 |
+
main()
|
uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
web_server.py
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
"""
|
| 3 |
+
FastAPI web server for Korean Learning MUD game.
|
| 4 |
+
Provides REST API endpoints for the HTML frontend.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
import logging
|
| 10 |
+
from typing import Dict, Any, List
|
| 11 |
+
from fastapi import FastAPI, HTTPException
|
| 12 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 13 |
+
from fastapi.staticfiles import StaticFiles
|
| 14 |
+
from fastapi.responses import HTMLResponse
|
| 15 |
+
from pydantic import BaseModel
|
| 16 |
+
import uvicorn
|
| 17 |
+
|
| 18 |
+
# Add src to Python path
|
| 19 |
+
sys.path.append(os.path.join(os.path.dirname(__file__), 'src'))
|
| 20 |
+
|
| 21 |
+
try:
|
| 22 |
+
from korean_cpc_agents.mud_game import KoreanLearningMUD
|
| 23 |
+
except ImportError as e:
|
| 24 |
+
print(f"Error importing MUD game: {e}")
|
| 25 |
+
print("Make sure you're running from the project root directory")
|
| 26 |
+
sys.exit(1)
|
| 27 |
+
|
| 28 |
+
# Configure logging
|
| 29 |
+
logging.basicConfig(level=logging.INFO)
|
| 30 |
+
logger = logging.getLogger(__name__)
|
| 31 |
+
|
| 32 |
+
# FastAPI app
|
| 33 |
+
app = FastAPI(title="Korean Learning MUD Game", version="1.0.0")
|
| 34 |
+
|
| 35 |
+
# CORS middleware for frontend
|
| 36 |
+
app.add_middleware(
|
| 37 |
+
CORSMiddleware,
|
| 38 |
+
allow_origins=["*"],
|
| 39 |
+
allow_credentials=True,
|
| 40 |
+
allow_methods=["*"],
|
| 41 |
+
allow_headers=["*"],
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
# Mount static files for assets
|
| 45 |
+
app.mount("/assets", StaticFiles(directory="assets"), name="assets")
|
| 46 |
+
|
| 47 |
+
# Global game instance
|
| 48 |
+
game_instance = None
|
| 49 |
+
|
| 50 |
+
# Pydantic models for API
|
| 51 |
+
class CommandRequest(BaseModel):
|
| 52 |
+
command: str
|
| 53 |
+
|
| 54 |
+
class TalkRequest(BaseModel):
|
| 55 |
+
message: str
|
| 56 |
+
|
| 57 |
+
class GameResponse(BaseModel):
|
| 58 |
+
success: bool
|
| 59 |
+
message: str
|
| 60 |
+
data: Dict[str, Any] = {}
|
| 61 |
+
|
| 62 |
+
class GameState(BaseModel):
|
| 63 |
+
current_room: str
|
| 64 |
+
discovered_words: List[str]
|
| 65 |
+
game_progress: Dict[str, Any]
|
| 66 |
+
|
| 67 |
+
@app.on_event("startup")
|
| 68 |
+
async def startup_event():
|
| 69 |
+
"""Initialize the game on server startup"""
|
| 70 |
+
global game_instance
|
| 71 |
+
try:
|
| 72 |
+
logger.info("Initializing Korean Learning MUD game...")
|
| 73 |
+
game_instance = KoreanLearningMUD(web_mode=True) # Enable web mode to avoid terminal encoding
|
| 74 |
+
logger.info("Game initialized successfully!")
|
| 75 |
+
except Exception as e:
|
| 76 |
+
logger.error(f"Failed to initialize game: {e}")
|
| 77 |
+
raise
|
| 78 |
+
|
| 79 |
+
@app.get("/")
|
| 80 |
+
async def read_root():
|
| 81 |
+
"""Serve the main game HTML page"""
|
| 82 |
+
try:
|
| 83 |
+
import os
|
| 84 |
+
logger.info(f"Current working directory: {os.getcwd()}")
|
| 85 |
+
logger.info(f"Looking for file: korean_mud_game.html")
|
| 86 |
+
logger.info(f"File exists: {os.path.exists('korean_mud_game.html')}")
|
| 87 |
+
|
| 88 |
+
with open("korean_mud_game.html", "r", encoding="utf-8") as f:
|
| 89 |
+
html_content = f.read()
|
| 90 |
+
logger.info(f"Successfully read {len(html_content)} characters from HTML file")
|
| 91 |
+
return HTMLResponse(content=html_content)
|
| 92 |
+
except FileNotFoundError as e:
|
| 93 |
+
logger.error(f"FileNotFoundError: {e}")
|
| 94 |
+
return HTMLResponse(content="<h1>Game file not found. Please run the server from the project root.</h1>")
|
| 95 |
+
except Exception as e:
|
| 96 |
+
logger.error(f"Unexpected error reading HTML file: {e}")
|
| 97 |
+
return HTMLResponse(content=f"<h1>Error loading game: {str(e)}</h1>")
|
| 98 |
+
|
| 99 |
+
@app.get("/api/game/state")
|
| 100 |
+
async def get_game_state() -> GameState:
|
| 101 |
+
"""Get current game state"""
|
| 102 |
+
if not game_instance:
|
| 103 |
+
raise HTTPException(status_code=500, detail="Game not initialized")
|
| 104 |
+
|
| 105 |
+
try:
|
| 106 |
+
return GameState(
|
| 107 |
+
current_room=game_instance.current_room,
|
| 108 |
+
discovered_words=list(game_instance.discovered_words),
|
| 109 |
+
game_progress=game_instance.game_progress
|
| 110 |
+
)
|
| 111 |
+
except Exception as e:
|
| 112 |
+
logger.error(f"Error getting game state: {e}")
|
| 113 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 114 |
+
|
| 115 |
+
@app.get("/api/game/room-info")
|
| 116 |
+
async def get_room_info() -> GameResponse:
|
| 117 |
+
"""Get current room information"""
|
| 118 |
+
if not game_instance:
|
| 119 |
+
raise HTTPException(status_code=500, detail="Game not initialized")
|
| 120 |
+
|
| 121 |
+
try:
|
| 122 |
+
room_info = game_instance.look_around()
|
| 123 |
+
|
| 124 |
+
# Get room mapping and NPC info
|
| 125 |
+
current_room = game_instance.current_room
|
| 126 |
+
agent_name = game_instance.room_mapping.get(current_room)
|
| 127 |
+
npc_info = {}
|
| 128 |
+
|
| 129 |
+
if agent_name:
|
| 130 |
+
npc_info = game_instance._get_npc_info(agent_name)
|
| 131 |
+
|
| 132 |
+
# Get objects in current room
|
| 133 |
+
room_objects = game_instance.room_objects.get(current_room, {})
|
| 134 |
+
objects_list = [{"name": obj_data["name"], "description": obj_data["description"]}
|
| 135 |
+
for obj_data in room_objects.values()]
|
| 136 |
+
|
| 137 |
+
return GameResponse(
|
| 138 |
+
success=True,
|
| 139 |
+
message=room_info,
|
| 140 |
+
data={
|
| 141 |
+
"current_room": current_room,
|
| 142 |
+
"npc_info": npc_info,
|
| 143 |
+
"room_mapping": game_instance.room_mapping,
|
| 144 |
+
"discovered_words": list(game_instance.discovered_words),
|
| 145 |
+
"objects": objects_list
|
| 146 |
+
}
|
| 147 |
+
)
|
| 148 |
+
except Exception as e:
|
| 149 |
+
logger.error(f"Error getting room info: {e}")
|
| 150 |
+
return GameResponse(success=False, message=f"Error: {str(e)}")
|
| 151 |
+
|
| 152 |
+
@app.post("/api/game/move")
|
| 153 |
+
async def move_to_room(request: CommandRequest) -> GameResponse:
|
| 154 |
+
"""Move to a different room"""
|
| 155 |
+
if not game_instance:
|
| 156 |
+
raise HTTPException(status_code=500, detail="Game not initialized")
|
| 157 |
+
|
| 158 |
+
try:
|
| 159 |
+
# Extract room name from command (e.g., "go kitchen" -> "kitchen")
|
| 160 |
+
parts = request.command.strip().split()
|
| 161 |
+
if len(parts) < 2:
|
| 162 |
+
return GameResponse(success=False, message="Please specify which room to go to")
|
| 163 |
+
|
| 164 |
+
room_name = parts[1].lower()
|
| 165 |
+
result = game_instance.go_to_room(room_name)
|
| 166 |
+
|
| 167 |
+
# Get NPC info for the new room
|
| 168 |
+
current_room = game_instance.current_room
|
| 169 |
+
agent_name = game_instance.room_mapping.get(current_room)
|
| 170 |
+
npc_info = {}
|
| 171 |
+
if agent_name:
|
| 172 |
+
npc_info = game_instance._get_npc_info(agent_name)
|
| 173 |
+
|
| 174 |
+
return GameResponse(
|
| 175 |
+
success=True,
|
| 176 |
+
message=result,
|
| 177 |
+
data={
|
| 178 |
+
"current_room": current_room,
|
| 179 |
+
"discovered_words": list(game_instance.discovered_words),
|
| 180 |
+
"room_objects": [obj['name'] for obj in game_instance.room_objects.get(current_room, {}).values()],
|
| 181 |
+
"room_mapping": game_instance.room_mapping,
|
| 182 |
+
"npc_info": npc_info
|
| 183 |
+
}
|
| 184 |
+
)
|
| 185 |
+
except Exception as e:
|
| 186 |
+
logger.error(f"Error moving to room: {e}")
|
| 187 |
+
return GameResponse(success=False, message=f"Error: {str(e)}")
|
| 188 |
+
|
| 189 |
+
@app.post("/api/game/talk")
|
| 190 |
+
async def talk_to_npc(request: TalkRequest) -> GameResponse:
|
| 191 |
+
"""Talk to the NPC in current room"""
|
| 192 |
+
if not game_instance:
|
| 193 |
+
raise HTTPException(status_code=500, detail="Game not initialized")
|
| 194 |
+
|
| 195 |
+
try:
|
| 196 |
+
logger.info(f"Player message: {request.message}")
|
| 197 |
+
response = game_instance.talk_to_npc(request.message)
|
| 198 |
+
|
| 199 |
+
return GameResponse(
|
| 200 |
+
success=True,
|
| 201 |
+
message=response,
|
| 202 |
+
data={
|
| 203 |
+
"current_room": game_instance.current_room,
|
| 204 |
+
"discovered_words": list(game_instance.discovered_words),
|
| 205 |
+
"room_objects": [obj['name'] for obj in game_instance.room_objects.get(game_instance.current_room, {}).values()]
|
| 206 |
+
}
|
| 207 |
+
)
|
| 208 |
+
except Exception as e:
|
| 209 |
+
logger.error(f"Error talking to NPC: {e}")
|
| 210 |
+
return GameResponse(success=False, message=f"NPC couldn't respond: {str(e)}")
|
| 211 |
+
|
| 212 |
+
@app.get("/api/game/rooms")
|
| 213 |
+
async def list_rooms() -> GameResponse:
|
| 214 |
+
"""Get list of available rooms"""
|
| 215 |
+
if not game_instance:
|
| 216 |
+
raise HTTPException(status_code=500, detail="Game not initialized")
|
| 217 |
+
|
| 218 |
+
try:
|
| 219 |
+
rooms = list(game_instance.room_mapping.keys())
|
| 220 |
+
return GameResponse(
|
| 221 |
+
success=True,
|
| 222 |
+
message="Available rooms",
|
| 223 |
+
data={"rooms": rooms, "room_mapping": game_instance.room_mapping}
|
| 224 |
+
)
|
| 225 |
+
except Exception as e:
|
| 226 |
+
logger.error(f"Error listing rooms: {e}")
|
| 227 |
+
return GameResponse(success=False, message=f"Error: {str(e)}")
|
| 228 |
+
|
| 229 |
+
@app.get("/api/game/help")
|
| 230 |
+
async def get_help() -> GameResponse:
|
| 231 |
+
"""Get help information with proper formatting"""
|
| 232 |
+
help_text = """🎮 KOREAN LEARNING MUD GAME HELP
|
| 233 |
+
|
| 234 |
+
📋 BASIC COMMANDS:
|
| 235 |
+
• look / l → Look around your current room
|
| 236 |
+
• examine [object] → Examine an object in detail
|
| 237 |
+
• take [object] → Try to take an object (learn cultural context)
|
| 238 |
+
• go [room] → Move to another room (hall, kitchen, garden, bedroom, study, classroom)
|
| 239 |
+
• talk [message] → Chat with Korean family member in your room
|
| 240 |
+
• rooms / map → See all available rooms and who's there
|
| 241 |
+
• help / ? → Show this help menu
|
| 242 |
+
|
| 243 |
+
🚀 QUICK SHORTCUTS:
|
| 244 |
+
• n/s/e/w → Go north/south/east/west
|
| 245 |
+
• Just type what you want to say directly (no "talk" needed!)
|
| 246 |
+
|
| 247 |
+
🏠 KOREAN FAMILY HOUSE LAYOUT:
|
| 248 |
+
🌸 Garden (정원)
|
| 249 |
+
👴 Grandpa Park
|
| 250 |
+
|
|
| 251 |
+
🍳 Kitchen ---- 🏠 Hall ---- 📚 Study
|
| 252 |
+
👵 Grandma 📖 Brother Jung
|
| 253 |
+
|
|
| 254 |
+
✏️ Classroom
|
| 255 |
+
👩🏫 Teacher Choi
|
| 256 |
+
|
|
| 257 |
+
🎵 Bedroom
|
| 258 |
+
👩 Sister Lee
|
| 259 |
+
|
| 260 |
+
🎯 GAME OBJECTIVE:
|
| 261 |
+
Learn Korean by talking to different family members! Each has their own personality:
|
| 262 |
+
• 👵 Grandma Kim (Kitchen): Teaches honorifics and formal speech
|
| 263 |
+
• 👴 Grandpa Park (Garden): Shares traditional culture and wisdom
|
| 264 |
+
• 👩 Sister Lee (Bedroom): Teaches K-pop slang and modern Korean
|
| 265 |
+
• 📖 Brother Jung (Study): Grammar expert and linguistic genius
|
| 266 |
+
• 👩🏫 Teacher Choi (Classroom): General Korean lessons and practical phrases
|
| 267 |
+
|
| 268 |
+
💬 HOW TO PLAY:
|
| 269 |
+
Just type naturally! Say "Hello", "Teach me Korean", "What's your favorite food?"
|
| 270 |
+
Examine objects in each room to learn about Korean culture!
|
| 271 |
+
The game will understand most commands. When in doubt, just talk to the NPCs!
|
| 272 |
+
|
| 273 |
+
🔍 INTERACTIVE OBJECTS:
|
| 274 |
+
Each room has objects you can examine. Try 'examine shoes' or 'look at family photo'.
|
| 275 |
+
Some objects trigger special conversations with family members!"""
|
| 276 |
+
|
| 277 |
+
return GameResponse(
|
| 278 |
+
success=True,
|
| 279 |
+
message=help_text
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
@app.post("/api/game/examine")
|
| 283 |
+
async def examine_object(request: CommandRequest) -> GameResponse:
|
| 284 |
+
"""Examine an object in the current room"""
|
| 285 |
+
if not game_instance:
|
| 286 |
+
raise HTTPException(status_code=500, detail="Game not initialized")
|
| 287 |
+
|
| 288 |
+
try:
|
| 289 |
+
# Extract object name from command
|
| 290 |
+
parts = request.command.strip().split()
|
| 291 |
+
if len(parts) < 2:
|
| 292 |
+
return GameResponse(success=False, message="Please specify which object to examine")
|
| 293 |
+
|
| 294 |
+
object_name = " ".join(parts[1:])
|
| 295 |
+
result = game_instance.examine_object(object_name)
|
| 296 |
+
|
| 297 |
+
return GameResponse(
|
| 298 |
+
success=True,
|
| 299 |
+
message=result,
|
| 300 |
+
data={
|
| 301 |
+
"current_room": game_instance.current_room,
|
| 302 |
+
"discovered_words": list(game_instance.discovered_words),
|
| 303 |
+
"room_objects": [obj['name'] for obj in game_instance.room_objects.get(game_instance.current_room, {}).values()],
|
| 304 |
+
"player_inventory": list(game_instance.player_inventory)
|
| 305 |
+
}
|
| 306 |
+
)
|
| 307 |
+
except Exception as e:
|
| 308 |
+
logger.error(f"Error examining object: {e}")
|
| 309 |
+
return GameResponse(success=False, message=f"Error: {str(e)}")
|
| 310 |
+
|
| 311 |
+
@app.post("/api/game/take")
|
| 312 |
+
async def take_object(request: CommandRequest) -> GameResponse:
|
| 313 |
+
"""Try to take an object in the current room"""
|
| 314 |
+
if not game_instance:
|
| 315 |
+
raise HTTPException(status_code=500, detail="Game not initialized")
|
| 316 |
+
|
| 317 |
+
try:
|
| 318 |
+
# Extract object name from command
|
| 319 |
+
parts = request.command.strip().split()
|
| 320 |
+
if len(parts) < 2:
|
| 321 |
+
return GameResponse(success=False, message="Please specify which object to take")
|
| 322 |
+
|
| 323 |
+
object_name = " ".join(parts[1:])
|
| 324 |
+
result = game_instance.take_object(object_name)
|
| 325 |
+
|
| 326 |
+
return GameResponse(
|
| 327 |
+
success=True,
|
| 328 |
+
message=result,
|
| 329 |
+
data={
|
| 330 |
+
"current_room": game_instance.current_room,
|
| 331 |
+
"discovered_words": list(game_instance.discovered_words),
|
| 332 |
+
"room_objects": [obj['name'] for obj in game_instance.room_objects.get(game_instance.current_room, {}).values()],
|
| 333 |
+
"player_inventory": list(game_instance.player_inventory)
|
| 334 |
+
}
|
| 335 |
+
)
|
| 336 |
+
except Exception as e:
|
| 337 |
+
logger.error(f"Error taking object: {e}")
|
| 338 |
+
return GameResponse(success=False, message=f"Error: {str(e)}")
|
| 339 |
+
|
| 340 |
+
@app.post("/api/game/command")
|
| 341 |
+
async def process_command(request: CommandRequest) -> GameResponse:
|
| 342 |
+
"""Process a general game command with flexible parsing"""
|
| 343 |
+
if not game_instance:
|
| 344 |
+
raise HTTPException(status_code=500, detail="Game not initialized")
|
| 345 |
+
|
| 346 |
+
try:
|
| 347 |
+
command = request.command.strip()
|
| 348 |
+
command_lower = command.lower()
|
| 349 |
+
|
| 350 |
+
# More flexible command parsing
|
| 351 |
+
if any(cmd in command_lower for cmd in ['look', 'see', 'observe', '보기', 'l']):
|
| 352 |
+
result = game_instance.look_around()
|
| 353 |
+
return GameResponse(success=True, message=result)
|
| 354 |
+
|
| 355 |
+
elif any(cmd in command_lower for cmd in ['go', 'move', 'travel', 'enter', '이동']):
|
| 356 |
+
return await move_to_room(request)
|
| 357 |
+
|
| 358 |
+
elif any(cmd in command_lower for cmd in ['rooms', 'list', 'map', '방목록', 'where']):
|
| 359 |
+
# Return formatted room list with map
|
| 360 |
+
rooms_data = await list_rooms()
|
| 361 |
+
if rooms_data.success:
|
| 362 |
+
room_list = "\n".join([f"• {room.title()} - {game_instance._get_npc_info(game_instance.room_mapping[room])['name'] if game_instance.room_mapping[room] else 'No NPC'}" for room in rooms_data.data['rooms']])
|
| 363 |
+
map_text = f"🏠 KOREAN FAMILY HOUSE MAP 🇰🇷\n\n" + \
|
| 364 |
+
f" 🌸 Garden (정원)\n" + \
|
| 365 |
+
f" 👴 Grandpa Park\n" + \
|
| 366 |
+
f" |\n" + \
|
| 367 |
+
f" 🍳 Kitchen ---- 🏠 Hall ---- 📚 Study\n" + \
|
| 368 |
+
f" 👵 Grandma Kim 📖 Brother Jung\n" + \
|
| 369 |
+
f" |\n" + \
|
| 370 |
+
f" ✏️ Classroom (교실)\n" + \
|
| 371 |
+
f" 👩🏫 Teacher Choi\n" + \
|
| 372 |
+
f" |\n" + \
|
| 373 |
+
f" 🎵 Bedroom (침실)\n" + \
|
| 374 |
+
f" 👩 Sister Lee\n\n" + \
|
| 375 |
+
f"🚪 Available Rooms:\n{room_list}\n\nUse 'go [room]' to move around!"
|
| 376 |
+
return GameResponse(success=True, message=map_text)
|
| 377 |
+
return rooms_data
|
| 378 |
+
|
| 379 |
+
elif any(cmd in command_lower for cmd in ['help', 'commands', '도움', '?', 'h']):
|
| 380 |
+
return await get_help()
|
| 381 |
+
|
| 382 |
+
elif any(cmd in command_lower for cmd in ['talk', 'say', 'speak', 'tell', '대화', 'chat']):
|
| 383 |
+
# Extract message after talk command
|
| 384 |
+
talk_words = ['talk', 'say', 'speak', 'tell', '대화', 'chat']
|
| 385 |
+
message = command
|
| 386 |
+
for word in talk_words:
|
| 387 |
+
if word in command_lower:
|
| 388 |
+
parts = command.split(word, 1)
|
| 389 |
+
if len(parts) > 1:
|
| 390 |
+
message = parts[1].strip()
|
| 391 |
+
if message:
|
| 392 |
+
break
|
| 393 |
+
|
| 394 |
+
if not message or message == command:
|
| 395 |
+
message = "Hello!"
|
| 396 |
+
|
| 397 |
+
# Use talk endpoint
|
| 398 |
+
talk_request = TalkRequest(message=message)
|
| 399 |
+
return await talk_to_npc(talk_request)
|
| 400 |
+
|
| 401 |
+
elif any(cmd in command_lower for cmd in ['north', 'south', 'east', 'west', 'n', 's', 'e', 'w']):
|
| 402 |
+
# Direction shortcuts from hall
|
| 403 |
+
directions = {
|
| 404 |
+
'north': 'garden', 'n': 'garden',
|
| 405 |
+
'south': 'classroom', 's': 'classroom',
|
| 406 |
+
'east': 'study', 'e': 'study',
|
| 407 |
+
'west': 'kitchen', 'w': 'kitchen'
|
| 408 |
+
}
|
| 409 |
+
for direction, room in directions.items():
|
| 410 |
+
if direction in command_lower:
|
| 411 |
+
move_request = CommandRequest(command=f"go {room}")
|
| 412 |
+
return await move_to_room(move_request)
|
| 413 |
+
|
| 414 |
+
else:
|
| 415 |
+
# Try to interpret as a talk message if it doesn't match commands
|
| 416 |
+
if len(command.split()) > 1 and not any(cmd in command_lower for cmd in ['go', 'move', 'travel', 'examine', 'take']):
|
| 417 |
+
talk_request = TalkRequest(message=command)
|
| 418 |
+
return await talk_to_npc(talk_request)
|
| 419 |
+
|
| 420 |
+
return GameResponse(
|
| 421 |
+
success=False,
|
| 422 |
+
message=f"🤔 I don't understand '{command}'. Try 'help' for commands, or just type what you want to say to the NPC!"
|
| 423 |
+
)
|
| 424 |
+
|
| 425 |
+
except Exception as e:
|
| 426 |
+
logger.error(f"Error processing command: {e}")
|
| 427 |
+
return GameResponse(success=False, message=f"Error: {str(e)}")
|
| 428 |
+
|
| 429 |
+
if __name__ == "__main__":
|
| 430 |
+
print("Starting Korean Learning MUD Web Server...")
|
| 431 |
+
print("Make sure you have created 'korean_mud_game.html' in the project root")
|
| 432 |
+
print("Server will be available at: http://localhost:7860")
|
| 433 |
+
|
| 434 |
+
# Use 0.0.0.0 for Docker compatibility, 127.0.0.1 for local dev
|
| 435 |
+
host = "0.0.0.0" if os.getenv("DOCKER_ENV") else "127.0.0.1"
|
| 436 |
+
|
| 437 |
+
uvicorn.run(
|
| 438 |
+
"web_server:app",
|
| 439 |
+
host=host,
|
| 440 |
+
port=7860,
|
| 441 |
+
reload=False if os.getenv("DOCKER_ENV") else True,
|
| 442 |
+
log_level="info"
|
| 443 |
+
)
|