@ecmaservegames/host
Version:
A host server for a game.
273 lines (211 loc) โข 7.21 kB
Markdown
# 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.
```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](https://developers.google.com/protocol-buffers/docs/overview)
```proto
/// 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.
```js
/// 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](https://koajs.com/)'s
middleware server.
```js
/// 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.
```proto
/// 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`.
```js
/// 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.
```js
// 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:
```js
/// 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.
```js
// 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:
```js
/// 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.
```js
// 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:
```js
/// index.js
const { GameServer } = require('<TBD>')
const { hideOtherPlayerHands } = require('./PlayerHand')
const game = new GameServer()
.useStateMask(hideOtherPlayerHands)
.run()
```