Rattini Emiliano
Nella mia parte mi sono concentrato in particolare sulla rappresentazione dello spazio, sull’implementazione della fase di act e sull’integrazione delle tre fasi successive.
Space
Oltre allo spazio già discusso è stato aggiunto un extension method per applicare il movimento alla posizione, modificandola sommandole la direzione moltiplicata per la velocità. Ho usato il simbolo + per la sua intuitività. Fa parte della SpaceSyntax
extension (p: Position)
@targetName("applyMovement")
def +(m: Movement): Position = calculateMovedPosition(p, m)
Act
Per Act si intende la fase in cui le azioni di gioco (prendere la palla, tirare, muoversi) avvengono, modificando lo stato della partita e ritornando un evento opzionale. Il comportamento è il seguente:
def actStep: State[Match, Option[Event]] =
State(state => {
val updatedState = state
.applyIf(existsSuccessfulTackle)(_.tackleBallCarrier())
.applyIf(isPossessionChanging)(_.updateBallPossession())
.updateMovements()
.moveEntities()
(updatedState, updatedState.detectEvent())
})
Ho aggiunto un metodo condizionale che applica la funzione al Match solo se viene soddisfatto un predicato, per aumentare la leggibilità del flusso. I 4 momenti della fase di act sono stati organizzati nel seguente modo:
- Tackling - Se un tackle è avvenuto con successo, tolgo la palla al portatore e lo fermo per qualche giro
- PossessionChange - Se il possesso sta cambiando, ovvero qualcuno sta prendendo la palla, do la palla a chi la sta prendendo e aggiorno il flag del possesso dentro la squadra; è necessario non farlo sempre perchè avrei situazioni in cui nessuna delle due squadre è in possesso palla, creando problemi con i comportamenti
- MovementsUpdate - Aggiorno i movimenti di tutte le entità, risolvendo le altre azioni del player
- PositionsUpdate - Aggiorno le posizioni applicando il movimento
Da qui, si sono rese necessarie 5 azioni per gestire correttamente le varie casistiche:
Move: composta da direzione e velocità, muove il giocatore ed eventuamente la palla se il giocatore è in possessoHit: composta da direzione e velocità, cambia movimento alla palla e la toglie al giocatore in possessoTake: assegna la palla al giocatoreInitial: azione nulla, data alla creazione del giocatoreStopped: ferma il giocatore per un numero di giri
Le implementazioni dei metodi precedenti sono state raggruppate in ActionProcessor come extension methods. Ecco tre esempi:
extension (state: Match)
def updateMovements(): Match =
val carrier = state.players.find(_.hasBall)
val teams = state.teams.map(_.processActions())
val ball = state.ball.updateMovement(carrier)
state.copy(teams = teams, ball = ball)
extension (player: Player)
def processAction(): Player = player.nextAction match
case Hit(_, _) =>
player.copy(movement = Movement.still, ball = None, nextAction = Stopped(MatchConfig.stoppedAfterHit))
case Move(direction, speed) => player.copy(movement = Movement(direction, speed))
case Stopped(duration) => player.copy(movement = Movement.still)
case Take(ball) => player.copy(movement = Movement.still)
case _ => player
extension (ball: Ball)
def updateMovement(carrier: Option[Player]): Ball = carrier match
case Some(Player(_, _, _, _, _, Hit(direction, speed))) =>
ball.copy(movement = Movement(direction, speed))
case Some(Player(_, position, _, _, _, Move(direction, speed))) =>
val movement = Movement(direction, speed)
val newPosition = position + (movement * (UIConfig.ballSize / 2))
ball.copy(position = newPosition.clampToField, movement = movement)
case Some(Player(_, position, movement, _, _, Take(ball))) =>
ball.copy(position = position + (movement * (UIConfig.ballSize / 2)), movement = movement)
case _ => ball
In quest’ultimo metodo c’è un piccolo workaround per non fare uscire la palla dal campo essendo essa davanti al giocatore e potendo il giocatore raggiungere il bordo. Qualche test usando AnyFlatSpec e Matchers:
"A player" should "gain possession of the ball if he's taking it" in:
val ball = Ball(Position(0, 0))
val player = Player(0, Position(0, 0), ball = Some(ball), nextAction = Take(ball))
player.updateBallPossession().ball should be(Some(ball))
it should "move when he has to" in:
val player = Player(0, Position(0, 0), nextAction = Move(defaultDirection, defaultSpeed))
player.processAction().movement should be(Movement(defaultDirection, defaultSpeed))
it should "stand still if he is stopped" in:
val player = Player(0, Position(0, 0), nextAction = Stopped(1))
player.processAction().movement should be(Movement.still)
it should "move correctly" in:
val initialPosition = Position(0, 0)
val initialMovement = Movement(defaultDirection, defaultSpeed)
val player = Player(0, initialPosition, movement = initialMovement)
player.move().position should be(initialPosition + initialMovement)
Update Flow
Come si può aver notato dal primo snippet sul comportamento della fase di Act, le varie fasi del ciclo di Update sono state modellate come variazioni di stato usando lo State del laboratorio 4.
case class State[S, A](run: S => (S, A))
In questo caso il valore prodotto viene usato solo nella Act, che ritorna un Event che rappresenta l’evento di goal, uno per squadra, e l’evento di palla uscita.
enum Event:
case BallOut, GoalEast, GoalWest
def update(state: Match): Match =
val updateFlow: State[Match, Option[Event]] =
for
_ <- decideStep
_ <- validateStep
event <- actStep
yield event
val (updated, event) = updateFlow.run(state)
handleEvent(updated, event)
L’evento di goal viene gestito aumentato lo Score dentro lo stato mentre quello di BallOut attraverso la modifica del movimento della palla con un rimbalzo.
import Event.*
private def handleEvent(state: Match, event: Option[Event]): Match =
event match
case Some(BallOut) =>
val bounceType = state.ball.position getBounce (fieldWidth, fieldHeight)
state.copy(ball = state.ball.copy(movement = state.ball.movement getMovementFrom bounceType))
case Some(GoalEast) =>
println("East Goal!!!")
SituationGenerator.kickOff(state.score.eastGoal, West)
case Some(GoalWest) =>
println("West Goal!!!")
SituationGenerator.kickOff(state.score.westGoal, East)
case _ => state