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