@ecmaservegames/host

A host server for a game.

Usage no npm install needed!

<script type="module">
  import ecmaservegamesHost from 'https://cdn.skypack.dev/@ecmaservegames/host';
</script>

README

EcmaserveGames/host โฌ›

EcmaserveGames is intended to be an expressive ecosystem for building web-based board(-like) games.

This is a host server for building your game application on.

Installation

npm install <TBD>

Running the Host

You will need to create your own entry file for Node.JS.

/// index.js
const { GameServer } = require('<TBD>')
const game = new GameServer().run()

Core Concepts

  • Game State

    This core concept is inspired by uni-direction data flow such as React or Flux. Any particular game instance has a structured game state that can be serialized and transmitted from the host to any connected participant (either as a player or observer).

    There is a corresponding socket address to receive updates on the game state. i.e. - https://<hostname>/games/<game_instance_id>/state

    As noted, game-state is not intended to be computed by the client so it relies on other mechanisms to update and mutate as the game is played.

    EcmaserveGames does not prescribe a game state definition and allows you to build it, as needed. The supported format for describing your state definition is Protocol Buffers or protobuf and requires a singular State message. ๐Ÿ”— Read More about Protocol Buffers

    /// state.proto
    
    package testgame;
    syntax = "proto3";
    message State { /// <-- Required name
      string current_player_id = 2;
      int32 current_player_roll_count = 3;
    }
    

    Loading these State messages is done as part of the host's builder pattern. You will need to supply a filename, packageName and optionally an initial game state for any created game instances.

    /// index.js
    const { GameServer } = require('<TBD>')
    const path = require('path')
    
    const game = new GameServer()
      .useState(
        // A filename
        path.resolve(__dirname, './state.proto'),
        // Your protobuf package name
        'testgame',
        // [OPTIONAL] An initial game state
        {
          currentPlayerId: "player-1",
          currentPlayerRolCount: 0
        }
      )
      .run()
    
  • Authentication

    Authentication is handled via middleware. It is not recommended to write a custom authentication method. EcmaserveGames/host expects the middleware context to supply the identified user as an object at <middleware_context>.state.user.

    If you are using Bearer tokens, it is recommended to use koa-jwt since the host is built using koa's middleware server.

    /// index.js
    const { GameServer } = require('<TBD>')
    const jwt = require('koa-jwt)
    
    const game = new GameServer()
    game
      .useAuthentication(jwt({ secret: '<JWT_SECRET_KEY>' }))
      .run()
    
  • Actions

    Actions are the trigger for updating game state. There are not pre-defined actions so you are free to create your own Actions defintion.

    Like Game State, Protocol Buffers are the supported means of defining your game's actions.

    However, to properly route rules and game mechanics, Actions definition must be in the following well-known structure.

    /// actions.proto
    
    package testgame;
    syntax = "proto3";
    
    message Actions {
      oneof Action {
        ... /// any keyed messages that become your action set
      }
    }
    

    Loading the Actions message is done as part of the host's builder pattern. You will need to supply a filename and packageName.

    /// index.js
    const { GameServer } = require('<TBD>')
    const path = require('path')
    
    const game = new GameServer()
      .useActions(
        // A filename
        path.resolve(__dirname, './actions.proto'),
        // Your protobuf package name
        'testgame',
      )
      .run()
    
  • Mechanics

    Mechanics are methods that manipulate game state when an action is triggered. They are not evaluated if the rules cannot be validated. They also do not require a return and can perform async operations.

    // CustomGameMechanic.js
    const { Mechanic } = require('<TBD>')
    
    const DoFoo = Mechanic
      .create('do_foo')
      .forActions('foo')
      .use(
        ({ 
          actions, // ๐Ÿ“ฉ The Actions Message received
          gameState // ๐ŸŽฒ The GameState
        }) => { 
          /// ๐Ÿฆ  Have fun mutating your gameState freely!
        })
    
    modules.exports = {
      DoFoo
    }
    

    They are added to the game host like such:

    /// index.js
    const { GameServer } = require('<TBD>')
    const { DoFoo } = require('./CustomGameMechanic')
    
    const game = new GameServer()
      .useMechanics(DoFoo)
      .run()
    
  • Rules

    Rules are methods that can validate if an action should be allowed and may manipulate the game state if that action is allowed.

    These are the real guts of the application and offer a great deal of flexibility.

    // GoldenRule.js
    const { Rule } = require('<TBD>')
    
    const GoldenRule = Rule
      .create('golden')
      .forActions('doOntoOther')
      .use(
        ({ 
          actions, // ๐Ÿ“ฉ The Actions Message received
          gameState, // ๐ŸŽฒ The current/unmutated GameState
          ruleResults, // ๐Ÿงช The results of previousl evaluated rules
          mutate, // ๐Ÿฆ  Register a mutation if the rules pass
          exeptionTo, // ๐Ÿ”€ A callback to remove another rule validation
                      // result. Keep in mind that the order of 
                      // adding rules is the order of rule evaluation.
          user // ๐Ÿ‘ค The user provided from authentication middleware
        }) => { 
          
        })
    
    modules.exports = {
      GoldenRule
    }
    

    Rules are added to the game host like such:

    /// index.js
    const { GameServer } = require('<TBD>')
    const { GoldenRule } = require('./GoldenRule')
    
    const game = new GameServer()
      .useRules(GoldenRule)
      .run()
    
  • Game State Masks

    Game state masks allow you to selectively hide parts of the game state to a connected participant. They are applied to each individual participant after a game state update has been triggered by an action and are not persisted in any way.

    // PlayerHand.js
    
    const hideOtherPlayerHands = 
      ({ 
        user, // ๐Ÿ‘ค The participant provided from authentication middleware
        mutate // ๐Ÿฆ  A callback to reveal the game state being sent to a participant
      }) => {
        mutate(state => {
          state.playerHands = state.playerHands.filter(
            hand => {
              return hand.playerId === user.playerId
            })
        })
      }
    
    modules.exports = {
      hideOtherPlayerHands
    }
    

    Game State masks are added to the game host like such:

    /// index.js
    const { GameServer } = require('<TBD>')
    const { hideOtherPlayerHands } = require('./PlayerHand')
    
    const game = new GameServer()
      .useStateMask(hideOtherPlayerHands)
      .run()