Luigi commited on
Commit
a510770
·
1 Parent(s): 12d64f8

feat(endgame): elimination-based game over + overlay; fix LLM chat_format and i18n draw

Browse files
__pycache__/ai_analysis.cpython-312.pyc CHANGED
Binary files a/__pycache__/ai_analysis.cpython-312.pyc and b/__pycache__/ai_analysis.cpython-312.pyc differ
 
__pycache__/app.cpython-312.pyc CHANGED
Binary files a/__pycache__/app.cpython-312.pyc and b/__pycache__/app.cpython-312.pyc differ
 
__pycache__/localization.cpython-312.pyc CHANGED
Binary files a/__pycache__/localization.cpython-312.pyc and b/__pycache__/localization.cpython-312.pyc differ
 
ai_analysis.py CHANGED
@@ -42,16 +42,28 @@ def _llama_worker(result_queue, model_path, prompt, messages, max_tokens, temper
42
  result_queue.put({'status': 'error', 'message': f"llama-cpp import failed: {exc}"})
43
  return
44
 
45
- try:
46
- llama = Llama(
47
- model_path=model_path,
48
- n_ctx=2048,
49
- n_threads=2,
50
- verbose=False,
51
- chat_format='qwen'
52
- )
53
- except Exception as exc:
54
- result_queue.put({'status': 'error', 'message': f"Failed to load model: {exc}"})
 
 
 
 
 
 
 
 
 
 
 
 
55
  return
56
 
57
  try:
@@ -120,7 +132,7 @@ def _llama_worker(result_queue, model_path, prompt, messages, max_tokens, temper
120
  prompt or '',
121
  max_tokens=max_tokens,
122
  temperature=temperature,
123
- stop=["\n", "Human:", "Assistant:"]
124
  )
125
  except Exception:
126
  raw_resp = None
 
42
  result_queue.put({'status': 'error', 'message': f"llama-cpp import failed: {exc}"})
43
  return
44
 
45
+ # Try loading the model with best-suited chat template for Qwen2.5
46
+ n_threads = max(1, min(4, os.cpu_count() or 2))
47
+ last_exc = None
48
+ llama = None
49
+ for chat_fmt in ('qwen2', 'qwen', None):
50
+ try:
51
+ kwargs: Dict[str, Any] = dict(
52
+ model_path=model_path,
53
+ n_ctx=4096,
54
+ n_threads=n_threads,
55
+ verbose=False,
56
+ )
57
+ if chat_fmt is not None:
58
+ kwargs['chat_format'] = chat_fmt # type: ignore[index]
59
+ llama = Llama(**kwargs) # type: ignore[arg-type]
60
+ break
61
+ except Exception as exc:
62
+ last_exc = exc
63
+ llama = None
64
+ continue
65
+ if llama is None:
66
+ result_queue.put({'status': 'error', 'message': f"Failed to load model: {last_exc}"})
67
  return
68
 
69
  try:
 
132
  prompt or '',
133
  max_tokens=max_tokens,
134
  temperature=temperature,
135
+ stop=["</s>", "<|endoftext|>"]
136
  )
137
  except Exception:
138
  raw_resp = None
app.py CHANGED
@@ -761,6 +761,52 @@ class ConnectionManager:
761
  free_pos = self.find_free_position_nearby(spawn_pos, new_unit.id)
762
  new_unit.position = free_pos
763
  building.production_progress = 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
764
 
765
  def find_nearest_enemy(self, unit: Unit) -> Optional[Unit]:
766
  """RED ALERT: Find nearest enemy unit"""
 
761
  free_pos = self.find_free_position_nearby(spawn_pos, new_unit.id)
762
  new_unit.position = free_pos
763
  building.production_progress = 0
