AK51 commited on
Commit
21abb25
·
verified ·
1 Parent(s): 63b9282

Upload 134 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +8 -0
  2. .gitattributes +108 -0
  3. .gitignore +9 -0
  4. Dockerfile +17 -0
  5. README.md +98 -10
  6. app.py +338 -0
  7. data/inventory.json +11 -0
  8. image/background/background.jpeg +3 -0
  9. image/building/car_park.jpeg +3 -0
  10. image/building/command_area.jpeg +3 -0
  11. image/building/docker.jpeg +3 -0
  12. image/building/fuel_storage_area.jpeg +3 -0
  13. image/building/gathering_area.jpeg +3 -0
  14. image/building/green_house_experimental.jpeg +3 -0
  15. image/building/laboratory.jpeg +3 -0
  16. image/building/launching_area.jpeg +3 -0
  17. image/building/living_area.jpeg +3 -0
  18. image/building/meeting_area.jpeg +3 -0
  19. image/building/mine.jpeg +3 -0
  20. image/building/mining_area.jpeg +3 -0
  21. image/building/planting_area.jpeg +3 -0
  22. image/building/recreation_area.jpeg +3 -0
  23. image/building/satellite.jpeg +3 -0
  24. image/building/sleep_area.jpeg +3 -0
  25. image/building/stand_alone_mining_unit.jpeg +3 -0
  26. image/building/stargate.jpeg +3 -0
  27. image/building_2D/arena.jpeg +3 -0
  28. image/building_2D/arena.png +3 -0
  29. image/building_2D/command_centre.jpeg +3 -0
  30. image/building_2D/command_centre.png +3 -0
  31. image/building_2D/communication_station.jpeg +3 -0
  32. image/building_2D/communication_station.png +3 -0
  33. image/building_2D/convention_centre.jpeg +3 -0
  34. image/building_2D/convention_centre.png +3 -0
  35. image/building_2D/forrest.jpeg +3 -0
  36. image/building_2D/forrest.png +3 -0
  37. image/building_2D/gas_station.jpeg +3 -0
  38. image/building_2D/gas_station.png +3 -0
  39. image/building_2D/greenhouse.jpeg +3 -0
  40. image/building_2D/greenhouse.png +3 -0
  41. image/building_2D/hangar.jpeg +3 -0
  42. image/building_2D/hangar.png +3 -0
  43. image/building_2D/hospital.jpeg +3 -0
  44. image/building_2D/hospital.png +3 -0
  45. image/building_2D/launchpad.jpeg +3 -0
  46. image/building_2D/launchpad.png +3 -0
  47. image/building_2D/living_area.jpeg +3 -0
  48. image/building_2D/living_area.png +3 -0
  49. image/building_2D/repair_shop.jpeg +3 -0
  50. image/building_2D/repair_shop.png +3 -0
.dockerignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ .env
4
+ .venv/
5
+ venv/
6
+ .git/
7
+ .gitignore
8
+ *.log
.gitattributes CHANGED
@@ -33,3 +33,111 @@ 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
+ image/background/background.jpeg filter=lfs diff=lfs merge=lfs -text
37
+ image/building_2D_interia/arena.jpeg filter=lfs diff=lfs merge=lfs -text
38
+ image/building_2D_interia/command_centre.jpeg filter=lfs diff=lfs merge=lfs -text
39
+ image/building_2D_interia/communication_station.jpeg filter=lfs diff=lfs merge=lfs -text
40
+ image/building_2D_interia/convention_centre.jpeg filter=lfs diff=lfs merge=lfs -text
41
+ image/building_2D_interia/forrest.jpeg filter=lfs diff=lfs merge=lfs -text
42
+ image/building_2D_interia/gas_station.jpeg filter=lfs diff=lfs merge=lfs -text
43
+ image/building_2D_interia/greenhouse.jpeg filter=lfs diff=lfs merge=lfs -text
44
+ image/building_2D_interia/hangar.jpeg filter=lfs diff=lfs merge=lfs -text
45
+ image/building_2D_interia/hospital.jpeg filter=lfs diff=lfs merge=lfs -text
46
+ image/building_2D_interia/launchpad.jpeg filter=lfs diff=lfs merge=lfs -text
47
+ image/building_2D_interia/living_area.jpeg filter=lfs diff=lfs merge=lfs -text
48
+ image/building_2D_interia/repair_shop.jpeg filter=lfs diff=lfs merge=lfs -text
49
+ image/building_2D_interia/robot_station.jpeg filter=lfs diff=lfs merge=lfs -text
50
+ image/building_2D_interia/solar_station.jpeg filter=lfs diff=lfs merge=lfs -text
51
+ image/building_2D_interia/weapon_shop.jpeg filter=lfs diff=lfs merge=lfs -text
52
+ image/building_2D/arena.jpeg filter=lfs diff=lfs merge=lfs -text
53
+ image/building_2D/arena.png filter=lfs diff=lfs merge=lfs -text
54
+ image/building_2D/command_centre.jpeg filter=lfs diff=lfs merge=lfs -text
55
+ image/building_2D/command_centre.png filter=lfs diff=lfs merge=lfs -text
56
+ image/building_2D/communication_station.jpeg filter=lfs diff=lfs merge=lfs -text
57
+ image/building_2D/communication_station.png filter=lfs diff=lfs merge=lfs -text
58
+ image/building_2D/convention_centre.jpeg filter=lfs diff=lfs merge=lfs -text
59
+ image/building_2D/convention_centre.png filter=lfs diff=lfs merge=lfs -text
60
+ image/building_2D/forrest.jpeg filter=lfs diff=lfs merge=lfs -text
61
+ image/building_2D/forrest.png filter=lfs diff=lfs merge=lfs -text
62
+ image/building_2D/gas_station.jpeg filter=lfs diff=lfs merge=lfs -text
63
+ image/building_2D/gas_station.png filter=lfs diff=lfs merge=lfs -text
64
+ image/building_2D/greenhouse.jpeg filter=lfs diff=lfs merge=lfs -text
65
+ image/building_2D/greenhouse.png filter=lfs diff=lfs merge=lfs -text
66
+ image/building_2D/hangar.jpeg filter=lfs diff=lfs merge=lfs -text
67
+ image/building_2D/hangar.png filter=lfs diff=lfs merge=lfs -text
68
+ image/building_2D/hospital.jpeg filter=lfs diff=lfs merge=lfs -text
69
+ image/building_2D/hospital.png filter=lfs diff=lfs merge=lfs -text
70
+ image/building_2D/launchpad.jpeg filter=lfs diff=lfs merge=lfs -text
71
+ image/building_2D/launchpad.png filter=lfs diff=lfs merge=lfs -text
72
+ image/building_2D/living_area.jpeg filter=lfs diff=lfs merge=lfs -text
73
+ image/building_2D/living_area.png filter=lfs diff=lfs merge=lfs -text
74
+ image/building_2D/repair_shop.jpeg filter=lfs diff=lfs merge=lfs -text
75
+ image/building_2D/repair_shop.png filter=lfs diff=lfs merge=lfs -text
76
+ image/building_2D/robot_station.jpeg filter=lfs diff=lfs merge=lfs -text
77
+ image/building_2D/robot_station.png filter=lfs diff=lfs merge=lfs -text
78
+ image/building_2D/solar_station.jpeg filter=lfs diff=lfs merge=lfs -text
79
+ image/building_2D/solar_station.png filter=lfs diff=lfs merge=lfs -text
80
+ image/building_2D/weapon_shop.jpeg filter=lfs diff=lfs merge=lfs -text
81
+ image/building_2D/weapon_shop.png filter=lfs diff=lfs merge=lfs -text
82
+ image/building/car_park.jpeg filter=lfs diff=lfs merge=lfs -text
83
+ image/building/command_area.jpeg filter=lfs diff=lfs merge=lfs -text
84
+ image/building/docker.jpeg filter=lfs diff=lfs merge=lfs -text
85
+ image/building/fuel_storage_area.jpeg filter=lfs diff=lfs merge=lfs -text
86
+ image/building/gathering_area.jpeg filter=lfs diff=lfs merge=lfs -text
87
+ image/building/green_house_experimental.jpeg filter=lfs diff=lfs merge=lfs -text
88
+ image/building/laboratory.jpeg filter=lfs diff=lfs merge=lfs -text
89
+ image/building/launching_area.jpeg filter=lfs diff=lfs merge=lfs -text
90
+ image/building/living_area.jpeg filter=lfs diff=lfs merge=lfs -text
91
+ image/building/meeting_area.jpeg filter=lfs diff=lfs merge=lfs -text
92
+ image/building/mine.jpeg filter=lfs diff=lfs merge=lfs -text
93
+ image/building/mining_area.jpeg filter=lfs diff=lfs merge=lfs -text
94
+ image/building/planting_area.jpeg filter=lfs diff=lfs merge=lfs -text
95
+ image/building/recreation_area.jpeg filter=lfs diff=lfs merge=lfs -text
96
+ image/building/satellite.jpeg filter=lfs diff=lfs merge=lfs -text
97
+ image/building/sleep_area.jpeg filter=lfs diff=lfs merge=lfs -text
98
+ image/building/stand_alone_mining_unit.jpeg filter=lfs diff=lfs merge=lfs -text
99
+ image/building/stargate.jpeg filter=lfs diff=lfs merge=lfs -text
100
+ image/character/cleaner_full.png filter=lfs diff=lfs merge=lfs -text
101
+ image/character/cleaner.jpeg filter=lfs diff=lfs merge=lfs -text
102
+ image/character/cleaner.png filter=lfs diff=lfs merge=lfs -text
103
+ image/character/communicator_full.png filter=lfs diff=lfs merge=lfs -text
104
+ image/character/communicator.jpeg filter=lfs diff=lfs merge=lfs -text
105
+ image/character/communicator.png filter=lfs diff=lfs merge=lfs -text
106
+ image/character/courier_full.png filter=lfs diff=lfs merge=lfs -text
107
+ image/character/courier.jpeg filter=lfs diff=lfs merge=lfs -text
108
+ image/character/courier.png filter=lfs diff=lfs merge=lfs -text
109
+ image/character/farmer_full.png filter=lfs diff=lfs merge=lfs -text
110
+ image/character/farmer.jpeg filter=lfs diff=lfs merge=lfs -text
111
+ image/character/farmer.png filter=lfs diff=lfs merge=lfs -text
112
+ image/character/humanoid_robot_full.png filter=lfs diff=lfs merge=lfs -text
113
+ image/character/humanoid_robot.jpeg filter=lfs diff=lfs merge=lfs -text
114
+ image/character/humanoid_robot.png filter=lfs diff=lfs merge=lfs -text
115
+ image/character/Main_female_character_full.png filter=lfs diff=lfs merge=lfs -text
116
+ image/character/Main_female_character.jpeg filter=lfs diff=lfs merge=lfs -text
117
+ image/character/Main_female_character.png filter=lfs diff=lfs merge=lfs -text
118
+ image/character/Main_male_character_full.png filter=lfs diff=lfs merge=lfs -text
119
+ image/character/Main_male_character.jpeg filter=lfs diff=lfs merge=lfs -text
120
+ image/character/Main_male_character.png filter=lfs diff=lfs merge=lfs -text
121
+ image/character/mechanic_full.png filter=lfs diff=lfs merge=lfs -text
122
+ image/character/mechanic.jpeg filter=lfs diff=lfs merge=lfs -text
123
+ image/character/mechanic.png filter=lfs diff=lfs merge=lfs -text
124
+ image/character/medic_full.png filter=lfs diff=lfs merge=lfs -text
125
+ image/character/medic.jpeg filter=lfs diff=lfs merge=lfs -text
126
+ image/character/medic.png filter=lfs diff=lfs merge=lfs -text
127
+ image/character/navigator_full.png filter=lfs diff=lfs merge=lfs -text
128
+ image/character/navigator.jpeg filter=lfs diff=lfs merge=lfs -text
129
+ image/character/navigator.png filter=lfs diff=lfs merge=lfs -text
130
+ image/character/pilot_full.png filter=lfs diff=lfs merge=lfs -text
131
+ image/character/pilot.jpeg filter=lfs diff=lfs merge=lfs -text
132
+ image/character/pilot.png filter=lfs diff=lfs merge=lfs -text
133
+ image/character/wheel_robot_full.png filter=lfs diff=lfs merge=lfs -text
134
+ image/character/wheel_robot.jpeg filter=lfs diff=lfs merge=lfs -text
135
+ image/character/wheel_robot.png filter=lfs diff=lfs merge=lfs -text
136
+ image/interia/isolated_automation_area.jpeg filter=lfs diff=lfs merge=lfs -text
137
+ image/interia/laboratory_place.jpeg filter=lfs diff=lfs merge=lfs -text
138
+ image/interia/sleeping_place.jpeg filter=lfs diff=lfs merge=lfs -text
139
+ image/mission/huge_automated_cargo_truck.jpeg filter=lfs diff=lfs merge=lfs -text
140
+ image/mission/huge_mobile_truck.jpeg filter=lfs diff=lfs merge=lfs -text
141
+ image/mission/huge_robot.jpeg filter=lfs diff=lfs merge=lfs -text
142
+ image/mission/huge_rocket.jpeg filter=lfs diff=lfs merge=lfs -text
143
+ image/mission/space_station.jpeg filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ .env
4
+ .venv/
5
+ venv/
6
+ .DS_Store
7
+ *.log
8
+ deepseek_key.txt
9
+ memory/
Dockerfile ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Hugging Face Spaces (Docker SDK) - Lunar Village RPG
2
+ FROM python:3.11-slim
3
+
4
+ ENV PYTHONUNBUFFERED=1 \
5
+ PIP_NO_CACHE_DIR=1 \
6
+ PORT=7860
7
+
8
+ WORKDIR /app
9
+
10
+ COPY requirements.txt .
11
+ RUN pip install --no-cache-dir -r requirements.txt
12
+
13
+ COPY . .
14
+
15
+ EXPOSE 7860
16
+
17
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -1,10 +1,98 @@
1
- ---
2
- title: MoonVillage
3
- emoji: 🐨
4
- colorFrom: purple
5
- colorTo: green
6
- sdk: static
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Lunar Village
3
+ emoji: 🌕
4
+ colorFrom: indigo
5
+ colorTo: yellow
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ license: mit
10
+ ---
11
+
12
+ # 🌕 Lunar Village — Moon Colony RPG
13
+
14
+ A **SmallVill-style** online RPG set on the Moon. Walk around a lunar colony,
15
+ chat with AI-driven crew members (powered by **DeepSeek**), gather resources,
16
+ and complete the colony's mega-projects: a **huge robot**, a **mobile truck**,
17
+ an **automated cargo hauler**, a **heavy rocket**, and an **orbital space station**.
18
+
19
+ Playable on desktop and **mobile** (touch joystick + tap-to-interact).
20
+
21
+ **Created by Andy Kong**
22
+
23
+ ---
24
+
25
+ ## 🎮 How to play
26
+
27
+ - **Move:** on-screen joystick (mobile) or `WASD` / arrow keys (desktop).
28
+ - **Interact:** walk up to a crew member and tap the 💬 button (or press `E` / `Space`) to talk.
29
+ - **Gather:** walk up to a building like the Mine or Greenhouse and tap the ⛏️ button to collect resources.
30
+ - **Build:** open the 🏗️ **Missions** panel to spend resources and construct the mega-projects.
31
+
32
+ ## 🔐 DeepSeek API key (important)
33
+
34
+ The game talks to DeepSeek through a **server-side proxy** (`/api/chat`) so your
35
+ API key is never exposed to players. The key is read from an environment
36
+ variable and should be stored as a **Space Secret**, never committed to code.
37
+
38
+ On Hugging Face:
39
+ 1. Open your Space → **Settings** → **Variables and secrets**.
40
+ 2. Add a **secret** named `DEEPSEEK_API_KEY` with your key value.
41
+ 3. (Optional) add `DEEPSEEK_MODEL` (default `deepseek-chat`) and
42
+ `DEEPSEEK_BASE_URL` (default `https://api.deepseek.com`).
43
+
44
+ > ⚠️ If you shared your key anywhere public (chat, screenshots, commits),
45
+ > regenerate it in the DeepSeek console before deploying.
46
+
47
+ If no key is set, the game still runs using built-in offline personality
48
+ replies, so you can test the whole experience without any credentials.
49
+
50
+ ## 🚀 Deploy to Hugging Face Spaces
51
+
52
+ 1. Create a new **Space** → SDK: **Docker**.
53
+ 2. Upload this whole folder (including `app.py`, `Dockerfile`, `requirements.txt`,
54
+ `static/`, and `image/`).
55
+ 3. Add the `DEEPSEEK_API_KEY` secret (see above).
56
+ 4. The Space builds the Docker image and serves the game on port `7860`.
57
+
58
+ ## 🖥️ Run locally
59
+
60
+ ```bash
61
+ pip install -r requirements.txt
62
+ # optional: enable AI
63
+ set DEEPSEEK_API_KEY=your_key_here # Windows (cmd)
64
+ # $env:DEEPSEEK_API_KEY="your_key_here" # Windows (PowerShell)
65
+ # export DEEPSEEK_API_KEY=your_key_here # macOS/Linux
66
+ python app.py
67
+ ```
68
+
69
+ Then open http://localhost:7860
70
+
71
+ ## 📁 Project structure
72
+
73
+ ```
74
+ Kiro_RPG/
75
+ ├─ app.py # FastAPI backend + DeepSeek proxy
76
+ ├─ requirements.txt
77
+ ├─ Dockerfile # Hugging Face Docker Space
78
+ ├─ image/ # game art (buildings, characters, missions)
79
+ └─ static/
80
+ ├─ index.html
81
+ ├─ css/style.css
82
+ └─ js/
83
+ ├─ config.js # world / buildings / characters / missions data
84
+ ├─ ai.js # AI chat client
85
+ ├─ ui.js # HUD, dialog, missions
86
+ ├─ game.js # canvas engine, player, NPCs
87
+ └─ boot.js # title screen
88
+ ```
89
+
90
+ ## 🧩 Tech
91
+
92
+ - **Backend:** FastAPI + httpx (async DeepSeek proxy)
93
+ - **Frontend:** vanilla HTML5 Canvas + JS (no build step, loads fast on mobile)
94
+ - **AI:** DeepSeek Chat (OpenAI-compatible API)
95
+
96
+ ---
97
+
98
+ Created by **Andy Kong**.
app.py ADDED
@@ -0,0 +1,338 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Lunar Village - a SmallVill-style online RPG set on the Moon.
3
+ FastAPI backend: serves the static game and proxies AI chat to DeepSeek.
4
+
5
+ The DeepSeek API key is read from the DEEPSEEK_API_KEY environment variable
6
+ (set it as a Secret in your Hugging Face Space). It is NEVER sent to the client.
7
+
8
+ Created by Andy Kong.
9
+ """
10
+ import os
11
+ import re
12
+ import json
13
+ import logging
14
+ from datetime import datetime, timezone
15
+ from typing import List, Optional
16
+
17
+ import httpx
18
+ from fastapi import FastAPI, HTTPException
19
+ from fastapi.middleware.cors import CORSMiddleware
20
+ from fastapi.staticfiles import StaticFiles
21
+ from fastapi.responses import FileResponse, JSONResponse
22
+ from pydantic import BaseModel, Field
23
+
24
+ logging.basicConfig(level=logging.INFO)
25
+ logger = logging.getLogger("lunar-village")
26
+
27
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
28
+ STATIC_DIR = os.path.join(BASE_DIR, "static")
29
+ IMAGE_DIR = os.path.join(BASE_DIR, "image")
30
+ MEMORY_DIR = os.path.join(BASE_DIR, "memory")
31
+ os.makedirs(MEMORY_DIR, exist_ok=True)
32
+
33
+ DATA_DIR = os.path.join(BASE_DIR, "data")
34
+ os.makedirs(DATA_DIR, exist_ok=True)
35
+ INVENTORY_PATH = os.path.join(DATA_DIR, "inventory.json")
36
+
37
+
38
+ def load_inventory() -> dict:
39
+ if os.path.isfile(INVENTORY_PATH):
40
+ try:
41
+ with open(INVENTORY_PATH, "r", encoding="utf-8") as f:
42
+ return json.load(f)
43
+ except Exception: # noqa: BLE001
44
+ pass
45
+ return {"resources": {}, "updated": None}
46
+
47
+
48
+ def save_inventory(resources: dict) -> dict:
49
+ data = {
50
+ "resources": {k: float(v) for k, v in resources.items()},
51
+ "updated": datetime.now(timezone.utc).isoformat(timespec="seconds"),
52
+ }
53
+ with open(INVENTORY_PATH, "w", encoding="utf-8") as f:
54
+ json.dump(data, f, ensure_ascii=False, indent=2)
55
+ return data
56
+
57
+ MAX_MEMORY_ENTRIES = 60 # keep the most recent N per character
58
+ MEMORY_IN_PROMPT = 10 # how many recent memories to feed the AI
59
+
60
+
61
+ def _safe_id(cid: str) -> str:
62
+ return re.sub(r"[^a-zA-Z0-9_-]", "", cid or "")[:64]
63
+
64
+
65
+ def _mem_path(cid: str) -> str:
66
+ return os.path.join(MEMORY_DIR, f"{_safe_id(cid)}.json")
67
+
68
+
69
+ def load_memory(cid: str) -> dict:
70
+ path = _mem_path(cid)
71
+ if os.path.isfile(path):
72
+ try:
73
+ with open(path, "r", encoding="utf-8") as f:
74
+ return json.load(f)
75
+ except Exception: # noqa: BLE001
76
+ pass
77
+ return {"id": _safe_id(cid), "name": "", "entries": []}
78
+
79
+
80
+ def save_memory(cid: str, data: dict) -> None:
81
+ try:
82
+ with open(_mem_path(cid), "w", encoding="utf-8") as f:
83
+ json.dump(data, f, ensure_ascii=False, indent=2)
84
+ except Exception as exc: # noqa: BLE001
85
+ logger.warning("Could not save memory for %s: %s", cid, exc)
86
+
87
+
88
+ def append_memory(cid: str, text: str, name: str = "") -> dict:
89
+ data = load_memory(cid)
90
+ if name and not data.get("name"):
91
+ data["name"] = name
92
+ data.setdefault("entries", []).append({
93
+ "t": datetime.now(timezone.utc).isoformat(timespec="seconds"),
94
+ "text": text.strip(),
95
+ })
96
+ data["entries"] = data["entries"][-MAX_MEMORY_ENTRIES:]
97
+ save_memory(cid, data)
98
+ return data
99
+
100
+
101
+ def memory_prompt(cid: str) -> str:
102
+ data = load_memory(cid)
103
+ entries = data.get("entries", [])[-MEMORY_IN_PROMPT:]
104
+ if not entries:
105
+ return ""
106
+ lines = "\n".join(f"- {e['text']}" for e in entries)
107
+ return (
108
+ "\n\nYour memories so far (oldest first, act consistently with them, "
109
+ "and follow any standing orders from the Commander):\n" + lines
110
+ )
111
+
112
+ DEEPSEEK_API_KEY = os.environ.get("DEEPSEEK_API_KEY", "").strip()
113
+ # Also try loading from a local key file (not uploaded to Hugging Face)
114
+ _KEY_FILE = os.path.join(BASE_DIR, "deepseek_key.txt")
115
+ if not DEEPSEEK_API_KEY and os.path.isfile(_KEY_FILE):
116
+ with open(_KEY_FILE, "r", encoding="utf-8") as _f:
117
+ DEEPSEEK_API_KEY = _f.read().strip()
118
+
119
+ DEEPSEEK_BASE_URL = os.environ.get("DEEPSEEK_BASE_URL", "https://api.deepseek.com").rstrip("/")
120
+ DEEPSEEK_MODEL = os.environ.get("DEEPSEEK_MODEL", "deepseek-chat")
121
+
122
+ # Allow a user-provided key at runtime (set via /api/set-key)
123
+ _USER_KEY = {"key": ""}
124
+
125
+ app = FastAPI(title="Lunar Village", version="1.0.0")
126
+
127
+ app.add_middleware(
128
+ CORSMiddleware,
129
+ allow_origins=["*"],
130
+ allow_methods=["*"],
131
+ allow_headers=["*"],
132
+ )
133
+
134
+
135
+ class ChatMessage(BaseModel):
136
+ role: str
137
+ content: str
138
+
139
+
140
+ class ChatRequest(BaseModel):
141
+ system: str = Field(default="", description="System / persona prompt for the NPC.")
142
+ messages: List[ChatMessage] = Field(default_factory=list)
143
+ temperature: float = 1.0
144
+ max_tokens: int = 220
145
+ character_id: Optional[str] = None # if set, that character's memory is injected
146
+ character_name: Optional[str] = None
147
+
148
+
149
+ class MemoryEntryRequest(BaseModel):
150
+ text: str
151
+ name: Optional[str] = None
152
+
153
+
154
+ class CharacterSeed(BaseModel):
155
+ id: str
156
+ name: Optional[str] = None
157
+ persona: Optional[str] = None
158
+
159
+
160
+ class MemoryInitRequest(BaseModel):
161
+ characters: List[CharacterSeed] = Field(default_factory=list)
162
+
163
+
164
+ class InventoryRequest(BaseModel):
165
+ resources: dict = Field(default_factory=dict)
166
+
167
+
168
+ class ChatResponse(BaseModel):
169
+ reply: str
170
+ fallback: bool = False
171
+
172
+
173
+ # A small offline personality-flavoured fallback so the game is still playable
174
+ # without an API key (e.g. local testing or if DeepSeek is unreachable).
175
+ FALLBACK_LINES = {
176
+ "farmer": "The greenhouse crops are holding up, but I need more water reclaimed from the ice mine.",
177
+ "mechanic": "Give me spare parts and I'll keep the rovers running. That huge truck won't build itself.",
178
+ "medic": "Radiation exposure is the real enemy up here. Stay inside during solar flares.",
179
+ "pilot": "The launch window is tight. We need fuel stabilised before the rocket can fly.",
180
+ "navigator": "I've been charting safe routes across the mare. Fewer craters, fewer breakdowns.",
181
+ "communicator": "Signal to Earth has a two-second delay. Patience, commander.",
182
+ "courier": "I move supplies between the domes. Tell me where you need them.",
183
+ "cleaner": "Moon dust gets everywhere. It clogs the airlocks if I don't keep at it.",
184
+ "humanoid_robot": "Directive acknowledged. Awaiting task assignment, commander.",
185
+ "wheel_robot": "Beep. Rolling out to the mining area for another load of ore.",
186
+ "default": "Welcome to the colony, commander. There's plenty of work to keep us all alive up here.",
187
+ }
188
+
189
+
190
+ def build_fallback(system: str, messages: List[ChatMessage]) -> str:
191
+ text = (system + " " + " ".join(m.content for m in messages)).lower()
192
+ for key, line in FALLBACK_LINES.items():
193
+ if key != "default" and key in text:
194
+ return line
195
+ return FALLBACK_LINES["default"]
196
+
197
+
198
+ @app.get("/api/health")
199
+ async def health():
200
+ active_key = _USER_KEY["key"] or DEEPSEEK_API_KEY
201
+ return {"ok": True, "ai_enabled": bool(active_key)}
202
+
203
+
204
+ class SetKeyRequest(BaseModel):
205
+ key: str = ""
206
+
207
+
208
+ @app.post("/api/set-key")
209
+ async def set_key(req: SetKeyRequest):
210
+ """Allow a user to provide their own DeepSeek API key for this session."""
211
+ _USER_KEY["key"] = req.key.strip()
212
+ return {"ok": True, "ai_enabled": bool(_USER_KEY["key"] or DEEPSEEK_API_KEY)}
213
+
214
+
215
+ @app.post("/api/memory/init")
216
+ async def memory_init(req: MemoryInitRequest):
217
+ """Ensure a memory file exists for every character (called once on game start)."""
218
+ created = []
219
+ for c in req.characters:
220
+ cid = _safe_id(c.id)
221
+ if not cid:
222
+ continue
223
+ if not os.path.isfile(_mem_path(cid)):
224
+ seed = {"id": cid, "name": c.name or "", "entries": []}
225
+ if c.persona:
226
+ seed["entries"].append({
227
+ "t": datetime.now(timezone.utc).isoformat(timespec="seconds"),
228
+ "text": f"I am {c.name or cid}, {c.persona}",
229
+ })
230
+ save_memory(cid, seed)
231
+ created.append(cid)
232
+ return {"ok": True, "created": created, "total": len(req.characters)}
233
+
234
+
235
+ @app.get("/api/memory")
236
+ async def memory_list():
237
+ ids = [f[:-5] for f in os.listdir(MEMORY_DIR) if f.endswith(".json")]
238
+ return {"characters": sorted(ids)}
239
+
240
+
241
+ @app.get("/api/memory/{cid}")
242
+ async def memory_get(cid: str):
243
+ return load_memory(cid)
244
+
245
+
246
+ @app.post("/api/memory/{cid}")
247
+ async def memory_post(cid: str, req: MemoryEntryRequest):
248
+ if not req.text.strip():
249
+ raise HTTPException(status_code=400, detail="empty memory text")
250
+ data = append_memory(cid, req.text, req.name or "")
251
+ return {"ok": True, "count": len(data.get("entries", []))}
252
+
253
+
254
+ @app.get("/api/inventory")
255
+ async def inventory_get():
256
+ """Return the colony's accumulated resources (persisted to data/inventory.json)."""
257
+ return load_inventory()
258
+
259
+
260
+ @app.post("/api/inventory")
261
+ async def inventory_post(req: InventoryRequest):
262
+ data = save_inventory(req.resources)
263
+ return {"ok": True, **data}
264
+
265
+
266
+ @app.post("/api/chat", response_model=ChatResponse)
267
+ async def chat(req: ChatRequest):
268
+ # Build the system prompt, injecting this character's memory if provided.
269
+ system = req.system
270
+ if req.character_id:
271
+ system += memory_prompt(req.character_id)
272
+
273
+ # Determine the active API key (user-provided takes priority)
274
+ active_key = _USER_KEY["key"] or DEEPSEEK_API_KEY
275
+
276
+ # No key configured -> graceful offline fallback so the game still works.
277
+ if not active_key:
278
+ return ChatResponse(reply=build_fallback(req.system, req.messages), fallback=True)
279
+
280
+ payload_messages = []
281
+ if system:
282
+ payload_messages.append({"role": "system", "content": system})
283
+ for m in req.messages:
284
+ payload_messages.append({"role": m.role, "content": m.content})
285
+
286
+ payload = {
287
+ "model": DEEPSEEK_MODEL,
288
+ "messages": payload_messages,
289
+ "temperature": req.temperature,
290
+ "max_tokens": req.max_tokens,
291
+ "stream": False,
292
+ }
293
+ headers = {
294
+ "Authorization": f"Bearer {active_key}",
295
+ "Content-Type": "application/json",
296
+ }
297
+
298
+ try:
299
+ async with httpx.AsyncClient(timeout=30.0) as client:
300
+ resp = await client.post(
301
+ f"{DEEPSEEK_BASE_URL}/chat/completions",
302
+ json=payload,
303
+ headers=headers,
304
+ )
305
+ if resp.status_code != 200:
306
+ logger.warning("DeepSeek error %s: %s", resp.status_code, resp.text[:300])
307
+ return ChatResponse(reply=build_fallback(req.system, req.messages), fallback=True)
308
+ data = resp.json()
309
+ reply = data["choices"][0]["message"]["content"].strip()
310
+ return ChatResponse(reply=reply, fallback=False)
311
+ except Exception as exc: # noqa: BLE001
312
+ logger.exception("DeepSeek request failed: %s", exc)
313
+ return ChatResponse(reply=build_fallback(req.system, req.messages), fallback=True)
314
+
315
+
316
+ # Serve game images and static assets.
317
+ if os.path.isdir(IMAGE_DIR):
318
+ app.mount("/image", StaticFiles(directory=IMAGE_DIR), name="image")
319
+
320
+
321
+ @app.get("/")
322
+ async def index():
323
+ index_path = os.path.join(STATIC_DIR, "index.html")
324
+ if os.path.isfile(index_path):
325
+ # never cache the HTML so versioned asset URLs are always picked up
326
+ return FileResponse(index_path, headers={"Cache-Control": "no-cache, no-store, must-revalidate"})
327
+ return JSONResponse({"error": "index.html not found"}, status_code=404)
328
+
329
+
330
+ if os.path.isdir(STATIC_DIR):
331
+ app.mount("/", StaticFiles(directory=STATIC_DIR, html=True), name="static")
332
+
333
+
334
+ if __name__ == "__main__":
335
+ import uvicorn
336
+
337
+ port = int(os.environ.get("PORT", "7860"))
338
+ uvicorn.run("app:app", host="0.0.0.0", port=port, reload=False)
data/inventory.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "resources": {
3
+ "ore": 153.90000000000174,
4
+ "metal": 14.600000000000168,
5
+ "fuel": 177.35000000000232,
6
+ "food": 0.2,
7
+ "energy": 272.90000000000657,
8
+ "parts": 175.2999999999926
9
+ },
10
+ "updated": "2026-07-12T14:21:08+00:00"
11
+ }
image/background/background.jpeg ADDED

