Luigi commited on
Commit
dbb8242
·
1 Parent(s): 1377a96

Add MCP support to the game and integrate NL-to-MCP translation capabilities

Browse files
Files changed (2) hide show
  1. app.py +418 -276
  2. todos.txt +2 -2
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
- Launch a nuclear missile at the target location.
 
 
1089
 
1090
- Args:
1091
- player_id (int): The ID of the player launching the nuke.
1092
- target (Position): The target location for the nuke.
1093
- """
1094
- # Check if player has superweapon ready
1095
- player = self.game_state.players.get(player_id)
1096
- if not player or not player.superweapon_ready:
1097
- return {"success": False, "error": "Superweapon not ready"}
 
 
 
 
1098
 
1099
- # Find all units in the target area (5x5 tiles)
1100
- affected_units = []
1101
- for unit in self.game_state.units.values():
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
- # Find all buildings in the target area
1107
- affected_buildings = []
1108
- for building in self.game_state.buildings.values():
1109
- if building.position.x >= target.x - TILE_SIZE and building.position.x <= target.x + TILE_SIZE and \
1110
- building.position.y >= target.y - TILE_SIZE and building.position.y <= target.y + TILE_SIZE:
1111
- affected_buildings.append(building)
 
 
 
 
 
 
1112
 
1113
- # Deal damage to units (50% chance to destroy each unit)
1114
- for unit in affected_units:
1115
- if random.random() < 0.5:
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
- # Destroy buildings (except HQ, which cannot be destroyed by nukes)
1123
- for building in affected_buildings:
1124
- if building.type != BuildingType.HQ:
1125
- del self.game_state.buildings[building.id]
 
 
1126
 
1127
- # Reset superweapon charge
1128
- player.superweapon_charge = 0
1129
- player.superweapon_ready = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1130
 
1131
- # Broadcast nuke launch event
1132
- await self.broadcast({
1133
- "type": "nuke_launched",
1134
- "player_id": player_id,
1135
- "target": target.to_dict(),
1136
- "affected_units": [u.to_dict() for u in affected_units],
1137
- "affected_buildings": [b.to_dict() for b in affected_buildings]
1138
- })
 
 
1139
 
1140
- return {"success": True, "message": "Nuclear missile launched"}
1141
-
1142
- async def execute_mcp_call(self, mcp_call: dict) -> dict:
1143
- """Execute an MCP tool call on the game state"""
1144
- tool = mcp_call.get("tool")
1145
- args = mcp_call.get("args", {})
 
 
 
 
1146
 
1147
- try:
1148
- if tool == "get_game_state":
1149
- return {
1150
- "action": "get_game_state",
1151
- "data": self.game_state.to_dict()
1152
- }
1153
 
1154
- elif tool == "move_units":
1155
- unit_ids = args.get("unit_ids", [])
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
- elif tool == "attack_unit":
1174
- attacker_ids = args.get("attacker_ids", [])
1175
- target_id = args.get("target_id", "")
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
- elif tool == "build_building":
1205
- building_type = args.get("building_type", "")
1206
- position_x = args.get("position_x", 0)
1207
- position_y = args.get("position_y", 0)
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
- elif tool == "get_ai_analysis":
1267
- language = args.get("language", "fr")
1268
- # Use the existing AI analysis system
1269
- from ai_analysis import get_ai_analyzer
1270
- analyzer = get_ai_analyzer()
1271
- if analyzer:
1272
- analysis = analyzer.summarize_combat_situation(self.game_state.to_dict(), language)
1273
- return {
1274
- "action": "get_ai_analysis",
1275
- "analysis": analysis
1276
- }
1277
- else:
1278
- return {
1279
- "action": "get_ai_analysis",
1280
- "error": "AI analyzer not available"
1281
- }
1282
 
1283
- else:
1284
- return {
1285
- "action": "unknown_tool",
1286
- "error": f"Unknown MCP tool: {tool}"
1287
- }
1288
-
1289
- except Exception as e:
1290
- return {
1291
- "action": "error",
1292
- "error": str(e)
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
- if translation_result.get("success"):
1302
- mcp_call = translation_result["translation"]
 
 
 
 
 
 
 
 
 
 
1303
 
1304
- # Execute the MCP call
1305
- result = await self.execute_mcp_call(mcp_call)
1306
 
1307
- # Send response back to user
1308
- await websocket.send_json({
1309
- "type": "nl_command_response",
1310
- "original_command": command,
1311
- "translation": mcp_call,
1312
- "result": result,
1313
- "success": True
1314
  })
1315
  else:
1316
- # Send error response
1317
- await websocket.send_json({
1318
- "type": "nl_command_response",
1319
- "original_command": command,
1320
- "error": translation_result.get("error", "Translation failed"),
1321
- "clarification": translation_result.get("clarification", ""),
1322
- "success": False
 
 
 
 
 
 
1323
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1324
 
1325
- except Exception as e:
1326
- await websocket.send_json({
1327
- "type": "nl_command_response",
1328
- "original_command": command,
1329
- "error": f"Command processing error: {str(e)}",
1330
- "success": False
1331
- })
1332
-
1333
- async def handle_message(self, websocket: WebSocket, message: dict):
1334
- """Handle incoming WebSocket messages"""
1335
- try:
1336
- if message["type"] == "nl_command":
1337
- # Handle natural language command
1338
- nl_text = message.get("text", "")
1339
- language = message.get("language", "fr")
1340
-
1341
- if not nl_text.strip():
1342
- await websocket.send_json({
1343
- "type": "nl_command_response",
1344
- "status": "error",
1345
- "message": "Empty command received"
 
 
 
1346
  })
1347
- return
 
 
 
 
 
 
 
 
 
 
 
1348
 
1349
- # Translate natural language to MCP
1350
- try:
1351
- from nl_to_mcp_translator import translate_nl_to_mcp
1352
- mcp_call = translate_nl_to_mcp(nl_text, language)
1353
 
1354
- if mcp_call.get("error"):
1355
- await websocket.send_json({
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
- # Execute the MCP call
1364
- result = await self.execute_mcp_call(mcp_call)
 
 
1365
 
1366
- # Send response back to client
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": "state_update",
1380
- "state": state_dict
 
1381
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
1382
 
1383
- except Exception as e:
1384
- await websocket.send_json({
1385
- "type": "nl_command_response",
1386
- "status": "error",
1387
- "message": f"Command execution failed: {str(e)}",
1388
- "original_text": nl_text
1389
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1390
 
1391
- # Handle other message types here...
1392
- # ...existing code...
 
 
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
- 1. Evaluate SLMs on MCP capability
2
- 2. Integrate the best one into app to perform NL-to-MCP translation
 
1
+ 0. Bring MCP support to the game
2
+ 1. Integrate the best one into app to perform NL-to-MCP translation