764
+
765
+ # Endgame checks: elimination-first (after all destructions this tick)
766
+ if not self.game_state.game_over:
767
+ p_units = sum(1 for u in self.game_state.units.values() if u.player_id == 0)
768
+ e_units = sum(1 for u in self.game_state.units.values() if u.player_id == 1)
769
+ p_buildings = sum(1 for b in self.game_state.buildings.values() if b.player_id == 0)
770
+ e_buildings = sum(1 for b in self.game_state.buildings.values() if b.player_id == 1)
771
+
772
+ player_language = self.game_state.players[0].language if 0 in self.game_state.players else "en"
773
+
774
+ if p_units == 0 and p_buildings == 0 and (e_units > 0 or e_buildings > 0):
775
+ # Player eliminated
776
+ self.game_state.game_over = True
777
+ self.game_state.winner = "enemy"
778
+ winner_name = LOCALIZATION.translate(player_language, "game.winner.enemy")
779
+ message = LOCALIZATION.translate(player_language, "game.win.banner", winner=winner_name)
780
+ asyncio.create_task(self.broadcast({
781
+ "type": "game_over",
782
+ "winner": "enemy",
783
+ "message": message
784
+ }))
785
+ elif e_units == 0 and e_buildings == 0 and (p_units > 0 or p_buildings > 0):
786
+ # Enemy eliminated
787
+ self.game_state.game_over = True
788
+ self.game_state.winner = "player"
789
+ winner_name = LOCALIZATION.translate(player_language, "game.winner.player")
790
+ message = LOCALIZATION.translate(player_language, "game.win.banner", winner=winner_name)
791
+ asyncio.create_task(self.broadcast({
792
+ "type": "game_over",
793
+ "winner": "player",
794
+ "message": message
795
+ }))
796
+ elif p_units == 0 and p_buildings == 0 and e_units == 0 and e_buildings == 0:
797
+ # Total annihilation -> draw
798
+ self.game_state.game_over = True
799
+ self.game_state.winner = "draw"
800
+ # Localized draw banner if available
801
+ try:
802
+ draw_msg = LOCALIZATION.translate(player_language, "game.draw.banner")
803
+ except Exception:
804
+ draw_msg = "Draw!"
805
+ asyncio.create_task(self.broadcast({
806
+ "type": "game_over",
807
+ "winner": "draw",
808
+ "message": draw_msg
809
+ }))
810
 
811
  def find_nearest_enemy(self, unit: Unit) -> Optional[Unit]:
812
  """RED ALERT: Find nearest enemy unit"""