Git LFS Details

  • SHA256: 8718ae2c02852aede8ddf6e18fd979ada4e9a01ef0e847b735d59f2393692a2e
  • Pointer size: 131 Bytes
  • Size of remote file: 139 kB
image/building/car_park.jpeg ADDED

Git LFS Details

  • SHA256: 2ac69f15d8f31713bf2f9bddf2a0bfc18c103afb3981804db0804ba3b56844a4
  • Pointer size: 131 Bytes
  • Size of remote file: 398 kB
image/building/command_area.jpeg ADDED

Git LFS Details

  • SHA256: 74af7fe21034cf1e2bbda972a3e146d05acf506f76cde852e224f91b0cce526e
  • Pointer size: 131 Bytes
  • Size of remote file: 262 kB
image/building/docker.jpeg ADDED

Git LFS Details

  • SHA256: ed3b65ee7fd6542df2b5847bd74a462793822542e31a5e8f94ff168122b562dd
  • Pointer size: 131 Bytes
  • Size of remote file: 249 kB
image/building/fuel_storage_area.jpeg ADDED

Git LFS Details

  • SHA256: 39ca1f5623c298441f34e0af5ef8e2507a64b96ac4dee03f995cc4cd1c5447c0
  • Pointer size: 131 Bytes
  • Size of remote file: 251 kB
