Skip to content

Game Logic (src/game-implementation)

As said, on its own the game engine doesn’t do anything but provide you a set of concepts. To put these concepts to use, you will need the following:

  • Define game objects with their actions and results
  • A way for the frontend to execute specific actions on specific game objects
  • Convert the results of these actions into something the frontend understands
  • A way to track what the user has been doing along the way

This is where the files under src/game-implementation come into play.

One key file is the services/gameService.ts. This file tells the game engine:

  • Which game objects and actions actually exist in the game world by using registerGameObject and registerAction in the constructor.
  • How user progression will be stored by providing a PlayerSession-type.

Only one instance of the GameService will be created whenever the backend application starts. This happens in the global.ts file.

Next there is routes.ts, which exposes two API endpoints: /state and /action. These are respectively handled by the functions handleStateRequest and handleActionRequest found in controllers/GameController.ts.

From a Single Responsibility perspective, handleStateRequest and handleActionRequest will only extract data from the request and return a response. The actual logic is performed by the executeAction function, which is where the actual game engine is put to work:

  • It will determine on which game object to execute the requested action. It does this by converting the alias of an game object into an actual instance. This conversion is handled by the getGameObjectsByAliases function found on the GameService. If no game object is provided, it will use the room the user is currently in based on its PlayerSession.

  • It will then use the executeAction function found on the GameService to execute the requested action on the game object. To do this, the executeAction will determine which Action-class is reponsible for handling the requested action, instance it and call the appropriate handle function. The handle function will then make sure the game object actually supports the action. If it does, it will be executed and its ActionResult returned.

  • As a last step, the action result has to be converted to a GameState, which is used to instruct the frontend what happened after executing an action. For this, the convertActionResultToGameState function is used. In this function, the type of ActionResult determines what kind of GameState is returned.

    For example: The game implementation comes with an additional ActionResult: SwitchPageActionResult. This ActionResult can be used to instruct the RootComponent on the frontend to switch to other “pages”. If this action result is used, it will return a different type of GameState.

Class Diagram

Just an impression

This class diagram gives an impression of how all the different classes link together, but is not a complete representation.

---
config:
  class:
    hideEmptyMembersBox: true
---
classDiagram
    direction LR

    class GameObject {
        <<abstract>>
        +get alias() string
        +name() SyncOrAsync<string>
    }

    class Room {
        <<abstract>>
        images() SyncOrAsync<string[]>
        actions() SyncOrAsync<Action[]>
        objects() SyncOrAsync<GameObject[]>
    }

    class Item {
        <<abstract>>
    }

    class Character {
        <<abstract>>
    }

    class Examine {
        <<interace>>
        +examine() ActionResult | undefined*
    }

    class Talk {
        <<interace>>
        +talk(choiceId?: number) ActionResult | undefined*
    }

    class Simple {
        <<interace>>
        +simple(alias: string) ActionResult | undefined*
    }

    class ActionResult {
        <<abstract>>
    }

    class TextActionResult {
        +get text() string[]
    }

    class TalkActionResult {
        +get character() Character
        +get choices() TalkChoice[]
    }

    class TalkChoice {
        +get id() number
        +get text() string
    }

    class SwitchPageActionResult {
        +get page() string
    }

    class BaseGameService~T~ {
        <<abstract>>
        #registerGameObject(gameObjectClass: GameObjectClass) void
        #registerAction(actionClass: ActionClass) void
        +createPlayerSessionMiddleware() ExpressMiddleware
        +createNewPlayerSession() T*
        +getPlayerSession() T
        +resetPlayerSession() void
        +getGameObjectsByAliases(aliases: string[]) GameObject[]
        +getGameObjectByAlias(alias: string) GameObject | undefined
        +executeAction(alias: string, gameObjects: GameObject[]) SyncOrAsync<ActionResult | undefined>
    }

    class GameService {
        +getGameObjectsFromInventory() GameObject[]
    }

    class PlayerSession {
        <<type>>
        +currentRoom string
        +inventory string[]
    }

    class Action {
        <<abstract>>
        +get alias() string 
        +name() SyncOrAsync<string>*
        +get needsObject() boolean
        +execute(alias: string, gameObjects: GameObject[]) ActionResult | undefined*
    }

    class ExamineAction {
        +readonly Alias: string = "examine"$
    }

    class TalkAction {
        +readonly Alias: string = "talk"$
    }

    class SimpleAction {
        +execute(alias: string, gameObject: GameObject) ActionResult | undefined$
    }

    class GameController {
        +handleStateRequest(_: Request, res: Response) Promise<void>
        +handleActionRequest(req: Request, res: Response) Promise<void>
        +executeAction(actionAlias: string, gameObjectAliases? string[]): Promise<GameState | undefined>
        +convertActionResultToGameState(actionResult?: ActionResult) Promise<GameState | undefined>
        -convertTalkChoiceToReference(action: TalkActionResult, choice: TalkChoice) ActionReference
        -convertActionToReference(action: Action) Promise<ActionReference>
        -convertGameObjectToReference(gameObject: GameObject) Promise<GameObjectReference>
    }

    class DefaultGameState {
        <<type>>
        +type : "default"
        +roomAlias : string
        +roomName : string
        +roomImages : string[]
        +text : string[]
        +actions : ActionReference[]
        +objects : GameObjectReference[]
    }

    class ActionReference {
        <<type>>
        +alias : string
        +name : string
        +needsObject : boolean
    }

    class GameObjectReference {
        <<type>>
        +alias : string
        +name : string
    }

    Room--|>GameObject
    Room..|>Examine
    Item--|>GameObject
    Character--|>GameObject
    Character..|>Talk

    ExamineAction--|>Action
    TalkAction--|>Action
    SimpleAction--|>Action

    TextActionResult--|>ActionResult
    TalkActionResult--|>TextActionResult
    SwitchPageActionResult--|>ActionResult

    GameService--|>BaseGameService

    GameController--GameService
    GameController--DefaultGameState

    GameService--Action
    GameService--PlayerSession

    Room--Action
    Room--GameObject

    DefaultGameState--ActionReference
    DefaultGameState--GameObjectReference

    TalkActionResult--TalkChoice

    ExamineAction--Examine
    TalkAction--Talk
    SimpleAction--Simple