Vai al contenuto
Nicola Sabaini

Progetti

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
Una partita di ExaWar in corso: la griglia esagonale con le unità schierate, il pannello delle risorse e la barra dei turni

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.

Il World Forge: il generatore di mappe, con i parametri geografici e le condizioni di vittoria
Aria, l'assistente della console di gioco