image/building/gathering_area.jpeg ADDED

Git LFS Details

  • SHA256: cf28b0f36caac1db5accd09c8f0aebd9479dddc6c670e0b7cc22e008d41e3d8f
  • Pointer size: 131 Bytes
  • Size of remote file: 347 kB
image/building/green_house_experimental.jpeg ADDED

Git LFS Details

  • SHA256: b28ea321a89c7874ebf6692de1a7c9edbaaea985bfe6eca6e6caa3a453c64c1f
  • Pointer size: 131 Bytes
  • Size of remote file: 419 kB
image/building/laboratory.jpeg ADDED

Git LFS Details

  • SHA256: 84c655ce945672861c34058e24e1d70370a73bd7b302f44cfb50be33d2ff5fa5
  • Pointer size: 131 Bytes
  • Size of remote file: 233 kB
image/building/launching_area.jpeg ADDED

Git LFS Details

  • SHA256: ccac701ca02310fea0b3e0cbe2aa67fe5fc87f0a36204054d09085f2741b614f
  • Pointer size: 131 Bytes
  • Size of remote file: 277 kB
image/building/living_area.jpeg ADDED

Git LFS Details

  • SHA256: eba23050bb67b5cab6982180fc087737a4d7bc342474c71bf8a3b1ae6304bd44
  • Pointer size: 131 Bytes
  • Size of remote file: 400 kB
