lunedì 15 marzo 2010

Bowling kata

Qualche nota sul kata del bowling in "modalità ocp".

Una esecuzione di un generico gioco del bowling è formato da una serie di frames, ognuno dei quali è composto da un certo numero di lanci. Ogni lancio è caratterizzato dal numero di birilli abbattuti.

Dunque secondo la regola che un frame generico è composto di due lanci, la sequenza di lanci {5,5}, che significa aver abbattuto 5 birilli al primo lancio e 5 al secondo, è un frame valido, mentre la sequenza {10,1} non lo è per due ragioni: una perché viene superato il totale di 10, e l'altra perché se il primo lancio è strike, il frame non può essere composto di un secondo lancio.

Quindi ogni potenziale frame è dotato di una composizione di vincoli che consentono di distinguere se il frame è valido o meno.
Come giò detto, una regola di validità è che il totale della sequenza di lanci per frame non è superiore a 10. ed un'altra è che un frame che abbia 10 al primo lancio, non ha un secondo lancio.

Tali regole vengono indicate nel codice nel seguente modo, attraverso la tecnica dei delegates. Ho definito come Constraint una funzione: che dato un frame restituisce un boolean.



Constraint sumOfAllRollMustbeLessThanTen = (x => x.Rolls.Sum() <= 10);
Constraint ifFirstRollIsTenThanTheFrameIsOver = (x => (!(x.Rolls[0] == 10) || x.Rolls.Count == 1));
Constraint ifFirstRollIsLessThanTenThenThereIsAnotherRollInTheFrame =
(x => (!(x.Rolls[0] < 10) || x.Rolls.Count == 2));


La factory, che costruisce ogni particolare bowling iniettandovi le relative regole, è il punto di partenza del kata del bowling, e compone la variante terrestre del bowling iniettando le regole di validità attraverso la chiamata: SetConstraintForFrame() cui viene passato il Constraint (dotato di una descrizione) e dell'indice di un frame.

Ciò significa che è possibile avere constraint diversi per ogni frame.
Infatti nel bowling "terrestre" i frames che vanno da 0 a 9 rispettano gli stessi constraint descritti in alto, mentre il decimo frame ha dei vincoli diversi: può essere composto fino a tre lanci, e solo se il punteggio di ognuno di essi è pari a 10.

Ciò si descrive con i seguenti constraints:


        
Constraint sumRollsNoHigherThanThirty = x => x.Rolls.Sum() <= 30;
Constraint ifFirstRollIsTenThanThereIsAtLeastAnotherRoll = x => !(x.Rolls[0]==10)||x.Rolls.Count > 1;

Constraint ifSecondRollIsTenThenThereIsAnotherRoll =
x => (!(x.Rolls.Count > 1 && x.Rolls[1] == 10) || x.Rolls.Count == 3);


(vedere nella factory)
Notare che viene usata la regola logica:
if A then B equivalente a !A || B

Oltre a determinare la validità di ogni frame, è necessario anche un modo per determinare le
regole che servono per calcolare il punteggio ed il bonus.
Solitamente il bonus è calcolato in funzione degli altri frames.
Per esempio la regola dello Spare restituisce come bonus il valore del lancio successivo al frame attuale, mentre la regola dello Strike restituisce come bonus il valore dei due lanci successivi.

Le diverse regole per ogni frame vengono valutate in ordine, e la condizione di Break indicata nella regola indica se è necessario continuare a valutare le regole successive.
Questo per evitare, per esempio, che uno Strike venga contato anche come Spare, cosa che potrebbe essere, visto che la definizione di Strike (primi 10 birilli abbattuti), è compatibile con la definizione di Spare (la somma dei due lanci abbatte 10 birilli).

Infine vi è la regola per calcolare il punteggio per frame, al quale sarà sommato il bonus, per determinare l'effettivo punteggio totale.
Il punteggio per frame è il semplice totale dei birilli abbattuti per ogni lancio all'interno del frame.

Un dilemma ancora da sciogliere è se i test unitari debbano allocare i frames o i singoli lanci.

Nel dubbio sono stati lasciati per il momento i test in entrambe le modalità.

Per esempio c'è il seguente test che, allocando esplicitamente i frames, testa il caso di giocate tutte a punteggio zero:



[Test]
public void TestAllZeroes()
{
Frame frame = new Frame(0,0);
for (int i = 0; i < 10; i++)
{
terrestrialGame.AddFrame(frame);
}
Assert.AreEqual(0, terrestrialGame.Score());
}


mentre il seguente è l'equivalente in termini di singoli lanci:



[Test]
public void TestAllZeroesByRolling()
{
for (int i=0;i<20;i++)
{
terrestrialGame.Roll(0);
}
Assert.AreEqual(0,terrestrialGame.Score());
}




La possibilità di allocare il frame è essenziale per testare i vincoli, ma una volta esposta la funzionalità e testata, è plausibile pensare di esporre al client solo il metodo "roll" e nascondere quello che si basa sull'esporre il frame, e dunque rendere il metodo che alloca il frame da pubblico a privato.
Questo significherebbe eliminare i test che si basano sui frames (accogliendo il punto di vista che i metodi privati non debbano essere testati direttamente).

Nella variante del "bowling marziano", che consiste in tre frames, ognuno costituito fino a tre lanci, ogni strike viene premiato con il risultato dell'ultimo frame.

Nessun commento:

Informazioni personali

La mia foto
I have been coding from the old C64 times. Studied Computer Sciences at Milan University. I also worked there in technical operations. Many years of experiences in coding Java and C#, desktop and web applications, with practices like unit testing. I used to play with 3d graphics in architecture recently with Blender 3d. Now I look for support related to some projects I am working on, oriented in automation in tourism related services, using functional programming framework, specifically F# and Suave.IO. email
tonyx1 (at) gmail.com github https://github.com/tonyx