ramsi-k commited on
Commit
fc190df
·
1 Parent(s): 0e4f2c9
Files changed (36) hide show
  1. .dockerignore +231 -0
  2. .gitattributes +4 -0
  3. .gitignore +225 -0
  4. Dockerfile +47 -0
  5. README.md +143 -1
  6. assets/images/5d2cd6b023516.gif +3 -0
  7. assets/images/8poegbt86i.jpg +3 -0
  8. assets/images/Ahjumma ChatGPT Image Mar 26, 2025, 05_36_15 PM.png +3 -0
  9. assets/images/Ahjumma and Ahjussi.png +3 -0
  10. assets/images/Ahjumma loved kimchi ChatGPT Image Mar 26, 2025, 06_05_13 PM.png +3 -0
  11. assets/images/Ahjussi ChatGPT Image May 5, 2025, 04_37_07 PM.png +3 -0
  12. assets/images/Family portrait ChatGPT Image Mar 26, 2025, 06_42_16 PM.png +3 -0
  13. assets/images/Family portraits 2ChatGPT Image Aug 17, 2025, 10_48_14 PM.png +3 -0
  14. assets/images/Idol ChatGPT Image Aug 17, 2025, 10_45_48 PM.png +3 -0
  15. assets/images/K-pop trainee ChatGPT Image May 5, 2025, 04_35_54 PM.png +3 -0
  16. assets/images/SeonsaengnimChatGPT Image Aug 17, 2025, 10_41_39 PM.png +3 -0
  17. assets/images/SunbaeOppa ChatGPT Image May 5, 2025, 04_07_13 PM.png +3 -0
  18. assets/images/app-running.png +3 -0
  19. assets/images/bukchon-hanok-thumb-scaled.jpg +3 -0
  20. assets/images/chat-grandfather.png +3 -0
  21. assets/images/chat-grandmother.png +3 -0
  22. assets/images/chat-words+inventory.png +3 -0
  23. assets/images/game poster ChatGPT Image May 5, 2025, 05_02_06 PM.png +3 -0
  24. assets/images/images.jpg +3 -0
  25. assets/images/repeat-background.jpg +3 -0
  26. docker-compose.yml +26 -0
  27. korean_mud_game.html +1219 -0
  28. pyproject.toml +29 -0
  29. src/korean_cpc_agents/__init__.py +0 -0
  30. src/korean_cpc_agents/config/agents.yaml +41 -0
  31. src/korean_cpc_agents/config/tasks.yaml +126 -0
  32. src/korean_cpc_agents/crew.py +154 -0
  33. src/korean_cpc_agents/main.py +126 -0
  34. src/korean_cpc_agents/mud_game.py +1088 -0
  35. uv.lock +0 -0
  36. 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
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ ![Korean Family Portrait](assets/images/Family%20portraits%202ChatGPT%20Image%20Aug%2017,%202025,%2010_48_14%20PM.png)
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 | ![Grandma](assets/images/Ahjumma%20ChatGPT%20Image%20Mar%2026,%202025,%2005_36_15%20PM.png) |
37
+ | **Grandpa Park Chul-min** | 박철민 할아버지 | 🌸 Garden | Traditional Culture & Proverbs | ![Grandpa](assets/images/Ahjussi%20ChatGPT%20Image%20May%205,%202025,%2004_37_07%20PM.png) |
38
+ | **Sister Lee Min-ji** | 이민지 언니 | 🎵 Bedroom | K-pop & Modern Slang | ![Sister](assets/images/K-pop%20trainee%20ChatGPT%20Image%20May%205,%202025,%2004_35_54%20PM.png) |
39
+ | **Brother Jung Jae-hyun** | 정재현 오빠 | 📚 Study | Grammar & Academic Korean | ![Brother](assets/images/SunbaeOppa%20ChatGPT%20Image%20May%205,%202025,%2004_07_13%20PM.png) |
40
+ | **Teacher Choi Soo-jin** | 최수진 선생님 | ✏️ Classroom | Practical Phrases & Communication | ![Teacher](assets/images/SeonsaengnimChatGPT%20Image%20Aug%2017,%202025,%2010_41_39%20PM.png) |
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
+ ![Game Running](assets/images/app-running.png)
64
+ _Main game interface with retro terminal styling_
65
+
66
+ ![Chat with Grandmother](assets/images/chat-grandmother.png)
67
+ _Learning proper Korean honorifics with Grandma in the Kitchen_
68
+
69
+ ![Chat with Grandfather](assets/images/chat-grandfather.png)
70
+ _Traditional Korean wisdom and proverbs in the Garden_
71
+
72
+ ![Vocabulary & Inventory Tracking](assets/images/chat-words+inventory.png)
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

  • SHA256: 23a4c08ab7bfa0add816f13476a62bb56452c29e154ed23ee51a66844de605e2
  • Pointer size: 132 Bytes
  • Size of remote file: 1.61 MB
assets/images/8poegbt86i.jpg ADDED

Git LFS Details

  • SHA256: 252bb3095d815b6f4bbff2316a3fd60f56598d14b705f2f53e03fd805b452e09
  • Pointer size: 131 Bytes
  • Size of remote file: 411 kB
assets/images/Ahjumma ChatGPT Image Mar 26, 2025, 05_36_15 PM.png ADDED

Git LFS Details

  • SHA256: 849d6fba64f0917b667517e0c8723d4e55b99beee5bffb0f92fe43a0035cf7e7
  • Pointer size: 132 Bytes
  • Size of remote file: 2.82 MB