image/building/meeting_area.jpeg ADDED

Git LFS Details

  • SHA256: 11fe175fda9bf35d35e755a7aba31251f011d5d3226c05879930b3eb8778150e
  • Pointer size: 131 Bytes
  • Size of remote file: 281 kB
image/building/mine.jpeg ADDED

Git LFS Details

  • SHA256: e3a8b7d3b8e6d8e77d10093217d9843067a472ed34aa9d141e953b1e5c8f5b1f
  • Pointer size: 131 Bytes
  • Size of remote file: 270 kB
image/building/mining_area.jpeg ADDED

Git LFS Details

  • SHA256: dcbf14333e8acf92b4a60f9d46517d19038512a3c4842ff77a734380489b5843
  • Pointer size: 131 Bytes
  • Size of remote file: 406 kB
image/building/planting_area.jpeg ADDED

Git LFS Details

  • SHA256: a05b4ec72fd6d676413bf4d3bb72d76692361edca77af10086c235336c6d9b59
  • Pointer size: 131 Bytes
  • Size of remote file: 330 kB
image/building/recreation_area.jpeg ADDED

Git LFS Details

  • SHA256: e56003bfe11200dfb05a88164b907c046bdd01c214934d9077502ef378a5043a
  • Pointer size: 131 Bytes
  • Size of remote file: 381 kB
