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 di 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.