ExaWar
Strategia a turni su griglia esagonale, scritta in Python. Cinque risorse, dodici tecnologie, quattordici missioni, multiplayer con server autoritativo. Gratuito e finito.
- Tipo
- Gioco
- Stato
- Rilasciato
- Linguaggio
- Python 3.12
- Grafica
- Pygame · NumPy
- Codice
- ~133k righe
- Test
- 1383
- Lingue
- 5
Un wargame a turni su griglia esagonale, ambientato in un futuro in cui le fazioni si combattono per la supremazia tecnologica. È scritto in Python 3.12 su Pygame, uscito a dicembre 2025 per Windows 10 e 11, ed è gratuito: nessun acquisto interno, nessun account, nessun seguito in lavorazione che giustifichi le mancanze.
Come si gioca
Il turno ha tre fasi rigide: produzione, azione, combattimento. In produzione il lancio dei dadi decide quali settori estraggono, e cosa estraggono dipende dal terreno. In azione muovi le unità e costruisci. In combattimento si risolve, e il terreno conta ancora: una foresta vale +2 in difesa, una collina +3.
L’economia gira su cinque flussi che vanno tenuti in equilibrio fra loro: energia per alimentare strutture e unità avanzate, materiali per costruire e riparare, dati per ricercare, cibo per le truppe umane, token per il mercato. Un deficit di energia non blocca il gioco, taglia la produzione — e te ne accorgi due turni dopo.
Le vittorie sono tre, e non sono varianti della stessa: distruggere tutti i quartier generali nemici, portare il Core AI al livello 5 e proteggerlo durante il caricamento finale, oppure prendere il controllo economico e territoriale del settore. Chi gioca per la singolarità e chi gioca per il dominio militare fa due partite diverse sulla stessa mappa.
Dentro ci sono 13 unità, 17 edifici, 12 tecnologie più 4 dottrine, 5 ere e 23 mappe. Le dottrine sono a coppie mutuamente esclusive — Sciame contro Corazzata, Logistica contro Guerra Elettronica: quando ne prendi una, l’altra è chiusa per il resto della partita.
Come è fatto
Il progetto sono 432 file Python tracciati, circa 133 000 righe. Alcune decisioni di struttura hanno retto meglio di altre, e vale la pena dire quali.
Lo stato ha un solo proprietario. engine/game_state.py è l’unico posto
dove lo stato di una partita vive, e si muta solo attraverso advance_phase()
e start_turn(). È la regola che ha impedito al gioco di diventare
ingestibile quando sono arrivati multiplayer e campagna: se qualunque modulo
potesse scrivere ovunque, sincronizzare due client sarebbe stato impossibile.
Il combattimento è server-side. resolve_combat() sta nel motore, non nel
client, e il server multiplayer è autoritativo: il client chiede, il server
decide. Usa random, quindi nei test va seminato — senza seed le partite
simulate diventano intermittenti e i test smettono di dire qualcosa.
I salvataggi sono atomici. Scrittura su file temporaneo, fsync,
replace. Un salvataggio interrotto a metà non esiste: o c’è quello vecchio o
c’è quello nuovo. E c’è una migrazione di schema, perché i salvataggi
sopravvivono alle versioni.
La localizzazione ha una sola fonte di verità, ed è un database SQLite:
assets/locales/locales.db. Cinque lingue, 1128 chiavi per lingua, zero
mancanti e zero orfane — ed è un controllo che gira nel gate prima di ogni
commit, non un buon proposito. I file di traduzione sparsi sono il modo
classico in cui un gioco multilingua marcisce: una chiave aggiunta in italiano
e dimenticata altrove, e quattro lingue mostrano una stringa grezza.
L’AI competitiva è una sola. GoalDrivenAI decide per obiettivi; le
versioni precedenti sono rimaste nel repository ma sono chiuse allo sviluppo.
L’AI della campagna è un’altra cosa ancora: conosce la missione in cui si
trova, e ogni livello da m1 a m14 ha la sua strategia registrata.
Aria, e la console
La parte che mi ha impegnato di più non è nel gameplay. Il gioco ha una console interna — circa 31 000 righe — con un filesystem virtuale isolato, tutto in memoria, e dentro ci vive Aria.
Aria capisce per significato, non per parole chiave. C’è un encoder semantico e5 in formato ONNX che confronta l’intento della frase con quello dei comandi, e funziona nelle cinque lingue del gioco senza tradurre niente. È opzionale: se il modello non c’è, Aria degrada a corrispondenza testuale invece di rifiutarsi di partire. Un gioco che non si avvia perché manca un file da qualche centinaio di megabyte non è un gioco, è un problema.
Sopra c’è il coder NEXL: un micro assistente per il linguaggio di scripting
interno. Recupera esempi per somiglianza semantica, poi compone il codice
con una grammatica invece di generarlo a caso, lo prova in una sandbox che
gira a vuoto, osserva cosa sarebbe successo e corregge. È deterministico e non
chiama eval da nessuna parte: quello che produce è NEXL valido per
costruzione, non NEXL sperato.
Multiplayer
Server autoritativo, messaggi firmati con HMAC, protezione contro i payload compressi che esplodono in decompressione. Funziona sia in rete locale, senza passare da internet, sia online attraverso un relay di lobby che gira su un piccolo server cloud — la stessa macchina che serve questo sito, e per questo il resto del sito è statico.
Quello che tiene insieme il tutto
1383 test in 128 file, girati headless in circa due minuti e venti. Prima di
ogni commit c’è un gate unico — scripts/check.py — che mette insieme il lint
con ruff, la parità delle cinque lingue, la validazione dei manifest di
campagna e la suite intera.
Non è disciplina per il gusto della disciplina: in un gioco a turni con AI, rete e salvataggi versionati, una regressione non si manifesta come un errore. Si manifesta come una partita che a un certo punto non torna più, dieci turni dopo il commit che l’ha rotta.