Spaces:
Sleeping
Sleeping
Add MCP support to the game and integrate NL-to-MCP translation capabilities
Browse files
app.py
CHANGED
|
@@ -24,7 +24,6 @@ import uuid
|
|
| 24 |
# Import localization and AI systems
|
| 25 |
from localization import LOCALIZATION
|
| 26 |
from ai_analysis import get_ai_analyzer, get_model_download_status
|
| 27 |
-
from nl_to_mcp_translator import translate_nl_to_mcp # Add NL translation import
|
| 28 |
|
| 29 |
# Game Constants
|
| 30 |
TILE_SIZE = 40
|
|
@@ -1084,309 +1083,452 @@ class ConnectionManager:
|
|
| 1084 |
pass
|
| 1085 |
|
| 1086 |
async def launch_nuke(self, player_id: int, target: Position):
|
| 1087 |
-
"""
|
| 1088 |
-
|
|
|
|
|
|
|
| 1089 |
|
| 1090 |
-
|
| 1091 |
-
|
| 1092 |
-
|
| 1093 |
-
|
| 1094 |
-
|
| 1095 |
-
|
| 1096 |
-
|
| 1097 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1098 |
|
| 1099 |
-
#
|
| 1100 |
-
|
| 1101 |
-
|
| 1102 |
-
if unit.position.x >= target.x - TILE_SIZE and unit.position.x <= target.x + TILE_SIZE and \
|
| 1103 |
-
unit.position.y >= target.y - TILE_SIZE and unit.position.y <= target.y + TILE_SIZE:
|
| 1104 |
-
affected_units.append(unit)
|
| 1105 |
|
| 1106 |
-
#
|
| 1107 |
-
|
| 1108 |
-
for building in self.game_state.buildings.
|
| 1109 |
-
|
| 1110 |
-
|
| 1111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1112 |
|
| 1113 |
-
#
|
| 1114 |
-
for
|
| 1115 |
-
|
| 1116 |
-
# Destroyed
|
| 1117 |
-
del self.game_state.units[unit.id]
|
| 1118 |
-
else:
|
| 1119 |
-
# Damaged (survived)
|
| 1120 |
-
unit.health = max(1, unit.health - 50)
|
| 1121 |
|
| 1122 |
-
|
| 1123 |
-
|
| 1124 |
-
|
| 1125 |
-
|
|
|
|
|
|
|
| 1126 |
|
| 1127 |
-
|
| 1128 |
-
|
| 1129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1130 |
|
| 1131 |
-
|
| 1132 |
-
|
| 1133 |
-
|
| 1134 |
-
|
| 1135 |
-
|
| 1136 |
-
|
| 1137 |
-
|
| 1138 |
-
|
|
|
|
|
|
|
| 1139 |
|
| 1140 |
-
|
| 1141 |
-
|
| 1142 |
-
|
| 1143 |
-
|
| 1144 |
-
|
| 1145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1146 |
|
| 1147 |
-
|
| 1148 |
-
|
| 1149 |
-
|
| 1150 |
-
|
| 1151 |
-
"data": self.game_state.to_dict()
|
| 1152 |
-
}
|
| 1153 |
|
| 1154 |
-
|
| 1155 |
-
|
| 1156 |
-
target_x = args.get("target_x", 0)
|
| 1157 |
-
target_y = args.get("target_y", 0)
|
| 1158 |
-
|
| 1159 |
-
# Find units by type or ID
|
| 1160 |
-
moved_units = []
|
| 1161 |
-
for unit_id, unit in self.game_state.units.items():
|
| 1162 |
-
if unit.player_id == 0: # Player units
|
| 1163 |
-
if unit.type.name.lower() in unit_ids or unit_id in unit_ids:
|
| 1164 |
-
unit.target = Position(target_x, target_y)
|
| 1165 |
-
moved_units.append(unit_id)
|
| 1166 |
-
|
| 1167 |
-
return {
|
| 1168 |
-
"action": "move_units",
|
| 1169 |
-
"units_moved": len(moved_units),
|
| 1170 |
-
"target": (target_x, target_y)
|
| 1171 |
-
}
|
| 1172 |
|
| 1173 |
-
|
| 1174 |
-
|
| 1175 |
-
|
| 1176 |
-
|
| 1177 |
-
# Find target unit
|
| 1178 |
-
target_unit = None
|
| 1179 |
-
for unit_id, unit in self.game_state.units.items():
|
| 1180 |
-
if unit.player_id == 1 and (unit_id == target_id or str(unit.type).lower() == target_id.lower()):
|
| 1181 |
-
target_unit = unit
|
| 1182 |
-
break
|
| 1183 |
-
|
| 1184 |
-
if target_unit:
|
| 1185 |
-
# Set attackers to target this unit
|
| 1186 |
-
attackers_set = 0
|
| 1187 |
-
for unit_id, unit in self.game_state.units.items():
|
| 1188 |
-
if unit.player_id == 0: # Player units
|
| 1189 |
-
if unit.type.name.lower() in attacker_ids or unit_id in attacker_ids:
|
| 1190 |
-
unit.target_unit_id = target_unit.id
|
| 1191 |
-
attackers_set += 1
|
| 1192 |
-
|
| 1193 |
-
return {
|
| 1194 |
-
"action": "attack_unit",
|
| 1195 |
-
"target": target_id,
|
| 1196 |
-
"attackers": attackers_set
|
| 1197 |
-
}
|
| 1198 |
-
else:
|
| 1199 |
-
return {
|
| 1200 |
-
"action": "attack_unit",
|
| 1201 |
-
"error": f"Target unit {target_id} not found"
|
| 1202 |
-
}
|
| 1203 |
|
| 1204 |
-
|
| 1205 |
-
|
| 1206 |
-
|
| 1207 |
-
|
| 1208 |
-
player_id = args.get("player_id", 0)
|
| 1209 |
-
|
| 1210 |
-
# Map building type string to enum
|
| 1211 |
-
building_map = {
|
| 1212 |
-
"hq": BuildingType.HQ,
|
| 1213 |
-
"power_plant": BuildingType.POWER_PLANT,
|
| 1214 |
-
"barracks": BuildingType.BARRACKS,
|
| 1215 |
-
"war_factory": BuildingType.WAR_FACTORY,
|
| 1216 |
-
"refinery": BuildingType.REFINERY,
|
| 1217 |
-
"defense_turret": BuildingType.DEFENSE_TURRET
|
| 1218 |
-
}
|
| 1219 |
-
|
| 1220 |
-
building_enum = building_map.get(building_type.lower())
|
| 1221 |
-
if building_enum:
|
| 1222 |
-
# Check if player has enough credits
|
| 1223 |
-
player = self.game_state.players.get(player_id)
|
| 1224 |
-
building_cost = {
|
| 1225 |
-
BuildingType.HQ: 0, # Can't build HQ
|
| 1226 |
-
BuildingType.POWER_PLANT: 300,
|
| 1227 |
-
BuildingType.BARRACKS: 500,
|
| 1228 |
-
BuildingType.WAR_FACTORY: 800,
|
| 1229 |
-
BuildingType.REFINERY: 600,
|
| 1230 |
-
BuildingType.DEFENSE_TURRET: 400
|
| 1231 |
-
}
|
| 1232 |
-
|
| 1233 |
-
cost = building_cost.get(building_enum, 1000)
|
| 1234 |
-
if player and player.credits >= cost:
|
| 1235 |
-
player.credits -= cost
|
| 1236 |
-
building_id = str(uuid.uuid4())
|
| 1237 |
-
|
| 1238 |
-
self.game_state.buildings[building_id] = Building(
|
| 1239 |
-
id=building_id,
|
| 1240 |
-
type=building_enum,
|
| 1241 |
-
player_id=player_id,
|
| 1242 |
-
position=Position(position_x, position_y),
|
| 1243 |
-
health=500,
|
| 1244 |
-
max_health=500,
|
| 1245 |
-
production_queue=[],
|
| 1246 |
-
production_progress=0
|
| 1247 |
-
)
|
| 1248 |
-
|
| 1249 |
-
return {
|
| 1250 |
-
"action": "build_building",
|
| 1251 |
-
"building": building_type,
|
| 1252 |
-
"position": (position_x, position_y),
|
| 1253 |
-
"cost": cost
|
| 1254 |
-
}
|
| 1255 |
-
else:
|
| 1256 |
-
return {
|
| 1257 |
-
"action": "build_building",
|
| 1258 |
-
"error": f"Not enough credits. Need {cost}, have {player.credits if player else 0}"
|
| 1259 |
-
}
|
| 1260 |
-
else:
|
| 1261 |
-
return {
|
| 1262 |
-
"action": "build_building",
|
| 1263 |
-
"error": f"Unknown building type: {building_type}"
|
| 1264 |
-
}
|
| 1265 |
|
| 1266 |
-
|
| 1267 |
-
|
| 1268 |
-
|
| 1269 |
-
|
| 1270 |
-
|
| 1271 |
-
|
| 1272 |
-
|
| 1273 |
-
|
| 1274 |
-
|
| 1275 |
-
|
| 1276 |
-
|
| 1277 |
-
|
| 1278 |
-
|
| 1279 |
-
|
| 1280 |
-
"error": "AI analyzer not available"
|
| 1281 |
-
}
|
| 1282 |
|
| 1283 |
-
|
| 1284 |
-
|
| 1285 |
-
|
| 1286 |
-
|
| 1287 |
-
|
| 1288 |
-
|
| 1289 |
-
|
| 1290 |
-
|
| 1291 |
-
|
| 1292 |
-
|
| 1293 |
-
|
| 1294 |
-
|
| 1295 |
-
async def handle_nl_command(self, websocket: WebSocket, command: str):
|
| 1296 |
-
"""Handle natural language commands from users"""
|
| 1297 |
-
try:
|
| 1298 |
-
# Translate NL to MCP
|
| 1299 |
-
translation_result = translate_nl_to_mcp(command)
|
| 1300 |
|
| 1301 |
-
|
| 1302 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1303 |
|
| 1304 |
-
#
|
| 1305 |
-
|
| 1306 |
|
| 1307 |
-
#
|
| 1308 |
-
|
| 1309 |
-
|
| 1310 |
-
|
| 1311 |
-
"
|
| 1312 |
-
"
|
| 1313 |
-
"
|
| 1314 |
})
|
| 1315 |
else:
|
| 1316 |
-
#
|
| 1317 |
-
|
| 1318 |
-
|
| 1319 |
-
|
| 1320 |
-
|
| 1321 |
-
"
|
| 1322 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1323 |
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1324 |
|
| 1325 |
-
|
| 1326 |
-
|
| 1327 |
-
|
| 1328 |
-
|
| 1329 |
-
|
| 1330 |
-
|
| 1331 |
-
|
| 1332 |
-
|
| 1333 |
-
|
| 1334 |
-
|
| 1335 |
-
|
| 1336 |
-
|
| 1337 |
-
|
| 1338 |
-
|
| 1339 |
-
|
| 1340 |
-
|
| 1341 |
-
|
| 1342 |
-
|
| 1343 |
-
|
| 1344 |
-
|
| 1345 |
-
|
|
|
|
|
|
|
|
|
|
| 1346 |
})
|
| 1347 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1348 |
|
| 1349 |
-
|
| 1350 |
-
|
| 1351 |
-
from nl_to_mcp_translator import translate_nl_to_mcp
|
| 1352 |
-
mcp_call = translate_nl_to_mcp(nl_text, language)
|
| 1353 |
|
| 1354 |
-
|
| 1355 |
-
|
| 1356 |
-
"type": "nl_command_response",
|
| 1357 |
-
"status": "error",
|
| 1358 |
-
"message": f"Translation error: {mcp_call['error']}",
|
| 1359 |
-
"original_text": nl_text
|
| 1360 |
-
})
|
| 1361 |
-
return
|
| 1362 |
|
| 1363 |
-
#
|
| 1364 |
-
|
|
|
|
|
|
|
| 1365 |
|
| 1366 |
-
#
|
| 1367 |
-
await websocket.send_json({
|
| 1368 |
-
"type": "nl_command_response",
|
| 1369 |
-
"status": "success",
|
| 1370 |
-
"message": f"Command executed: {result.get('action', 'unknown')}",
|
| 1371 |
-
"result": result,
|
| 1372 |
-
"original_text": nl_text,
|
| 1373 |
-
"translated_call": mcp_call
|
| 1374 |
-
})
|
| 1375 |
-
|
| 1376 |
-
# Broadcast game state update to all clients
|
| 1377 |
-
state_dict = self.game_state.to_dict()
|
| 1378 |
await self.broadcast({
|
| 1379 |
-
"type": "
|
| 1380 |
-
"
|
|
|
|
| 1381 |
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1382 |
|
| 1383 |
-
|
| 1384 |
-
|
| 1385 |
-
"
|
| 1386 |
-
"
|
| 1387 |
-
"message": f"Command execution failed: {str(e)}",
|
| 1388 |
-
"original_text": nl_text
|
| 1389 |
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1390 |
|
| 1391 |
-
|
| 1392 |
-
|
|
|
|
|
|
| 24 |
# Import localization and AI systems
|
| 25 |
from localization import LOCALIZATION
|
| 26 |
from ai_analysis import get_ai_analyzer, get_model_download_status
|
|
|
|
| 27 |
|
| 28 |
# Game Constants
|
| 29 |
TILE_SIZE = 40
|
|
|
|
| 1083 |
pass
|
| 1084 |
|
| 1085 |
async def launch_nuke(self, player_id: int, target: Position):
|
| 1086 |
+
"""Launch nuclear strike at target location"""
|
| 1087 |
+
# Damage radius: 200 pixels = 5 tiles
|
| 1088 |
+
NUKE_DAMAGE_RADIUS = 200.0
|
| 1089 |
+
NUKE_MAX_DAMAGE = 200 # Maximum damage at center
|
| 1090 |
|
| 1091 |
+
# Damage all units within radius
|
| 1092 |
+
units_to_remove = []
|
| 1093 |
+
for unit_id, unit in self.game_state.units.items():
|
| 1094 |
+
distance = unit.position.distance_to(target)
|
| 1095 |
+
if distance <= NUKE_DAMAGE_RADIUS:
|
| 1096 |
+
# Damage decreases with distance (full damage at center, 50% at edge)
|
| 1097 |
+
damage_factor = 1.0 - (distance / NUKE_DAMAGE_RADIUS) * 0.5
|
| 1098 |
+
damage = int(NUKE_MAX_DAMAGE * damage_factor)
|
| 1099 |
+
|
| 1100 |
+
unit.health -= damage
|
| 1101 |
+
if unit.health <= 0:
|
| 1102 |
+
units_to_remove.append(unit_id)
|
| 1103 |
|
| 1104 |
+
# Remove destroyed units
|
| 1105 |
+
for unit_id in units_to_remove:
|
| 1106 |
+
del self.game_state.units[unit_id]
|
|
|
|
|
|
|
|
|
|
| 1107 |
|
| 1108 |
+
# Damage buildings within radius
|
| 1109 |
+
buildings_to_remove = []
|
| 1110 |
+
for building_id, building in self.game_state.buildings.items():
|
| 1111 |
+
distance = building.position.distance_to(target)
|
| 1112 |
+
if distance <= NUKE_DAMAGE_RADIUS:
|
| 1113 |
+
# Damage decreases with distance
|
| 1114 |
+
damage_factor = 1.0 - (distance / NUKE_DAMAGE_RADIUS) * 0.5
|
| 1115 |
+
damage = int(NUKE_MAX_DAMAGE * damage_factor)
|
| 1116 |
+
|
| 1117 |
+
building.health -= damage
|
| 1118 |
+
if building.health <= 0:
|
| 1119 |
+
buildings_to_remove.append(building_id)
|
| 1120 |
|
| 1121 |
+
# Remove destroyed buildings
|
| 1122 |
+
for building_id in buildings_to_remove:
|
| 1123 |
+
del self.game_state.buildings[building_id]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1124 |
|
| 1125 |
+
print(f"💥 NUKE launched by player {player_id} at ({target.x:.0f}, {target.y:.0f})")
|
| 1126 |
+
print(f" Destroyed {len(units_to_remove)} units and {len(buildings_to_remove)} buildings")
|
| 1127 |
+
|
| 1128 |
+
async def handle_command(self, command: dict):
|
| 1129 |
+
"""Handle game commands from clients"""
|
| 1130 |
+
cmd_type = command.get("type")
|
| 1131 |
|
| 1132 |
+
if cmd_type == "move_unit":
|
| 1133 |
+
unit_ids = command.get("unit_ids", [])
|
| 1134 |
+
target = command.get("target")
|
| 1135 |
+
if target and "x" in target and "y" in target:
|
| 1136 |
+
base_target = Position(target["x"], target["y"])
|
| 1137 |
+
|
| 1138 |
+
# If multiple units, spread them in a formation
|
| 1139 |
+
if len(unit_ids) > 1:
|
| 1140 |
+
# Formation pattern: circular spread around target
|
| 1141 |
+
radius = 30.0 # Distance between units in formation
|
| 1142 |
+
for idx, uid in enumerate(unit_ids):
|
| 1143 |
+
if uid in self.game_state.units:
|
| 1144 |
+
unit = self.game_state.units[uid]
|
| 1145 |
+
|
| 1146 |
+
# Calculate offset position in circular formation
|
| 1147 |
+
angle = (idx * 360.0 / len(unit_ids)) * (3.14159 / 180.0)
|
| 1148 |
+
offset_x = radius * (1 + idx // 8) * 0.707106781 * ((idx % 2) * 2 - 1)
|
| 1149 |
+
offset_y = radius * (1 + idx // 8) * 0.707106781 * (((idx + 1) % 2) * 2 - 1)
|
| 1150 |
+
|
| 1151 |
+
unit.target = Position(
|
| 1152 |
+
base_target.x + offset_x,
|
| 1153 |
+
base_target.y + offset_y
|
| 1154 |
+
)
|
| 1155 |
+
|
| 1156 |
+
# FIX: Clear combat target and set manual order flag
|
| 1157 |
+
unit.target_unit_id = None
|
| 1158 |
+
unit.manual_order = True
|
| 1159 |
+
|
| 1160 |
+
# If it's a Harvester, enable manual control to override AI
|
| 1161 |
+
if unit.type == UnitType.HARVESTER:
|
| 1162 |
+
unit.manual_control = True
|
| 1163 |
+
# Clear AI state
|
| 1164 |
+
unit.gathering = False
|
| 1165 |
+
unit.returning = False
|
| 1166 |
+
unit.ore_target = None
|
| 1167 |
+
else:
|
| 1168 |
+
# Single unit - move to exact target
|
| 1169 |
+
for uid in unit_ids:
|
| 1170 |
+
if uid in self.game_state.units:
|
| 1171 |
+
unit = self.game_state.units[uid]
|
| 1172 |
+
unit.target = base_target
|
| 1173 |
+
|
| 1174 |
+
# FIX: Clear combat target and set manual order flag
|
| 1175 |
+
unit.target_unit_id = None
|
| 1176 |
+
unit.manual_order = True
|
| 1177 |
+
|
| 1178 |
+
# If it's a Harvester, enable manual control to override AI
|
| 1179 |
+
if unit.type == UnitType.HARVESTER:
|
| 1180 |
+
unit.manual_control = True
|
| 1181 |
+
# Clear AI state
|
| 1182 |
+
unit.gathering = False
|
| 1183 |
+
unit.returning = False
|
| 1184 |
+
unit.ore_target = None
|
| 1185 |
|
| 1186 |
+
elif cmd_type == "attack_unit":
|
| 1187 |
+
attacker_ids = command.get("attacker_ids", [])
|
| 1188 |
+
target_id = command.get("target_id")
|
| 1189 |
+
|
| 1190 |
+
for uid in attacker_ids:
|
| 1191 |
+
if uid in self.game_state.units and target_id in self.game_state.units:
|
| 1192 |
+
attacker = self.game_state.units[uid]
|
| 1193 |
+
attacker.target_unit_id = target_id
|
| 1194 |
+
attacker.target_building_id = None # Clear building target
|
| 1195 |
+
attacker.manual_order = True # Set manual order flag
|
| 1196 |
|
| 1197 |
+
elif cmd_type == "attack_building":
|
| 1198 |
+
attacker_ids = command.get("attacker_ids", [])
|
| 1199 |
+
target_id = command.get("target_id")
|
| 1200 |
+
|
| 1201 |
+
for uid in attacker_ids:
|
| 1202 |
+
if uid in self.game_state.units and target_id in self.game_state.buildings:
|
| 1203 |
+
attacker = self.game_state.units[uid]
|
| 1204 |
+
attacker.target_building_id = target_id
|
| 1205 |
+
attacker.target_unit_id = None # Clear unit target
|
| 1206 |
+
attacker.manual_order = True # Set manual order flag
|
| 1207 |
|
| 1208 |
+
elif cmd_type == "build_unit":
|
| 1209 |
+
unit_type_str = command.get("unit_type")
|
| 1210 |
+
player_id = command.get("player_id", 0)
|
| 1211 |
+
preferred_building_id = command.get("building_id") # optional: choose production building
|
|
|
|
|
|
|
| 1212 |
|
| 1213 |
+
if not unit_type_str:
|
| 1214 |
+
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1215 |
|
| 1216 |
+
try:
|
| 1217 |
+
unit_type = UnitType(unit_type_str)
|
| 1218 |
+
except ValueError:
|
| 1219 |
+
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1220 |
|
| 1221 |
+
# RED ALERT: Check cost!
|
| 1222 |
+
cost = UNIT_COSTS.get(unit_type, 0)
|
| 1223 |
+
player_language = self.game_state.players[player_id].language if player_id in self.game_state.players else "en"
|
| 1224 |
+
current_credits = self.game_state.players[player_id].credits if player_id in self.game_state.players else 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1225 |
|
| 1226 |
+
if current_credits < cost:
|
| 1227 |
+
# Not enough credits! (translated)
|
| 1228 |
+
message = LOCALIZATION.translate(
|
| 1229 |
+
player_language,
|
| 1230 |
+
"notification.insufficient_credits",
|
| 1231 |
+
cost=cost,
|
| 1232 |
+
current=current_credits
|
| 1233 |
+
)
|
| 1234 |
+
await self.broadcast({
|
| 1235 |
+
"type": "notification",
|
| 1236 |
+
"message": message,
|
| 1237 |
+
"level": "error"
|
| 1238 |
+
})
|
| 1239 |
+
return
|
|
|
|
|
|
|
| 1240 |
|
| 1241 |
+
# Find required building type
|
| 1242 |
+
required_building = PRODUCTION_REQUIREMENTS.get(unit_type)
|
| 1243 |
+
|
| 1244 |
+
if not required_building:
|
| 1245 |
+
return
|
| 1246 |
+
|
| 1247 |
+
# If provided, use preferred building if valid
|
| 1248 |
+
suitable_building = None
|
| 1249 |
+
if preferred_building_id and preferred_building_id in self.game_state.buildings:
|
| 1250 |
+
b = self.game_state.buildings[preferred_building_id]
|
| 1251 |
+
if b.player_id == player_id and b.type == required_building:
|
| 1252 |
+
suitable_building = b
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1253 |
|
| 1254 |
+
# Otherwise choose least busy eligible building
|
| 1255 |
+
if not suitable_building:
|
| 1256 |
+
eligible = [
|
| 1257 |
+
b for b in self.game_state.buildings.values()
|
| 1258 |
+
if b.player_id == player_id and b.type == required_building
|
| 1259 |
+
]
|
| 1260 |
+
if eligible:
|
| 1261 |
+
suitable_building = min(eligible, key=lambda b: len(b.production_queue))
|
| 1262 |
+
|
| 1263 |
+
if suitable_building:
|
| 1264 |
+
# RED ALERT: Deduct credits!
|
| 1265 |
+
self.game_state.players[player_id].credits -= cost
|
| 1266 |
|
| 1267 |
+
# Add to production queue
|
| 1268 |
+
suitable_building.production_queue.append(unit_type_str)
|
| 1269 |
|
| 1270 |
+
# Translated notification
|
| 1271 |
+
unit_name = LOCALIZATION.translate(player_language, f"unit.{unit_type_str}")
|
| 1272 |
+
message = LOCALIZATION.translate(player_language, "notification.unit_training", unit=unit_name)
|
| 1273 |
+
await self.broadcast({
|
| 1274 |
+
"type": "notification",
|
| 1275 |
+
"message": message,
|
| 1276 |
+
"level": "success"
|
| 1277 |
})
|
| 1278 |
else:
|
| 1279 |
+
# Translated requirement message
|
| 1280 |
+
unit_name = LOCALIZATION.translate(player_language, f"unit.{unit_type_str}")
|
| 1281 |
+
building_name = LOCALIZATION.translate(player_language, f"building.{required_building.value}")
|
| 1282 |
+
message = LOCALIZATION.translate(
|
| 1283 |
+
player_language,
|
| 1284 |
+
"notification.unit_requires",
|
| 1285 |
+
unit=unit_name,
|
| 1286 |
+
requirement=building_name
|
| 1287 |
+
)
|
| 1288 |
+
await self.broadcast({
|
| 1289 |
+
"type": "notification",
|
| 1290 |
+
"message": message,
|
| 1291 |
+
"level": "error"
|
| 1292 |
})
|
| 1293 |
+
|
| 1294 |
+
elif cmd_type == "build_building":
|
| 1295 |
+
building_type_str = command.get("building_type")
|
| 1296 |
+
position = command.get("position")
|
| 1297 |
+
player_id = command.get("player_id", 0)
|
| 1298 |
+
|
| 1299 |
+
if not building_type_str or not position:
|
| 1300 |
+
return
|
| 1301 |
+
|
| 1302 |
+
try:
|
| 1303 |
+
building_type = BuildingType(building_type_str)
|
| 1304 |
+
except ValueError:
|
| 1305 |
+
return
|
| 1306 |
+
|
| 1307 |
+
# RED ALERT: Check cost!
|
| 1308 |
+
cost = BUILDING_COSTS.get(building_type, 0)
|
| 1309 |
+
player_language = self.game_state.players[player_id].language if player_id in self.game_state.players else "en"
|
| 1310 |
+
current_credits = self.game_state.players[player_id].credits if player_id in self.game_state.players else 0
|
| 1311 |
+
|
| 1312 |
+
if current_credits < cost:
|
| 1313 |
+
# Not enough credits! (translated)
|
| 1314 |
+
message = LOCALIZATION.translate(
|
| 1315 |
+
player_language,
|
| 1316 |
+
"notification.insufficient_credits",
|
| 1317 |
+
cost=cost,
|
| 1318 |
+
current=current_credits
|
| 1319 |
+
)
|
| 1320 |
+
await self.broadcast({
|
| 1321 |
+
"type": "notification",
|
| 1322 |
+
"message": message,
|
| 1323 |
+
"level": "error"
|
| 1324 |
+
})
|
| 1325 |
+
return
|
| 1326 |
+
|
| 1327 |
+
# Rule: limit multiple same-type buildings if disabled
|
| 1328 |
+
if not ALLOW_MULTIPLE_SAME_BUILDING and building_type != BuildingType.HQ:
|
| 1329 |
+
for b in self.game_state.buildings.values():
|
| 1330 |
+
if b.player_id == player_id and b.type == building_type:
|
| 1331 |
+
message = LOCALIZATION.translate(player_language, "notification.building_limit_one", building=LOCALIZATION.translate(player_language, f"building.{building_type_str}"))
|
| 1332 |
+
await self.broadcast({"type":"notification","message":message,"level":"error"})
|
| 1333 |
+
return
|
| 1334 |
+
|
| 1335 |
+
# Enforce HQ build radius
|
| 1336 |
+
# Find player's HQ
|
| 1337 |
+
hq = None
|
| 1338 |
+
for b in self.game_state.buildings.values():
|
| 1339 |
+
if b.player_id == player_id and b.type == BuildingType.HQ:
|
| 1340 |
+
hq = b
|
| 1341 |
+
break
|
| 1342 |
+
if hq and position and "x" in position and "y" in position:
|
| 1343 |
+
max_dist = HQ_BUILD_RADIUS_TILES * TILE_SIZE
|
| 1344 |
+
dx = position["x"] - hq.position.x
|
| 1345 |
+
dy = position["y"] - hq.position.y
|
| 1346 |
+
if (dx*dx + dy*dy) ** 0.5 > max_dist:
|
| 1347 |
+
message = LOCALIZATION.translate(player_language, "notification.building_too_far_from_hq")
|
| 1348 |
+
await self.broadcast({"type":"notification","message":message,"level":"error"})
|
| 1349 |
+
return
|
| 1350 |
+
|
| 1351 |
+
# RED ALERT: Deduct credits!
|
| 1352 |
+
self.game_state.players[player_id].credits -= cost
|
| 1353 |
+
|
| 1354 |
+
if position and "x" in position and "y" in position:
|
| 1355 |
+
self.game_state.create_building(
|
| 1356 |
+
building_type,
|
| 1357 |
+
player_id,
|
| 1358 |
+
Position(position["x"], position["y"])
|
| 1359 |
+
)
|
| 1360 |
|
| 1361 |
+
# Translated notification
|
| 1362 |
+
building_name = LOCALIZATION.translate(player_language, f"building.{building_type_str}")
|
| 1363 |
+
message = LOCALIZATION.translate(player_language, "notification.building_placed", building=building_name)
|
| 1364 |
+
await self.broadcast({
|
| 1365 |
+
"type": "notification",
|
| 1366 |
+
"message": message,
|
| 1367 |
+
"level": "success"
|
| 1368 |
+
})
|
| 1369 |
+
|
| 1370 |
+
elif cmd_type == "stop_units":
|
| 1371 |
+
unit_ids = command.get("unit_ids", [])
|
| 1372 |
+
for uid in unit_ids:
|
| 1373 |
+
if uid in self.game_state.units:
|
| 1374 |
+
self.game_state.units[uid].target = None
|
| 1375 |
+
|
| 1376 |
+
elif cmd_type == "prepare_nuke":
|
| 1377 |
+
player_id = command.get("player_id", 0)
|
| 1378 |
+
if player_id in self.game_state.players:
|
| 1379 |
+
player = self.game_state.players[player_id]
|
| 1380 |
+
if player.superweapon_ready:
|
| 1381 |
+
player.nuke_preparing = True
|
| 1382 |
+
await self.broadcast({
|
| 1383 |
+
"type": "nuke_preparing",
|
| 1384 |
+
"player_id": player_id
|
| 1385 |
})
|
| 1386 |
+
|
| 1387 |
+
elif cmd_type == "cancel_nuke":
|
| 1388 |
+
player_id = command.get("player_id", 0)
|
| 1389 |
+
if player_id in self.game_state.players:
|
| 1390 |
+
self.game_state.players[player_id].nuke_preparing = False
|
| 1391 |
+
|
| 1392 |
+
elif cmd_type == "launch_nuke":
|
| 1393 |
+
player_id = command.get("player_id", 0)
|
| 1394 |
+
target = command.get("target")
|
| 1395 |
+
|
| 1396 |
+
if player_id in self.game_state.players and target:
|
| 1397 |
+
player = self.game_state.players[player_id]
|
| 1398 |
|
| 1399 |
+
if player.superweapon_ready and player.nuke_preparing:
|
| 1400 |
+
target_pos = Position(target["x"], target["y"])
|
|
|
|
|
|
|
| 1401 |
|
| 1402 |
+
# Launch nuke effect
|
| 1403 |
+
await self.launch_nuke(player_id, target_pos)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1404 |
|
| 1405 |
+
# Reset superweapon state
|
| 1406 |
+
player.superweapon_ready = False
|
| 1407 |
+
player.superweapon_charge = 0
|
| 1408 |
+
player.nuke_preparing = False
|
| 1409 |
|
| 1410 |
+
# Broadcast nuke launch
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1411 |
await self.broadcast({
|
| 1412 |
+
"type": "nuke_launched",
|
| 1413 |
+
"player_id": player_id,
|
| 1414 |
+
"target": {"x": target_pos.x, "y": target_pos.y}
|
| 1415 |
})
|
| 1416 |
+
|
| 1417 |
+
elif cmd_type == "change_language":
|
| 1418 |
+
player_id = command.get("player_id", 0)
|
| 1419 |
+
language = command.get("language", "en")
|
| 1420 |
+
|
| 1421 |
+
if player_id in self.game_state.players:
|
| 1422 |
+
# Validate language
|
| 1423 |
+
supported = list(LOCALIZATION.get_supported_languages())
|
| 1424 |
+
if language in supported:
|
| 1425 |
+
self.game_state.players[player_id].language = language
|
| 1426 |
+
|
| 1427 |
+
# Trigger immediate AI analysis in new language
|
| 1428 |
+
self.last_ai_analysis_time = 0
|
| 1429 |
|
| 1430 |
+
await self.broadcast({
|
| 1431 |
+
"type": "notification",
|
| 1432 |
+
"message": f"Language changed to {LOCALIZATION.get_display_name(language)}",
|
| 1433 |
+
"level": "info"
|
|
|
|
|
|
|
| 1434 |
})
|
| 1435 |
+
|
| 1436 |
+
elif cmd_type == "request_ai_analysis":
|
| 1437 |
+
# Force immediate AI analysis
|
| 1438 |
+
await self.run_ai_analysis()
|
| 1439 |
+
|
| 1440 |
+
await self.broadcast({
|
| 1441 |
+
"type": "ai_analysis_update",
|
| 1442 |
+
"analysis": self.last_ai_analysis
|
| 1443 |
+
})
|
| 1444 |
+
|
| 1445 |
+
# Global connection manager
|
| 1446 |
+
manager = ConnectionManager()
|
| 1447 |
+
|
| 1448 |
+
# Routes
|
| 1449 |
+
@app.get("/")
|
| 1450 |
+
async def get_home():
|
| 1451 |
+
"""Serve the main game interface"""
|
| 1452 |
+
return HTMLResponse(content=open("static/index.html").read())
|
| 1453 |
+
|
| 1454 |
+
@app.get("/health")
|
| 1455 |
+
async def health_check():
|
| 1456 |
+
"""Health check endpoint for HuggingFace Spaces"""
|
| 1457 |
+
return {
|
| 1458 |
+
"status": "healthy",
|
| 1459 |
+
"players": len(manager.game_state.players),
|
| 1460 |
+
"units": len(manager.game_state.units),
|
| 1461 |
+
"buildings": len(manager.game_state.buildings),
|
| 1462 |
+
"active_connections": len(manager.active_connections),
|
| 1463 |
+
"ai_available": manager.ai_analyzer.model_available,
|
| 1464 |
+
"supported_languages": list(LOCALIZATION.get_supported_languages())
|
| 1465 |
+
}
|
| 1466 |
+
|
| 1467 |
+
@app.get("/api/languages")
|
| 1468 |
+
async def get_languages():
|
| 1469 |
+
"""Get supported languages"""
|
| 1470 |
+
languages = []
|
| 1471 |
+
for lang_code in LOCALIZATION.get_supported_languages():
|
| 1472 |
+
languages.append({
|
| 1473 |
+
"code": lang_code,
|
| 1474 |
+
"name": LOCALIZATION.get_display_name(lang_code)
|
| 1475 |
+
})
|
| 1476 |
+
return {"languages": languages}
|
| 1477 |
+
|
| 1478 |
+
@app.get("/api/translations/{language}")
|
| 1479 |
+
async def get_translations(language: str):
|
| 1480 |
+
"""Get all translations for a language"""
|
| 1481 |
+
from localization import TRANSLATIONS
|
| 1482 |
+
if language not in TRANSLATIONS:
|
| 1483 |
+
language = "en"
|
| 1484 |
+
return {"translations": TRANSLATIONS[language], "language": language}
|
| 1485 |
+
|
| 1486 |
+
@app.post("/api/player/{player_id}/language")
|
| 1487 |
+
async def set_player_language(player_id: int, language: str):
|
| 1488 |
+
"""Set player's preferred language"""
|
| 1489 |
+
if player_id in manager.game_state.players:
|
| 1490 |
+
manager.game_state.players[player_id].language = language
|
| 1491 |
+
return {"success": True, "language": language}
|
| 1492 |
+
return {"success": False, "error": "Player not found"}
|
| 1493 |
+
|
| 1494 |
+
@app.get("/api/ai/status")
|
| 1495 |
+
async def get_ai_status():
|
| 1496 |
+
"""Get AI analyzer status"""
|
| 1497 |
+
return {
|
| 1498 |
+
"available": manager.ai_analyzer.model_available,
|
| 1499 |
+
"model_path": manager.ai_analyzer.model_path if manager.ai_analyzer.model_available else None,
|
| 1500 |
+
"last_analysis": manager.last_ai_analysis
|
| 1501 |
+
}
|
| 1502 |
+
|
| 1503 |
+
@app.websocket("/ws")
|
| 1504 |
+
async def websocket_endpoint(websocket: WebSocket):
|
| 1505 |
+
"""WebSocket endpoint for real-time game communication"""
|
| 1506 |
+
await manager.connect(websocket)
|
| 1507 |
+
|
| 1508 |
+
try:
|
| 1509 |
+
# Send initial state
|
| 1510 |
+
await websocket.send_json({
|
| 1511 |
+
"type": "init",
|
| 1512 |
+
"state": manager.game_state.to_dict()
|
| 1513 |
+
})
|
| 1514 |
+
|
| 1515 |
+
# Handle incoming messages
|
| 1516 |
+
while True:
|
| 1517 |
+
data = await websocket.receive_json()
|
| 1518 |
+
await manager.handle_command(data)
|
| 1519 |
+
|
| 1520 |
+
except WebSocketDisconnect:
|
| 1521 |
+
manager.disconnect(websocket)
|
| 1522 |
+
except Exception as e:
|
| 1523 |
+
print(f"WebSocket error: {e}")
|
| 1524 |
+
manager.disconnect(websocket)
|
| 1525 |
+
|
| 1526 |
+
# Mount static files (will be created next)
|
| 1527 |
+
try:
|
| 1528 |
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
| 1529 |
+
except:
|
| 1530 |
+
pass
|
| 1531 |
|
| 1532 |
+
if __name__ == "__main__":
|
| 1533 |
+
import uvicorn
|
| 1534 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
todos.txt
CHANGED
|
@@ -1,2 +1,2 @@
|
|
| 1 |
-
|
| 2 |
-
|
|
|
|
| 1 |
+
0. Bring MCP support to the game
|
| 2 |
+
1. Integrate the best one into app to perform NL-to-MCP translation
|