image/building/satellite.jpeg ADDED

Git LFS Details

  • SHA256: 066198660d8f534792e7e5ca09d65b179f39465fe7fc5c80648d362baca2b0f2
  • Pointer size: 131 Bytes
  • Size of remote file: 211 kB
image/building/sleep_area.jpeg ADDED

Git LFS Details

  • SHA256: f23f82832f76bf9f78f422bb4fa61063ede8da1022cd96a696738d3b3623381d
  • Pointer size: 131 Bytes
  • Size of remote file: 387 kB
image/building/stand_alone_mining_unit.jpeg ADDED

Git LFS Details

  • SHA256: f9a3d51b8ee491281e8dda91ea0e1511ddba06a98ad8eecef522804ff6adef01
  • Pointer size: 131 Bytes
  • Size of remote file: 259 kB
image/building/stargate.jpeg ADDED

Git LFS Details

  • SHA256: 116ff80630536e3b6ec6bc6d29d110898c2e37bac9668424ff643f3611adceb2
  • Pointer size: 131 Bytes
  • Size of remote file: 196 kB
image/building_2D/arena.jpeg ADDED

Git LFS Details

  • SHA256: 4630c9c3b4e229f6a6db104ad3f0352eac4a7348634f2e61353647627878f020
  • Pointer size: 131 Bytes
  • Size of remote file: 318 kB
