Table of Contents generated with DocToc
- Multi-player Conway's Game of Life
This is a web-based multi-player game: Conway's Game of Life.
This game is a world of cells on 2-d grid. The world is having endless rounds of natural evolution, e.g. 3 seconds for a round, according to the following rules:
1. Any live cell with fewer than two live neighbours dies, as if caused by under-population.
2. Any live cell with two or three live neighbours lives on to the next generation.
3. Any live cell with more than three live neighbours dies, as if by overcrowding.
4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
Each player will be assigned a color and some random cells with that color on the grid. The players can also put more cells onto the grid by clicking. The cells they created will evolve together with other cells.
A dead cell that comes back to life will have a color that is the averaged of its neighbor cells who saved it.
- express - Web server
- socket-io - widely used lib for real time connection
- Vue - Front end framework
- Typescript - static type checkings
- mocha - testing framework
- Webpack - front end assets bundling
A heroku account is required.
You must have a heroku account and a heroku CLI installed. See https://devcenter.heroku.com/articles/heroku-cli
git clone https://github.com/victor-develop/conway-gof.git
cd conway-gof
heroku create
heroku push master
And then check out the web url provided in the console.
To run this game locally, you should have Docker installed. You can download and install from https://www.docker.com/get-docker
At the time of development I am using version 17.09.0
on Windows machine. Docker should work aross different platforms
-
Clone the repository and enter the directory root
git clone https://github.com/victor-develop/conway-gof.git cd conway-gof
-
Run Docker
docker-compose up -d
-
Enter the bash of the docker image
docker-compose exec conway /bin/bash
-
You shall be at the /home/dev now. The
package.json
at root directory is NOT the package.json for this project. This is just a file created for Heroku's requirement to deploy the app successfully. Instead, you should go tosolution
folder to see the real package.json, where the project source code stays. -
Install npm dependencies
cd solution npm install
-
start the server
// still at `solution` directory npm run dev
Now you can go to
localhost:8080
and start playing the game. To try out multi-players just open another browser window
Enter the shell with
docker-compose exec conway /bin/bash
cd solution
And then, in the bash shell, you may:
- development with live-reload server:
npm run dev
- run tests:
npm run test
The source code is mostly under ./solution/ts_code/
, divided into 3 major parts: client, common, and server, which will be transpiled into JS under ./solution/dist/
with corresponding folder structure.
Webpack will also transplie and place a bundled front-end base.bundle.js
into dist/client/static
. HOWEVER, The ./solution/dist/client/static
is NOT only a folder for transpliled content, but also some static assets like index.html, .js, .css and so on. These are legacy files which are not yet integrated into the bundling process. It is not a good practice to mix genereted files and source files together, so these static assests may probably be moved out from dist
and get packed into webpack bundle sometime later.
./ # config of docker, webpack, nodemon, typescript, tslint, etc.
├─.vscode # vscode debug config
├─docker
│ └─docker-image
└─solution # source code and compiled/transpiled files
├─dist # mainly contains files transpiled from ./solution/ts_code
│ ├─client
│ │ ├─static # NOTE: The 'static' files contains static content which need not transpliling
│ ├─common
│ └─server
├─node_modules
└─ts_code # Typescript source code
├─client # client-side code, running in browsers
│ ├─src
│ └─tests
├─common # reusable components/classes for both front and back ends
│ ├─src
│ └─tests
└─server # server-side code, running in Node.js
├─src
│ ├─config # project configurations
└─tests
This is not a strict UML but a rough conceptual overview of the whole application.
As in the diagram,
common
includes data models, interfaces, events, and utitlities, e.g. logger that are shared by both front&back ends.client
contains front end logic. Vue is used as the reactive presenter of :ClientState, which is part of :Client, :GameApi takes care of communication with server, and coordinates with :Client through events.server
's main component is the :Game instance, the function setApiService handles communication with clients and manipulate the :Game instance accordingly
The real time connection is currently implemented with SocketIO, but can also be replaced with other solutions if needed in the future, by changing the GameApi at the client-side and setApiService at the server-side, without affecting other components.
GameBoard is esstially a list of alive cells plus the world border: width and height. Dead cells are not stored, but they will be stored once they come back to life according to reproduction rule. Cells are arranged in (0,0)-started 2-d grid plain.
A function that takes a board as input and output a "evolved" board with a list of cells updated.
The Game class at server side broadcast its state to clients via api service whenerver updated. Ideally, the game board can be updated by evolution or manually updated by players at any time. But it would be complex and hard to debug if the game board is being updated by evolution and by user at the same moement.
Thus, the Game internally uses a queue to avoid muting the game state concurrently. Any update logic to the board will be packed in a funtion and queued up, and the board will be updated sequentially according to queue order. The Game keeps scanning and consuming the job queue every 2 milleseconds, making it feeling reactive in players' experience. The following diagram shows different things happen which will enqueue an update function.
To enable easier testing and debug, the following items were intentionally designed as injected dependencies of the constructor of Game .(below only list some arguments important to note, for full list of argumrents required plz See ./solution/ts_code/server/game-engine/game.ts -> constructor())
evolveFunc: (board: GameBoard) => GameBoard
: the logic of natural evolutiongetRandomPattern: (board: GameBoard) => Pattern
: random pattern generator,it can be mocked with a function which returns known pattern series during testingeventBus: IEventBus
: mainly used to broadcast game state updatesevolveTimer: IIntervalLoopSetter
: an interface that does similar function as native JSsetInterval
, by mocking this object, you can manually control how and when the board evolves during testingjobQueueTimer: IIntervalLoopSetter
: an interfae that does similar function as native JSsetInterval
, by mocking this object, you can decide the behaviour of how and when a update-board job is dequeued and consumed during testing
By using these injection you can simulate a completely predicatable test against the state changes of the game board. See ./solution/ts_code/server/tests/set-api-service.spec.ts
Different components depend on IEventBus
to coordinates with each other.
// .\solution\ts_code\common\src\ievent-bus.ts
export interface IEventBus {
emit: (eventKey: string, ...args) => any
on: (eventKey: string, callback) => any
}
These files defined the event keys:
apiEvents
: ./solution/ts_code/common/src/api/api-eventssocketEvents
: ./solution/ts_code/common/src/api/socket-events
The major events happened between server and clients is shown below (error events omitted):
This project used bunyan for logging implementation of the ILogger
interface. It can be replaced with other solutions as well if needed in future.
-
In development environment it will output everything to the
stdout
. -
During tests it will output logs to
./solution/dist/server/tests/ouput_xxxxx.txt
depending on the datetime at runtime.
The whole application starts with one root level logger, and different places in the application may spawn a child logger by using logger.child(customKey: string)
, and all the json output by the child logger will have the property customKey you can track with.
All the event buses, as well as socket-io in the application should be already attached with a logger by ./solution/common/src/log-event-bus.ts
.
Every time when an event, e.g. apiEvents.gameStateUpdate
is registerd with a handler or emmited, the event will be automatically logged as well as the arguments passed through.
You can easily track the application on your heroku deployment by redirecting heroku logs
to you own disk. You can also use some json search and filter techniques provided by bunyan cli
Note: bunyan logger just automatically appended a word 'undefined' at 'msg' fields of each log, this is not an application error.
For example, a root level log is like this
{"name":"Development Log","hostname":"6a6b14989f8f","pid":464,"level":30,"msg":"listening on 8080 undefined","time":"2017-12-26T07:41:16.694Z","v":0}
And a child logger spawned by logger.child('apiService - socketio')
looks like this, as you can see it has a property named apiService - socketio
. This is a sample log when a handler is registered(mounted) on the error event.
{"name":"Development Log","hostname":"6a6b14989f8f","pid":464,"apiService - socketio":"apiService - socketio","level":30,"eventKey":"error","msg":"event mounted on 1514274076702","time":"2017-12-26T07:41:16.702Z","v":0}
And here is another sample log when the event game-state-update is emitted.
{"name":"Development Log","hostname":"6a6b14989f8f","pid":464,"level":30,"eventKey":"game-state-update","args":[{"updateAt":1514274121728,"players":[{"uid":"S1h76OkXf","name":"Plaki","color":"#a5c359"}]}],"msg":"event emitted 1514274121728","time":"2017-12-26T07:42:01.728Z","v":0}
-
Data from client side has not been validated yet. For example, an object without desired properties sent from client side may leads to app crash, this may happen in cases, e.g. if scripts on browser side is modified by a thrid-party software.
-
Currently the server broadcasts the whole grid together with the list of players to clients to ensure syncronization. A better approach(with more effort) would be only sending the updated data for most clients in stable connection and let clients which fails to syncronize request for whole-state update. For example, the client may check for an increamental sync-id transmitted from server, if the difference is more than 1, the client requests for whole-state update.
-
The game instance runs and the game world evolves endlessly together with the server regardless of the existence of player. This is a waste. When there is no player, the game instance can stopped until a new player enters. But it has to store the state somewhere in advance and reloads it, calculating how many rounds of interval it has stopped for and directly evolve for that many times to recover to the right state it should be at.
-
A single Node.js app can support very limited number of concurrent socket connections. One of the ideas to scale up, is that multiple nodes could be used for purely socket connections and all of them use api tp connect to the same game world provided by a service(another server, another language for faster caculation ...), which only responsible for maintaining the game state.
-
currently the board is bordered and cannot have negative coordinates. But the
evolveBoard()
function CAN support borderless evolution argrithmatically. What else needed is to implement a board which can dynamically shrink and enlarge its width and height according to the cells it has, of course then the client intereface should also support world-exploring feature. -
For production environment, use
pm2
to restart process in case of exit, and add health monitor like appmetrics -
common/src/api/
apiEvents
contains keys representing client->server and server->client, better to sepearate them in later versions. -
dist/client/static: Move it somewhere else and copy it into
dist
at build time, so thatdist
can be a directory purely for built artifacts. -
tune bunyan to avoid the 'undefined' word appended in the log
-
front end is using TempLogger outputting to console, should be changed to bunyan, consider important logs to be stored in localstorage/report to server
-
better error messages to players
-
a log reader to organize and present the logs nicely
-
double check and correct file naming consistency
This project is licensed under the MIT License - see the LICENSE.md file for details
Credits to below, where I adapted/learnt from their code for
Drawing game board
Reference for setting up the project structure and environment
-
Rising Stack: https://blog.risingstack.com/building-a-node-js-app-with-typescript-tutorial/
-
luixaviles: https://github.com/luixaviles/socket-io-typescript-chat
Readme Template