assets/images/Ahjumma and Ahjussi.png ADDED

Git LFS Details

  • SHA256: 9245866bbdccdf97eeaa32aa84077a74064f3a7f9c31a31ff9daa5470a71f3d6
  • Pointer size: 132 Bytes
  • Size of remote file: 3.35 MB
assets/images/Ahjumma loved kimchi ChatGPT Image Mar 26, 2025, 06_05_13 PM.png ADDED

Git LFS Details

  • SHA256: 10b185829922104b13e647f8230bf83ad992ce6f47362a3133fa2e9fd78c7824
  • Pointer size: 132 Bytes
  • Size of remote file: 2.93 MB
assets/images/Ahjussi ChatGPT Image May 5, 2025, 04_37_07 PM.png ADDED

Git LFS Details

  • SHA256: 5895b375513e4b0919e2ae650ae6a68b448442905bab547e9c1316701027b5ff
  • Pointer size: 132 Bytes
  • Size of remote file: 3.17 MB
assets/images/Family portrait ChatGPT Image Mar 26, 2025, 06_42_16 PM.png ADDED

Git LFS Details

  • SHA256: 543ca46d0cda565b5f6db2c8fb3c2ac956499af179ecb2652abadae6dab68d72
  • Pointer size: 132 Bytes
  • Size of remote file: 1.97 MB
assets/images/Family portraits 2ChatGPT Image Aug 17, 2025, 10_48_14 PM.png ADDED

Git LFS Details

  • SHA256: 8c59ef0e4f32d2543099fad7aaaab75d4ea7d7696d30f0625dc4181291112ae7
  • Pointer size: 132 Bytes
  • Size of remote file: 3.11 MB
assets/images/Idol ChatGPT Image Aug 17, 2025, 10_45_48 PM.png ADDED

Git LFS Details

  • SHA256: 2778f56c307516204ae4e2a4d86e0af25b19e41908726f30c42d76419ed205d5
  • Pointer size: 132 Bytes
  • Size of remote file: 2.92 MB
assets/images/K-pop trainee ChatGPT Image May 5, 2025, 04_35_54 PM.png ADDED

Git LFS Details

  • SHA256: 0bf0601d14d90aa807ad15fb15b6a4f8b9b9f2e3e15ea7d42a841d0353662f77
  • Pointer size: 132 Bytes
  • Size of remote file: 3.1 MB
assets/images/SeonsaengnimChatGPT Image Aug 17, 2025, 10_41_39 PM.png ADDED

Git LFS Details

  • SHA256: a85bbe6cee81955a653aa3fa1c27bc63ec21b8a35faf823dd3e7cfe615f74682
  • Pointer size: 132 Bytes
  • Size of remote file: 3.07 MB
assets/images/SunbaeOppa ChatGPT Image May 5, 2025, 04_07_13 PM.png ADDED

Git LFS Details

  • SHA256: a70609097ef9fe54da733f83691a25ef381616a31eb32ebd0f31c8f608a5e734
  • Pointer size: 132 Bytes
  • Size of remote file: 2.83 MB
assets/images/app-running.png ADDED

Git LFS Details

  • SHA256: 9447598246e37b41c772b99f52e5ae305e43bb50415adff8a9495e39f025d816
  • Pointer size: 132 Bytes
  • Size of remote file: 1.91 MB
assets/images/bukchon-hanok-thumb-scaled.jpg ADDED

Git LFS Details

  • SHA256: e8e8902b65d585677ce5befb38e268286e42c89ba935bc61f50ee32b438858f6
  • Pointer size: 130 Bytes
  • Size of remote file: 89.4 kB
assets/images/chat-grandfather.png ADDED

Git LFS Details

  • SHA256: f28abfa92bfbc3c7f2fcc47aba8b30509432a759f08f5bcf0d065476dc851792
  • Pointer size: 131 Bytes
  • Size of remote file: 113 kB
assets/images/chat-grandmother.png ADDED

Git LFS Details

  • SHA256: e204fcfe25aec55413241aaf4f9fe724af3790f4c5afb9a5c4c49fe9064731c8
  • Pointer size: 130 Bytes
  • Size of remote file: 94.2 kB
assets/images/chat-words+inventory.png ADDED

Git LFS Details

  • SHA256: 25a9d737eb451f7ecda36740b1a4c1aac5855a40c527dde18dba1096bc2a0ba4
  • Pointer size: 130 Bytes
  • Size of remote file: 97.8 kB
assets/images/game poster ChatGPT Image May 5, 2025, 05_02_06 PM.png ADDED

Git LFS Details

  • SHA256: cb323a2c42cc2627e7952dea731da407d215aa31031f921e71c5ac18a02b0b29
  • Pointer size: 132 Bytes
  • Size of remote file: 3.18 MB
assets/images/images.jpg ADDED

Git LFS Details

  • SHA256: d38428626bf2a22e4007acd03778970aafbf35736321698cc67778dee9d7f503
  • Pointer size: 129 Bytes
  • Size of remote file: 7.03 kB
assets/images/repeat-background.jpg ADDED

Git LFS Details

  • SHA256: 6f027338419d503c8818d1b9c3066fa34dd5f5e6a5820ad7e3a2e45d6c418167
  • Pointer size: 131 Bytes
  • Size of remote file: 113 kB
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
+ )