Last time on Tic-tac-Jolie
Nella seconda parte della serie avevamo:
- affrontato (in prima battuta) la definizione di dati (tipi) in Jolie;
- elencato i principali tipi di dati utilizzati nelle interfacce;
- introdotto il concetto di porta e la sua giustificazione teorica;
- elencato e commentato le porte utilizzate;
- introdotto il listato completo dei servizio gestione scacchiera.
In questo nuovo appuntamento, vedremo la gestione della scacchiera e le operazioni necessarie per cominciare la partita vera e propria.
Scacchiere (giochi) disponibili e inizio gioco: operazioni startGame, listGames
Le due operazioni con cui interagiamo inizialmente con il gestore di schacchiere sono startGame e listOpenGames.
startGame(request) (response)
inizia il gioco sulla scacchiera passata in ingresso.
Invece,
listOpenGames(request) (response)
restituisce l’elenco delle scacchier in cui c’è già un giocatore in attesa di un avversario.
Vediamo ora il codice un po’ più in dettaglio.
listOpenGames
[ listOpenGames( request )( response ) {
request è una variabile creata dinamicamente di tipo dati ListOpenGamesRequest; response è una variabile creata dinamicamente di tipo dati ListOpenGamesResponse.
count = 0;
Dichiaro la variabile count e la inizializzo a 0. Dichiarazione dinamica di una variabile il cui tipo a tempo di esecuzione è di tipo int. Lo scope di vita della variabile è l’operazione listOpenGames. Al termine di essa, la variabile cesserà di esistere.
C’è un aspetto da notare: il simbolo “;” non denota una terminazione di istruzione, ma indica che le istruzioni sono eseguite sequenzialmente. In generale “;” indica la composizione sequenziale di due behaviour: il behaviour successivo sarà eseguito solo al termine del codice di quello precedente. Tale simbolo non è necessario se poniamo le operazioni su due linee diverse, poiché il fine linea viene interpretato come “;”.
foreach( g : global.games ) {
Troviamo qui una vecchia conoscenza di altri linguaggi imperativi: si scandisce un array (global.games), iterando su di esso ordinatamente.
Il valore corrente è associato alla variabile g che rappresenta l’identificatore della scacchiera (token). global.games fa riferimento alla variabile globale games (come si vedrà negli approfondimenti).
Ogni nodo nei tipi di dati definiti è potenzialmente un array. Infatti, se non definiamo nessun range — quante volte il dato può apparire — viene assunto [1,1]. Vale a dire che la variabile è univoca, come si vedrà negli approfondimenti. Facendo un esempio, x.a equivale a una variabile con molteplicità [1,1] ed equivale ad accedere a un array con un solo elemento = x.a[0].
response.game_token[ count ] = g;
Ricordiamoci che response ha come tipo dati ListOpenGamesResponse, che definisce al suo interno un campo games opzionale che può comparire tra zero e un numero infinito di volte. respons.game_token[count] assegna all’array game_token nella posizione count il valore di g. Se la posizione count non esisteva, viene creata.
count++
}
}]
Incremento la prossima posizione in cui memorizzare il nome della scacchiera disponibile.
Il gestore delle scacchiere riceve una richiesta di inizio gioco su una determinata scacchiera. Si presentano due casi:
- la scacchiera esiste, quindi possiamo dare avvio al gioco tra i due avversari;
- la scacchiera non esiste, pertanto creiamo la scacchiera tra quelle esistenti e assegniamo un ruolo (testa, croce) al chiamante, che rimarrà poi in attesa.
startGame
Passiamo ora a commentare il codice inerente a startGame.
[ startGame( request )( response ) {
new_game = false;
if ( !is_defined( global.games.( request.game ) ) ) {
new_game = true;
is_defined: Jolie è un linguaggio con tipi dinamici, come abbiamo visto; si rende necessario a tempo di esecuzione verificare se esista nel dizionario delle variabili la variabile che desideriamo utilizzare.
global.games.(request.game): se request.game = xyz, allora global.games.(request.game) fa riferimento a global.games.xyz. Nel nostro caso verifichiamo se tra le scacchiere definite globalmente vi è la scacchiera specificata nel messaggio in ingresso.
Questo ramo è eseguito se ci troviamo di fronte ad una richiesta di giocare su una nuova scacchiera: mancando request.game tutto il path di riferimento alla variabile risulterà a sua volta non definito.
token = new;
Alla variabile token viene associato un nuovo valore, generato automaticamente dal sistema. Esso rappresenterà l’identificativo della scacchiera.
global.games.( token ) = true;
global.games.( token ).circle_participant = new;
global.games.( token ).circle_participant.location
= request.user_location;
global.games.( token ).cross_participant = new;
Se token = xyz, inizializziamo i dati associati alla schacchiera. In notazione XML:
<global>
<games>
<xyz>
true
<cicle_participant>
id univoco per il giocatore che usa “O”
<location>
ubicazione del giocatore sulla rete
</location>
</circle_participant>
<cross_participant>
id univoco per il giocatore che usa “X”
</cross_participant>
</xyz>
</games>
</global>
Non essendoci ancora un altro avversario, non posso inizializzare la sua ubicazione sulla rete.
response.game_token = token;
response.role_token
= global.games.( token ).circle_participant;
response.role_type = “circle”
si }
Si restituiscono al chiamante le informazioni necessarie: il nome della scacchiera (token), l’id univoco del giocatore alla scacchiera (circle_participant), il tipo di simbolo usato (“circle”).
Come si vede, il primo giocatore assume come simbolo il cerchio.
else {
response.game_token = request.game;
response.role_token
= global.games.( request.game ).cross_participant;
global.games.( request.game ).cross_participant.location
= request.user_location;
response.role_type = “cross”
}
}]
È la situazione in cui arriva un avversario su una scacchiera con un giocatore che attende. Restituisce: il nome della scacchiera (che è quello presente nella richiesta) e il token che identifica il giocatore “croce” (cross) e che avevamo definito nella prima chiamata a startGame dal giocatore 1. Inoltre memorizzo l’ubicazione del giocatore 2 sulla rete
{
if ( !new_game ) {
A questo punto il messaggio di risposta è già stato inviato al chiamante l’operazione, ma il servizio continua l’esecuzione del codice posto tra le graffe successive alle parentesi quadre.
Ricordiamo la sintassi di un input choice con risposta:
[ op (msg)(resp) { behaviour} ] { behaviour}
L’ultimo blocco ha una funzione simile al finally di try {}catch{} in Java. Esso viene eseguito al termine dell’eventuale blocco interno alle parentesi quadre.
Notiamo che lo scope è ancora quello dell’eventuale blocco interno; infatti facciamo riferimento a new_game per verificare se si tratta di un nuovo gioco. Se non si tratta di un nuovo gioco, ossia abbiamo già i due contendenti assieme, sarà arrivato il momento di dare fuoco alle polveri.
with( initiate_request ) {
.game_token = request.game;
.circle_participant
= global.games.(
request.game ).circle_participant;
.circle_participant.location
= global.games.(
request.game ).circle_participant.location;
.cross_participant = global.games.(
request.game ).cross_participant;
.cross_participant.location = global.games.(
request.game ).cross_participant.location
};
with (name) permette di far riferimento al nome di una variabile — ricordiamoci che solitamente essa è un albero — come radice di seguenti path di accesso ai nodi figli. Quindi invece che scrivere:
x.y.z = 4
x.c = 5
scriveremo
with (x) {
y.z = 4
c = 5
}
Il codice inizializza sostanzialmente una struttura dati che è identificata con una radice di nome initiate_request e che ha al suo interno i dati identificativi del gioco copiati da global.games.
initiateGame@MySelf( initiate_request );
Chiamo l’operazione (interna) initiateGame tramite la porta MySelf, che ha associata l’interfaccia InternalInterface. Essendo un’operazione one-way è non bloccante e il codice continua nell’esecuzione.
undef( global.games.( request.game ) )
}
Rimuovo l’identificativo della scacchiera tra quelle che attendono un avversario (rimozione dinamica di una variabile interna a global.games).
La gestione della partita vera e propria: operazione initiateGame
In Jolie abbiamo il concetto di procedura, ma questa non ha variabili locali né parametri formali associati.
Come eliminiamo l’impasse? Semplicissimo! Definiamo un’operazione! Pensiamoci bene: una procedura o funzione non sono altro che versioni statiche di un’operazione, ben piantate dentro il nostro codice monolite e non spostabili esternamente se non con grandi difficoltà.
Ora la loro naturale estensione è di essere un’operazione dentro un servizio, che nel nostro caso siamo noi stessi.
interface InternalInterface {
OneWay:
initiateGame( InitiateGameRequest )
}
In initiateGame dovremo accettare le mosse da parte dei contendenti e gestire il fatto che, essendo asincrone, potrebbero arrivare anche in ordine errato. Ecco illustrato in figura 1 quanto vogliamo ottenere.
La notazione // indica un commento in Jolie.
Commento all’operazione
Ed ecco di seguito il codice dell’operazione con relativo commento.
[ initiateGame( request ) ] {
csets.token = request.game_token;
cset.token serve per la gestione della sessione. Vedi approfondimento correlation sets (sessioni).
circle_participant = request.circle_participant;
circle_participant.location
= request.circle_participant.location;
cross_participant = request.cross_participant;
cross_participant.location
= request.cross_participant.location;
Copia i valori di circle_participant e cross_participant in due variabili locali.
for( i = 0, i < 9, i++ ) { places[i] = 0 };
Inizializza la situazione della schacchiera di gioco (variabile places di tipo array).
/* send start messages */
User.location = cross_participant.location;
usr.places -> places; usr.message
= “Wait for a move from circle player”;
usr.status_game = “stay”;
syncPlaces@User( usr );
User.location = circle_participant.location;
usr.status_game = “play”;
usr.message = “It is your turn to play”;
syncPlaces@User( usr );
Comunica ai due contendenti la situazione della scacchiera e che essi debbono prepararsi a compiere le loro mosse. Notiamo subito la peculiarità dell’uso della variabile User ove viene settato dinamicamente il nodo (campo) interno location.
/* start game */
moves = 0; circle_wins = false; cross_wins = false;
while( moves < 9 && !circle_wins && !cross_wins ) {
Inizializza il numero di mosse attualmente fatto, e i due flag che indicano quale dei due contendenti abbia vinto. Eseguiamo il loop finchè vi è un posto libero e nessuno dei due contendenti abbia vinto.
/* waiting for a move */
scope( move ) {
install( MoveNotAllowed => nullProcess );
move( mv_request )() {
A questo punto “esponiamo dinamicamente” un’operazione (move), ovvero il servizio si mette in attesa di ricevere una chiamata (messaggio) a move da parte di uno dei giocatori.
/* check the turn */
if ( (moves%2) == 0 ) {
/* circle move */
if ( mv_request.participant_token !
= circle_participant ) {
throw( MoveNotAllowed, “It is not your turn” )
moves%2 == 0 –> mossa pari, tocca al cerchio. Se la mossa viene dal giocatore che non ha il token associato al giocatore cerchio, genero l’eccezione per notificare al chiamante l’errata mossa. Questo comportamento si rende necessario perché le mosse arrivano al gestore della scacchiera in modo asincrono, quindi non necessariamente nel corretto ordine.
} else {
places[ mv_request.place ] = 1;
User.location = circle_participant.location;
usr.places -> places; usr.message
= “Wait for a move from cross player”;
usr.status_game = “stay”;
syncPlaces@User( usr );
User.location = cross_participant.location;
usr.message = “It is your turn to play”;
usr.status_game = “play”;
syncPlaces@User( usr )
}
La mossa arriva dal giocatore “cerchio”: riempio la casella con il suo simbolo, preparo la porta per la risposta, e rispondo a “cerchio” con l’istruzione di attendere (stay). Avvisa anche il giocatore “croce” di effettuare la sua mossa (play), naturalmente avendo prima settato nuovamente il parametro location nella porta User.
} else {
/* cross move */
if ( mv_request.participant_token !
= cross_participant ) {
throw(MoveNotAllowed, “It is not your turn”)
} else {
places[ mv_request.place ] = -1;
User.location = cross_participant.location;
usr.places -> places; usr.message
= “Wait for a move from circle player”;
usr.status_game = “stay”;
syncPlaces@User( usr );
User.location = circle_participant.location;
usr.status_game = “play”;
usr.message = “It is your turn to play”;
syncPlaces@User( usr )
}
}
}
;
Turno per giocatore “croce”: se mi trovo una mossa di “cerchio” genero l’eccezione. Inserisce il simbolo per il giocatore “croce”, invia a “croce” il messaggio di attendere e la situazione della scacchiera. Invia a “cerchio” la situazione della scacchiera e l’istruzione di effettuare la mossa.
moves++;
checkVictory
}
}
;
if ( !circle_wins && ! cross_wins ) {
usr.places -> places;
usr.status_game = “end”;
User.location = circle_participant.location;
usr.message = ““;
syncPlaces@User( usr );
User.location = cross_participant.location;
usr.message = ““;
syncPlaces@User( usr )
}
}
Incrementa il numero di mosse. checkVictory è una routine che verifica se siamo in una condizione di vittoria e setta le variabili circle_wins e cross_wins. Essendo una routine all’interno dell’operazione initiateGame, condivide con essa lo scope di vita delle varie variabili locali. Nell’appendice A.10, si approfondirà la sintassi delle routine locali.
Conclusioni
Attraverso l’analisi del listato, in questa terza parte abbiamo commentato il codice dell’operazione listOpenGames e il codice dell’operazione startGames e abbiamo introdotto il concetto di operazione interna e commentato il codice dell’operazione initiateGame. Nel prossimo numero, continueremo l’implementazione in Jolie del nostro gioco del tris, vedendo anzitutto Il codice d’implementazione del microservizio “giocatore” e parlando poi di correlation sets.
Ha cominciato con l'informatica da autodidatta, nel lontano 1982 — anni ruggenti — con il mitico Lemon II (clone di Apple II) e registratore a cassette. Ha poi conseguito una laurea vecchio ordinamento in ingegneria elettronica indirizzo informatica presso l’Università degli Studi di Bologna.
Negli anni, ha spaziato in diversi settori — assicurazione qualità, pianificazione produzione, logistica — mantenendo sempre un occhio sulla parte informatica, settore in cui ha poi rifocalizzato i suoi interessi. Lavora attualmente presso Imola Informatica, con il ruolo di software engineer.
Claudio Guidi è un ricercatore ed un imprenditore nell‘area dei microservices. Co-Leader del progetto Jolie (http://www.jolie-lang,org), ha conseguito il suo Ph.D. in computer science presso l‘Università di Bologna con una tesi sulla formalizzazione dei linguaggi per il Service Oriented Computing. Insieme a Fabrizio Montesi, l‘altro creatore di Jolie, ha fondato italianaSoftware che già oggi vende soluzioni legate alla system integration orientate ai microservizi.