Nel corso dello sviluppo mi sono occupato principalmente dell’implementazione delle seguenti features:
Gli ultimi tre punti sono stati sviluppati prevalentemente in autonomia mentre il primo punto è stato fatto in collaborazione con Alessandra Versari.
I due mini-giochi che ho sviluppato usano due tipi diversi di domanda generata. Infatti, Count Words genera domande del tipo SimpleTextQuestion mentre Colored Count genera domande del tipo ColoredCountQuestion.
Nel caso di Count Words, le domande vengono generate aumentando il numero di parole da contare.
override protected def correctAnswer(question: SimpleTextQuestion): Int =
question.text.split("\\s+").count(_.nonEmpty)
override def generateQuestion: (MiniGameLogic[SimpleTextQuestion, Int, Boolean], SimpleTextQuestion) =
val minRand = math.max(1, difficulty - 1)
val numOfWords =
if difficulty <= MIN_DIFFICULTY then MIN_NUMBER_WORDS + Random.between(0, difficulty + 1)
else MIN_NUMBER_WORDS + Random.between(minRand, difficulty + 1)
val wordsGenerated = Seq.fill(numOfWords)(WORDS(Random.nextInt(WORDS.size))).mkString(" ")
val question = SimpleTextQuestion(wordsGenerated)
advance(question)
Nel caso di Colored Count, invece, vengono aumentati i numeri mostrati.
override protected def correctAnswer(question: ColoredCountQuestion): Int =
question.numbersWithColor.count((_, c) => c == question.colorRequired)
override def generateQuestion: (MiniGameLogic[ColoredCountQuestion, Int, Boolean], ColoredCountQuestion) =
val totalNumbers = MIN_NUMBERS + difficulty * MULT_DIFFICULTY
val numbers = Seq.fill(totalNumbers)(Random.between(MIN_POSSIBLE_NUMBER, MAX_POSSIBLE_NUMBER))
val colorList = Seq.fill(totalNumbers)(
ColoredCountColors.values(Random.nextInt(ColoredCountColors.values.length))
)
val zipped = numbers zip colorList
val questionColor = ColoredCountColors.values(Random.nextInt(ColoredCountColors.values.length))
val question = ColoredCountQuestion(zipped, questionColor)
advance(question)
Per implementare l’algoritmo di calcolo dell’età è stato creato un oggetto Singleton che funzionasse come “helper”. Questo algoritmo prende in ingresso una lista (immutabile) di QuestionResult il quale contiene i tempi di risposta e la correttezza delle risposte dell’utente. Il risultato viene calcolato partendo da un’età base (20 anni) a cui vengono sommate delle penalità per tempo ed errori.
case class QuestionResult (responseTime: Long, isCorrect: Boolean)
object BrainAgeCalculator:
private val BASE_AGE = 20
private val MAXIMUM_AGE = 100
private val SECONDS_UNITY = 1000
private val ERROR_PERCENT = 50
def calcBrainAge(results: List[QuestionResult]): Int =
if results.isEmpty then return MAXIMUM_AGE
val avgTime = results.map(_.responseTime).sum.toDouble / results.length
val errorRate = results.count(!_.isCorrect).toDouble / results.length
val timePenalty = (avgTime / SECONDS_UNITY).toInt
val errorPenalty = (errorRate * ERROR_PERCENT).toInt
val result = baseAge + timePenalty + errorPenalty
if result <= MAXIMUM_AGE then result else MAXIMUM_AGE
Il controller rappresenta il punto centrale del progetto. Infatti, si tratta dell’entità che coordina la grafica e la logica nel pieno spirito MVC. Per fare ciò, è stato implementata una case class GameController la quale ha i seguenti campi:
Una prima difficoltà incontrata è stata riguardo alla collezione delle logiche dei mini-giochi. Infatti, in Scala, non è possibile riempire una collezione con trait che usano i generics i quali vengono implementati in maniere differenti. Per ovviare a questo problema, non è stata creata una collezione di MiniGameLogic bensì è stata creata una collezione di MiniGames, ovvero di tipi definiti nell’enum che rappresenta i mini-giochi.
enum MiniGames(val displayName: String):
case FastCalc extends MiniGames("Fast Calc")
case CountWords extends MiniGames("Count Words")
case RightDirections extends MiniGames("Right Directions")
case ColoredCount extends MiniGames("Colored Count")
case WordMemory extends MiniGames("Word Memory")
In questo modo, i mini-giochi vengono poi creati tramite una factory.
private val miniGamesFactory: Map[MiniGames, () => MiniGameLogic[_, _, _]] = Map(
FastCalc -> (() => FastCalcLogic(FAST_CALC_TURNS)),
CountWords -> (() => CountWordsLogic(COUNT_WORDS_TURNS)),
RightDirections -> (() => RightDirectionsLogic(MAX_NUMBER_OF_ROUNDS)),
ColoredCount -> (() => ColoredCountLogic(COLORED_COUNT_TURNS)),
WordMemory -> (() => WordMemoryLogic(WORD_MEMORY_TURNS))
)
def chooseCurrentGame(miniGame: MiniGames): GameController =
this.copy(currentGame = miniGamesFactory.get(miniGame).map(m => (m.apply(), miniGame)))
Inoltre, dato che per il training vengono chiesti dei risultati riguardo alle risposte dell’utente, è stato implementato una extension con i metodi utili.
extension (results: List[utils.QuestionResult])
def correctAnswers: Int = results.count(_.isCorrect)
def wrongAnswers: Int = results.count(!_.isCorrect)
def totalTimeInSeconds: Int = (results.map(_.responseTime).sum / SECONDS_UNITY).toInt
Infine, sono stati implementati i metodi per la scelta del nuovo mini-gioco, la generazione della domanda e il controllo della risposta.
def nextGame: GameController =
if numMiniGamesPlayed == MAX_NUMBER_OF_MINIGAMES_AGE_TEST then
val finalController = this.copy(currentGame = None)
viewCallback.foreach(_.onGameFinished(finalController))
finalController
else
val nextMiniGame = remainingMiniGames(Random.nextInt(remainingMiniGames.size))
this.copy(
currentGame = miniGamesFactory.get(nextMiniGame).map(m => (m.apply(), nextMiniGame)),
remainingMiniGames = remainingMiniGames.filterNot(_ == nextMiniGame),
numMiniGamesPlayed = numMiniGamesPlayed + 1
)
def getQuestion: (GameController, Question) =
val (updatedLogic, generatedQuestion) = currentGame.get._1.generateQuestion
val updatedController = this.copy(
currentGame = Some((updatedLogic, currentGame.get._2)),
startTime = Some(System.currentTimeMillis())
)
(updatedController, generatedQuestion)
def checkAnswer(answer: String): Option[(GameController, Boolean)] =
for
game <- currentGame
start <- startTime
yield
val elapsedTime = System.currentTimeMillis() - start
val isAnswerCorrect = game._1.parseAnswer(answer) match
case Some(parsedAnswer) =>
game._1.validateAnswer(parsedAnswer) match
case b: Boolean => b
case d: Double => d >= PERCENT_ACCETTABLE_ANSWER
case _ => false
case _ => false
val updatedController = this.copy(
currentGame = Some(game),
results = utils.QuestionResult(elapsedTime, isAnswerCorrect) :: results
)
(updatedController, isAnswerCorrect)
In questo ultimo metodo, checkAnswer, le risposte non previste (stringhe vuote, input non numerici…) vengono trattate direttamente come risposte errate, andando così a gestire eventuali eccezioni.