Exit e-book
Show all chapters
11
Implementing the GameMode module
11. 
Implementing the GameMode module

Sign up to our Newsletter

Signing up to our newsletter allows you to read all our ebooks.

I agree to receive marketing communication from Scalac.
You can unsubscribe from these communications at any time. For more information on how to unsubscribe, view ourĀ Privacy Policy.

Mastering Modularity in ZIO with Zlayer
11

Implementing the GameMode module

And here we have the implementation of the GameMode module in mode/game/package.scala. Again, the structure of this module is pretty similar to what we did for previous modules:

  • The GameMode module Has a GameMode.Service, which is defined inside the GameMode object. And, the GameMode.Service, exposes some capabilities, which in this case are the process and render methods.
  • We have defined a live implementation.

We have capability accessors generated by the @accessible annotation: GameMode.process and GameMode.render.

type GameMode = Has[GameMode.Service]

  @accessible
  object GameMode {
    trait Service {
      def process(input: String, state: State.Game): UIO[State]
      def render(state: State.Game): UIO[String]
    }
   val live: ZLayer[GameCommandParser with GameView with OpponentAi with GameLogic, Nothing, GameMode] =
        ZLayer.fromFunction { env =>
          val opponentAiService        = env.get[OpponentAi.Service]
          val gameCommandParserService = env.get[GameCommandParser.Service]
          val gameLogicService         = env.get[GameLogic.Service]
          val gameViewService          = env.get[GameView.Service]

          new Service {
            override def process(input: String, state: State.Game): UIO[State] =
              if (state.result != GameResult.Ongoing)
                UIO.succeed(State.Menu(None, MenuFooterMessage.Empty))
              else if (isAiTurn(state))
                opponentAiService
                  .randomMove(state.board)
                  .flatMap(takeField(_, state))
                  .orDieWith(_ => new IllegalStateException)
              else
                gameCommandParserService
                  .parse(input)
                  .flatMap {
                    case GameCommand.Menu =>
                      UIO.succeed(State.Menu(Some(state), MenuFooterMessage.Empty))
                    case GameCommand.Put(field) =>
                      takeField(field, state)
                  }
                  .orElse(
                    ZIO.succeed(
                     state.copy(footerMessage = GameFooterMessage.InvalidCommand)
                    )
                  )

            private def isAiTurn(state: State.Game): Boolean =
              (state.turn == Piece.Cross && state.cross == Player.Ai) ||
                (state.turn == Piece.Nought && state.nought == Player.Ai)

            private def takeField(field: Field, state: State.Game): UIO[State] =
              (for {
                updatedBoard  <- gameLogicService.putPiece(state.board, field, state.turn)
                updatedResult <- gameLogicService.gameResult(updatedBoard)
                updatedTurn   <- gameLogicService.nextTurn(state.turn)
              } yield state.copy(
                board = updatedBoard,
                result = updatedResult,
                turn = updatedTurn,
                footerMessage = GameFooterMessage.Empty
              )).orElse(
                UIO.succeed(state.copy(footerMessage = GameFooterMessage.FieldOccupied))
              )

            override def render(state: State.Game): UIO[String] = {
              val player = if (state.turn == Piece.Cross) state.cross else state.nought
              for {
                header  <- gameViewService.header(state.result, state.turn, player)
                content <- gameViewService.content(state.board, state.result)
                footer  <- gameViewService.footer(state.footerMessage)
              } yield List(header, content, footer).mkString("\n\n")
            }
          }
       }

    // Below code is autogenerated by @accessible annotation, so we don't need to write it
    def process(input: String, state: State.Game): URIO[GameMode, State] =
      ZIO.accessM(_.get.process(input, state))

    def render(state: State.Game): URIO[GameMode, String] = ZIO.accessM(_.get.render(state))

Looking in more detail at how the live implementation is defined, we need to create a ZLayer that depends on the GameCommandParser, GameView, GameLogic and OpponentAi modules. For that, similarly to what we did when reimplementing the Terminal module, we can use ZLayer.fromFunction (we could have used ZLayer.fromEffect or ZIO#toLayer as well). ZLayer.fromFunction expects a function that takes one input (the environment containing the GameCommandParser, GameView, GameLogic and OpponentAi modules), and returns a Service (the GameMode.Service in this case). And, because GameCommandParser, GameView, GameLogic and OpponentAi are modules, we can use Has#get for accessing their capabilities.

Using ZLayer.fromFunction works, but again, in this case it is a little inconvenient that we have to use Has#get to access capabilities from the environment. The good news is that ZIO provides another very helpful function for this use case: ZLayer.fromServices, which expects a function that takes several Services as inputs, and returns another Service as output. So, the live implementation would be:

val live: URLayer[GameCommandParser with GameView with OpponentAi with GameLogic, GameMode] =
        ZLayer.fromServices[
          GameCommandParser.Service,
          GameView.Service,
          OpponentAi.Service,
          GameLogic.Service,
          GameMode.Service
        ] { (gameCommandParserService, gameViewService, opponentAiService, gameLogicService) =>
          new Service {
            override def process(input: String, state: State.Game): UIO[State] =
              if (state.result != GameResult.Ongoing) UIO.succeed(State.Menu(None, MenuFooterMessage.Empty))
              else if (isAiTurn(state))
                opponentAiService
                  .randomMove(state.board)
                  .flatMap(takeField(_, state))
                  .orDieWith(_ => new IllegalStateException)
              else
                gameCommandParserService
                  .parse(input)
                  .flatMap {
                    case GameCommand.Menu =>
                      UIO.succeed(State.Menu(Some(state), MenuFooterMessage.Empty))
                    case GameCommand.Put(field) =>
                      takeField(field, state)
                  }
                  .orElse(ZIO.succeed(state.copy(footerMessage = GameFooterMessage.InvalidCommand)))

            private def isAiTurn(state: State.Game): Boolean =
              (state.turn == Piece.Cross && state.cross == Player.Ai) ||
                (state.turn == Piece.Nought && state.nought == Player.Ai)

            private def takeField(field: Field, state: State.Game): UIO[State] =
              (for {
                updatedBoard  <- gameLogicService.putPiece(state.board, field, state.turn)
                updatedResult <- gameLogicService.gameResult(updatedBoard)
                updatedTurn   <- gameLogicService.nextTurn(state.turn)
              } yield state.copy(
                board = updatedBoard,
                result = updatedResult,
                turn = updatedTurn,
                footerMessage = GameFooterMessage.Empty
              )).orElse(UIO.succeed(state.copy(footerMessage = GameFooterMessage.FieldOccupied)))

            override def render(state: State.Game): UIO[String] = {
              val player = if (state.turn == Piece.Cross) state.cross else state.nought
              for {
                header  <- gameViewService.header(state.result, state.turn, player)
                content <- gameViewService.content(state.board, state.result)
                footer  <- gameViewService.footer(state.footerMessage)
              } yield List(header, content, footer).mkString("\n\n")
            }
          }
        }
PREVIOUS
Chapter
10
NEXT
Chapter
12