localization.py CHANGED
@@ -83,6 +83,7 @@ TRANSLATIONS: Dict[str, Dict[str, str]] = {
83
  "menu.actions.stop.tooltip": "Stop selected units",
84
  "menu.actions.attack_move": "Attack Move",
85
  "menu.actions.attack_move.tooltip": "Attack enemies while moving (hotkey: A)",
 
86
  "menu.control_groups.title": "🎮 Control Groups",
87
  "menu.stats.title": "📈 Game Stats",
88
  "menu.stats.player_units": "Player Units:",
@@ -132,6 +133,7 @@ TRANSLATIONS: Dict[str, Dict[str, str]] = {
132
  "hud.model.download.retry": "Retrying download…",
133
  "hud.model.download.done": "Model ready",
134
  "hud.model.download.error": "Model download failed",
 
135
  },
136
  "fr": {
137
  "game.window.title": "RTS Minimaliste",
@@ -201,6 +203,7 @@ TRANSLATIONS: Dict[str, Dict[str, str]] = {
201
  "menu.actions.stop.tooltip": "Arrêter les unités sélectionnées",
202
  "menu.actions.attack_move": "Attaque en mouvement",
203
  "menu.actions.attack_move.tooltip": "Attaquer les ennemis en se déplaçant (raccourci : A)",
 
204
  "menu.control_groups.title": "🎮 Groupes de contrôle",
205
  "menu.stats.title": "📈 Statistiques",
206
  "menu.stats.player_units": "Unités joueur :",
@@ -241,6 +244,7 @@ TRANSLATIONS: Dict[str, Dict[str, str]] = {
241
  "menu.sound.disable": "Désactiver le son",
242
  "notification.sound.enabled": "Son activé",
243
  "notification.sound.disabled": "Son désactivé",
 
244
  "menu.camera.zoom_in": "Zoom avant",
245
  "menu.camera.zoom_out": "Zoom arrière",
246
  "menu.camera.reset": "Réinitialiser la vue",
@@ -319,6 +323,7 @@ TRANSLATIONS: Dict[str, Dict[str, str]] = {
319
  "menu.actions.stop.tooltip": "停止選取的單位",
320
  "menu.actions.attack_move": "攻擊移動",
321
  "menu.actions.attack_move.tooltip": "移動時攻擊敵人(快捷鍵:A)",
 
322
  "menu.control_groups.title": "🎮 控制組",
323
  "menu.stats.title": "📈 遊戲統計",
324
  "menu.stats.player_units": "玩家單位:",
@@ -368,6 +373,7 @@ TRANSLATIONS: Dict[str, Dict[str, str]] = {
368
  "hud.model.download.retry": "重試下載…",
369
  "hud.model.download.done": "模型已就緒",
370
  "hud.model.download.error": "模型下載失敗",
 
371
  },
372
  }
373
 
 
83
  "menu.actions.stop.tooltip": "Stop selected units",
84
  "menu.actions.attack_move": "Attack Move",
85
  "menu.actions.attack_move.tooltip": "Attack enemies while moving (hotkey: A)",
86
+ "menu.actions.restart": "Restart",
87
  "menu.control_groups.title": "🎮 Control Groups",
88
  "menu.stats.title": "📈 Game Stats",
89
  "menu.stats.player_units": "Player Units:",
 
133
  "hud.model.download.retry": "Retrying download…",
134
  "hud.model.download.done": "Model ready",
135
  "hud.model.download.error": "Model download failed",
136
+ "game.draw.banner": "Draw!",
137
  },
138
  "fr": {
139
  "game.window.title": "RTS Minimaliste",
 
203
  "menu.actions.stop.tooltip": "Arrêter les unités sélectionnées",
204
  "menu.actions.attack_move": "Attaque en mouvement",
205
  "menu.actions.attack_move.tooltip": "Attaquer les ennemis en se déplaçant (raccourci : A)",
206
+ "menu.actions.restart": "Recommencer",
207
  "menu.control_groups.title": "🎮 Groupes de contrôle",
208
  "menu.stats.title": "📈 Statistiques",
209
  "menu.stats.player_units": "Unités joueur :",
 
244
  "menu.sound.disable": "Désactiver le son",
245
  "notification.sound.enabled": "Son activé",
246
  "notification.sound.disabled": "Son désactivé",
247
+ "game.draw.banner": "Match nul !",
248
  "menu.camera.zoom_in": "Zoom avant",
249
  "menu.camera.zoom_out": "Zoom arrière",
250
  "menu.camera.reset": "Réinitialiser la vue",
 
323
  "menu.actions.stop.tooltip": "停止選取的單位",
324
  "menu.actions.attack_move": "攻擊移動",
325
  "menu.actions.attack_move.tooltip": "移動時攻擊敵人(快捷鍵:A)",
326
+ "menu.actions.restart": "重新開始",
327
  "menu.control_groups.title": "🎮 控制組",
328
  "menu.stats.title": "📈 遊戲統計",
329
  "menu.stats.player_units": "玩家單位:",
 
373
  "hud.model.download.retry": "重試下載…",
374
  "hud.model.download.done": "模型已就緒",
375
  "hud.model.download.error": "模型下載失敗",
376
+ "game.draw.banner": "平手!",
377
  },
378
  }
379
 
static/game.js CHANGED
@@ -204,10 +204,38 @@ class GameClient {
204
  case 'notification':
205
  this.showNotification(data.message, data.level || 'info');
206
  break;
 
 
 
207
  default:
208
  console.log('Unknown message type:', data.type);
209
  }
210
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
 
212
  sendCommand(command) {
213
  if (this.ws && this.ws.readyState === WebSocket.OPEN) {
 
204
  case 'notification':
205
  this.showNotification(data.message, data.level || 'info');
206
  break;
207
+ case 'game_over':
208
+ this.onGameOver(data);
209
+ break;
210
  default:
211
  console.log('Unknown message type:', data.type);
212
  }
213
  }
214
+
215
+ onGameOver(payload) {
216
+ // Show overlay with localized message
217
+ const overlay = document.getElementById('game-over-overlay');
218
+ const msgEl = document.getElementById('game-over-message');
219
+ const btn = document.getElementById('game-over-restart');
220
+ if (overlay && msgEl && btn) {
221
+ // Prefer server-provided message; fallback by winner
222
+ let message = payload.message || '';
223
+ if (!message) {
224
+ const key = payload.winner === 'player' ? 'game.winner.player' : payload.winner === 'enemy' ? 'game.winner.enemy' : 'game.draw.banner';
225
+ if (payload.winner === 'draw') {
226
+ message = this.translate('game.draw.banner');
227
+ } else {
228
+ const winnerName = this.translate(key);
229
+ message = this.translate('game.win.banner', { winner: winnerName });
230
+ }
231
+ }
232
+ msgEl.textContent = message;
233
+ // Localize button
234
+ btn.textContent = this.translate('menu.actions.restart') || 'Restart';
235
+ btn.onclick = () => window.location.reload();
236
+ overlay.classList.remove('hidden');
237
+ }
238
+ }
239
 
240
  sendCommand(command) {
241
  if (this.ws && this.ws.readyState === WebSocket.OPEN) {
static/index.html CHANGED
@@ -235,6 +235,16 @@
235
  <p>Connecting to server...</p>
236
  </div>
237
  </div>
 
 
 
 
 
 
 
 
 
 
238
  </div>
239
 
240
  <script src="/static/sounds.js"></script>
 
235
  <p>Connecting to server...</p>
236
  </div>
237
  </div>
238
+
239
+ <!-- Game Over Overlay -->
240
+ <div id="game-over-overlay" class="hidden">
241
+ <div class="game-over-content">
242
+ <h2 id="game-over-message">Game Over</h2>
243
+ <div class="game-over-actions">
244
+ <button id="game-over-restart">Restart</button>
245
+ </div>
246
+ </div>
247
+ </div>
248
  </div>
249
 
250
  <script src="/static/sounds.js"></script>
static/styles.css CHANGED
@@ -6,6 +6,38 @@
6
  margin: 6px 0 8px;
7
  font-size: 12px;
8
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  #production-source .label { opacity: 0.8; }
10
  #production-source-name { font-weight: 700; }
11
  #production-source-clear {
 
6
  margin: 6px 0 8px;
7
  font-size: 12px;
8
  }
9
+
10
+ /* Game Over Overlay */
11
+ #game-over-overlay {
12
+ position: fixed;
13
+ inset: 0;
14
+ background: rgba(0,0,0,0.75);
15
+ display: flex;
16
+ align-items: center;
17
+ justify-content: center;
18
+ z-index: 9999;
19
+ }
20
+ #game-over-overlay.hidden { display: none; }
21
+ .game-over-content {
22
+ background: #111;
23
+ color: #fff;
24
+ padding: 24px 28px;
25
+ border-radius: 8px;
26
+ box-shadow: 0 8px 24px rgba(0,0,0,0.4);
27
+ min-width: 320px;
28
+ text-align: center;
29
+ }
30
+ .game-over-content h2 { margin: 0 0 12px; font-size: 20px; }
31
+ .game-over-actions { display: flex; gap: 12px; justify-content: center; }
32
+ .game-over-actions button {
33
+ background: #2d7ef7;
34
+ color: #fff;
35
+ border: none;
36
+ border-radius: 6px;
37
+ padding: 8px 12px;
38
+ cursor: pointer;
39
+ }
40
+ .game-over-actions button:hover { background: #226ad1; }
41
  #production-source .label { opacity: 0.8; }
42
  #production-source-name { font-weight: 700; }
43
  #production-source-clear {