Come funziona
Il gioco è composto da:
- una CLI scritta in Python che viene utilizzata dal giocatore;
- uno script di controllo (checker.py) che verifica in background le azioni del giocatore e assegna i punti;
- un server in Java (Spring Boot) che:
- espone la UI (pagine web statiche);
- riceve i dati dallo script di controllo;
- salva i risultati in un database PostgreSQL;
- invia lo stato del gioco alla UI tramite WebSocket;
- una web UI composta da HTML, CSS e vanilla JavaScript.
Ciascun livello del gioco richiede:
- un container OCI (Docker);
- uno script checker.py specifico per il livello che viene richiamato dallo script di controllo principale;
- un WebComponent che viene caricato dalla UI.
Aiuto! Perché mescolare così tante tecnologie?
Perché siamo un LUG e volevamo creare un gioco che non sia solo divertente da giocare, ma anche interessante da sviluppare e mantenere. Unendo tecnologie diverse, possiamo creare un progetto che sia ricco di sfide e opportunità di apprendimento, permettendo a vari membri dell’associazione di contribuire con il proprio expertise e di crescere insieme al progetto.
Il seguente schema raffigura la comunicazione tra i vari componenti:
Il gioco inizia quando l’utente avvia lo script start-game.py
(CLI). Lo script effettua una prima chiamata al server per verificare se lo username scelto dall’utente è disponibile, in quanto è possibile utilizzare un dato username una sola volta. Non vi è alcun meccanismo di autenticazione tra lo script e il server. Si assume che entrambi i software girino sulla stessa rete locale.
Se lo username è disponibile, lo script notifica il server che sta per iniziare una nuova partita. Il server inoltra questa informazione alla UI, tramite WebSocket.
Lo script avvia poi un container, la cui immagine dipende dal livello e dalla lingua selezionati dall’utente. Il container viene fatto partire privo di rete (--network=none
) per limitare la fantasia dell’utente.
Sul container vengono montati 3 volumi:
- il file
/home/player/.bashrc
, montato in read-only; - il file
/opt/linux-bomb/log
; - la cartella
/bomb
Il container parte con un utente non privilegiato player
e viene utilizzato il flag --userns=keep-id
per evitare problemi con i permessi all’interno dei volumi. Si assume che il container venga avviato da un utente con id 1000.
La cartella /bomb
contiene i file che rappresentano lo stato della bomba ed è quella sulla quale l’utente dovrà agire. Viene inizializzata dall’entrypoint dell’immagine base a partire dalla cartella /opt/linux-bomb/bomb
, definita dai Dockerfile di ciascun livello.
Il file .bashrc
definisce la funzionalità che permette di monitorare i comandi eseguiti dall’utente. Questa funzionalità è stata implementa sfruttando un meccanismo della bash che consente di specificare un comando all’interno di una variabile d’ambiente chiamata PROMPT_COMMAND
. Il comando contenuto in questa variabile verrà eseguito al termine di ciascun comando eseguito dall’utente.
Nel nostro caso avremmo potuto configurare PROMPT_COMMAND
per inviare il comando e il codice d’errore direttamente al server. Tuttavia, poiché abbiamo preferito disabilitare la rete all’interno del container, il nostro PROMPT_COMMAND
si limita ad accodare l’ultimo comando eseguito e il suo codice di uscita nel file /opt/linux-bomb/log
. Questo file, essendo montato su un volume esterno in fase di creazione del container, è leggibile anche dall’esterno del container, permettendo il monitoraggio. Come meccanismo aggiuntivo “anti-cheating” si potrebbe invocare chattr +a
su questo file, consentendo così solo l’operazione di append.
Lo script checker.py
verifica periodicamente il contenuto del file di log, e per ogni comando eseguito invoca uno script checker specifico per ogni livello, che si occupa di definire la logica di assegnamento dei punteggi in base ai file e alle cartelle rilevate in /bomb
. Lo script principale invia al server il risultato, applicando anche eventuali penalità per i comandi errati. Anche in questo caso non vi è alcuna forma di autenticazione con il server. Il server inoltre non effettua alcun controllo, ma si limita a fidarsi di quanto ricevuto dal checker e a memorizzare il risultato sul database. Tutte le operazioni sono poi notificate alla UI tramite WebSocket.
Lo script checker verifica anche se il container è ancora in esecuzione, terminando il gioco (con l’esplosione della bomba) se rileva che il container è stato terminato. Lo script si occupa anche di terminare il container in caso di vittoria.
Il container parte con una bash avviata dal comando timeout
, che permette di terminare automaticamente il container al raggiungimento del tempo massimo concetto per il livello.
Quando il container viene terminato parte un altro script Python (end-message.py
), che chiama le API per verificare se il gioco è terminato con una vittoria o un fallimento, in modo da visualizzare gli opportuni messaggi per l’utente.
Il container e lo script end-message.py
vengono avviati insieme all’interno di una nuova shell avviata come ultimo step dello script start-game.py
utilizzando la funzione os.execvp()
, che esegue un nuovo programma sostituendosi al processo corrente.