image/building_2D/arena.png ADDED

Git LFS Details

  • SHA256: f78241fd01629b58ac10bae678865adae32b0302817e956123d84e6ae5e8b286
  • Pointer size: 132 Bytes
  • Size of remote file: 1.14 MB
image/building_2D/command_centre.jpeg ADDED

Git LFS Details

  • SHA256: 277ea39518e7286eafeac7ac9cbeac699b387c1da266925503c17d18b9d149cc
  • Pointer size: 131 Bytes
  • Size of remote file: 302 kB
image/building_2D/command_centre.png ADDED

Git LFS Details

  • SHA256: af14e4f271d652128b74b6743affe637ba7487f04d0f82a828162658b3f98f02
  • Pointer size: 131 Bytes
  • Size of remote file: 978 kB
image/building_2D/communication_station.jpeg ADDED

Git LFS Details

  • SHA256: 70ea3f2c983bcd259f4f7e35e150458464906b61e1ec6cc19e760c026d183946
  • Pointer size: 131 Bytes
  • Size of remote file: 239 kB
image/building_2D/communication_station.png ADDED

Git LFS Details

  • SHA256: 5eb7c34570326e1ef580db757216fa7627b23ca57b61e3674899e98a749da256
  • Pointer size: 131 Bytes
  • Size of remote file: 619 kB
image/building_2D/convention_centre.jpeg ADDED

Git LFS Details

  • SHA256: 5f2c293529735afa647cf08c6d75b21fbc62894f1e22ec1e8cd86534fac611d8
  • Pointer size: 131 Bytes
  • Size of remote file: 317 kB
image/building_2D/convention_centre.png ADDED

Git LFS Details

  • SHA256: 8a455e578e0bcf577b02c9121d67455daab95eac0bc006f37d28d3c2779c04f6
  • Pointer size: 132 Bytes
  • Size of remote file: 1.29 MB
image/building_2D/forrest.jpeg ADDED

Git LFS Details

  • SHA256: 906ead1cb4b4f4561ccbfa4eaff60c2f848850fe031cd878754e12faf00b36fc
  • Pointer size: 131 Bytes
  • Size of remote file: 297 kB
image/building_2D/forrest.png ADDED

Git LFS Details

  • SHA256: 1b741340d46032835c8b09be4bda203cc74eb8c245a9c45ff5dab13eb2c7a2cd
  • Pointer size: 132 Bytes
  • Size of remote file: 1.05 MB
image/building_2D/gas_station.jpeg ADDED

Git LFS Details

  • SHA256: eec02b247aec7f48278f9f18828030ebc4181f216943ce1b7b7ab68105d34533
  • Pointer size: 131 Bytes
  • Size of remote file: 244 kB
image/building_2D/gas_station.png ADDED

Git LFS Details

  • SHA256: 840150eb1c75672c38720e3d3bb65c8b2261e54e61a0828bbcbf9aefdbc8aba4
  • Pointer size: 132 Bytes
  • Size of remote file: 1.19 MB
image/building_2D/greenhouse.jpeg ADDED

Git LFS Details

  • SHA256: fa0ac9836b8f85c9a7cf2eb75d9be66274c0aba484f5420df0db12173a9d9100
  • Pointer size: 131 Bytes
  • Size of remote file: 316 kB
image/building_2D/greenhouse.png ADDED

Git LFS Details

  • SHA256: b4b82ff68262ed7803afbe7b76eedf5a003a7240cddd900747bedb0db0d2505f
  • Pointer size: 132 Bytes
  • Size of remote file: 1.38 MB
image/building_2D/hangar.jpeg ADDED

Git LFS Details

  • SHA256: 4f3c917f59731e4cb4ec778196c60e982f504bdd6bd7de8650ae22836bd94876
  • Pointer size: 131 Bytes
  • Size of remote file: 301 kB
image/building_2D/hangar.png ADDED

Git LFS Details

  • SHA256: 1bf7555f3fffb3d4c254b0ba767474e4e9fbc02c1e2e93d6fcf587fe25dc246a
  • Pointer size: 132 Bytes
  • Size of remote file: 1.05 MB
image/building_2D/hospital.jpeg ADDED

Git LFS Details

  • SHA256: df951ab4270b4f7bf37b5c7034a8c8d49226d35b94da6440953377f0f5404638
  • Pointer size: 131 Bytes
  • Size of remote file: 304 kB
image/building_2D/hospital.png ADDED

Git LFS Details

  • SHA256: 66a83f09c5a3e292678606c74a26254571770bde96c574aafb545f5ff287aeeb
  • Pointer size: 132 Bytes
  • Size of remote file: 1.16 MB
image/building_2D/launchpad.jpeg ADDED

Git LFS Details

  • SHA256: a73a95493a08694da8bff3b06dfe5f7356d69458e0f58facd0cc57ca968a8d89
  • Pointer size: 131 Bytes
  • Size of remote file: 285 kB
image/building_2D/launchpad.png ADDED

Git LFS Details

  • SHA256: bfdc7ca4ab8a1c93dae733930f8a205d2517d2fb0120ac2ba91dfe6df1724a1a
  • Pointer size: 131 Bytes
  • Size of remote file: 974 kB
image/building_2D/living_area.jpeg ADDED

Git LFS Details

  • SHA256: aafd0f25b7d94c377c9b5f0751a6e6f66809dd2701be3816c9586ce101164462
  • Pointer size: 131 Bytes
  • Size of remote file: 311 kB
image/building_2D/living_area.png ADDED

Git LFS Details

  • SHA256: dff09679d77f306a87cabf17e343fde81c50956f68047ea5f015abeeaba5efbe
  • Pointer size: 132 Bytes
  • Size of remote file: 1.17 MB
image/building_2D/repair_shop.jpeg ADDED

Git LFS Details

  • SHA256: 6460cc5791b908bd991279a0284bc91d31d9221c65bb4a76b55bff521c877b2f
  • Pointer size: 131 Bytes
  • Size of remote file: 301 kB
image/building_2D/repair_shop.png ADDED

Git LFS Details

  • SHA256: 7fade5850a323e6f0affaa5300747e792db0a38e663237e3d5509da7a50975d1
  • Pointer size: 131 Bytes
  • Size of remote file: 994 kB