Skip to content

Commit c861f4b

Browse files
author
hiukim
committed
part 5 - game modelling
1 parent a84d387 commit c861f4b

File tree

5 files changed

+287
-68
lines changed

5 files changed

+287
-68
lines changed

imports/api/collections/games.js

Lines changed: 17 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,26 @@
11
import { Mongo } from 'meteor/mongo';
2+
import { Game } from '../models/game.js';
23

3-
export default Games = new Mongo.Collection('games');
4+
export default Games = new Mongo.Collection('games', {
5+
transform(doc) {
6+
return new Game(doc);
7+
}
8+
});
49

510
_.extend(Games, {
6-
newGame() {
7-
let gameDoc = {
8-
board: [[null, null, null], [null, null, null], [null, null, null]],
9-
players: []
10-
};
11-
let gameId = Games.insert(gameDoc); // insert a new game document into the collection
12-
return gameId;
13-
},
11+
saveGame(game) {
1412

15-
joinGame(gameId, user) {
16-
console.log("gameId; ", gameId, user);
17-
let game = Games.findOne(gameId);
18-
if (game.players.length === 2) {
19-
throw "game is full";
20-
}
21-
game.players.push({
22-
userId: user._id,
23-
username: user.username
24-
});
25-
Games.update(game._id, {
26-
$set: {players: game.players}
13+
let gameDoc = {};
14+
_.each(game.persistentFields(), (field) => {
15+
gameDoc[field] = game[field];
2716
});
28-
},
2917

30-
leaveGame(gameId, user) {
31-
let game = Games.findOne(gameId);
32-
game.players = _.reject(game.players, (player) => {
33-
return player.userId === user._id;
34-
});
35-
Games.update(game._id, {
36-
$set: {players: game.players}
37-
});
18+
if (game._id) {
19+
Games.update(game._id, {
20+
$set: gameDoc
21+
});
22+
} else {
23+
Games.insert(gameDoc);
24+
}
3825
}
3926
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import {Game} from "../models/game.js";
2+
import Games from "../collections/games.js";
3+
4+
export let GamesController = {
5+
newGame(user) {
6+
let game = new Game();
7+
game.userJoin(user);
8+
Games.saveGame(game);
9+
},
10+
11+
userJoinGame(gameId, user) {
12+
let game = Games.findOne(gameId);
13+
game.userJoin(user);
14+
Games.saveGame(game);
15+
},
16+
17+
userLeaveGame(gameId, user) {
18+
let game = Games.findOne(gameId);
19+
game.userLeave(user);
20+
Games.saveGame(game);
21+
},
22+
23+
userMarkGame(gameId, user, row, col) {
24+
let game = Games.findOne(gameId);
25+
game.userMark(user, row, col);
26+
Games.saveGame(game);
27+
}
28+
}

imports/api/models/game.js

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
/**
2+
* GameStatus constants
3+
*/
4+
export const GameStatuses = {
5+
WAITING: 'WAITING', // waiting player to join
6+
STARTED: 'STARTED', // all spots are filled; can start playing
7+
FINISHED: 'FINISHED', // game is finished
8+
ABANDONED: 'ABANDONED' // all players left; game is abandoned
9+
}
10+
11+
/**
12+
* Game model, encapsulating game-related logics
13+
* It is data store independent
14+
*/
15+
export class Game {
16+
/**
17+
* Constructor accepting a single param gameDoc.
18+
* gameDoc should contain the permanent fields of the game instance.
19+
* Normally, the fields are saved into data store, and later get retrieved
20+
*
21+
* If gameDoc is not given, then we will instantiate a new object with default fields
22+
*
23+
* @param {Object} [gameDoc] Optional doc retrieved from Games collection
24+
*/
25+
constructor(gameDoc) {
26+
if (gameDoc) {
27+
_.extend(this, gameDoc);
28+
} else {
29+
this.status = GameStatuses.WAITING;
30+
this.board = [[null, null, null], [null, null, null], [null, null, null]];
31+
this.players = [];
32+
}
33+
}
34+
35+
/**
36+
* Return a list of fields that are required for permanent storage
37+
*
38+
* @return {[]String] List of fields required persistent storage
39+
*/
40+
persistentFields() {
41+
return ['status', 'board', 'players'];
42+
}
43+
44+
/**
45+
* Handle join game action
46+
*
47+
* @param {User} user Meteor.user object
48+
*/
49+
userJoin(user) {
50+
if (this.status !== GameStatuses.WAITING) {
51+
throw "cannot join at current state";
52+
}
53+
if (this.userIndex(user) !== null) {
54+
throw "user already in game";
55+
}
56+
57+
this.players.push({
58+
userId: user._id,
59+
username: user.username
60+
});
61+
62+
// game automatically start with 2 players
63+
if (this.players.length === 2) {
64+
this.status = GameStatuses.STARTED;
65+
}
66+
}
67+
68+
/**
69+
* Handle leave game action
70+
*
71+
* @param {User} user Meteor.user object
72+
*/
73+
userLeave(user) {
74+
if (this.status !== GameStatuses.WAITING) {
75+
throw "cannot leave at current state";
76+
}
77+
if (this.userIndex(user) === null) {
78+
throw "user not in game";
79+
}
80+
this.players = _.reject(this.players, (player) => {
81+
return player.userId === user._id;
82+
});
83+
84+
// game is considered abandoned when all players left
85+
if (this.players.length === 0) {
86+
this.status = GameStatuses.ABANDONED;
87+
}
88+
}
89+
90+
/**
91+
* Handle user action. i.e. putting marker on the game board
92+
*
93+
* @param {User} user
94+
* @param {Number} row Row index of the board
95+
* @param {Number} col Col index of the board
96+
*/
97+
userMark(user, row, col) {
98+
let playerIndex = this.userIndex(user);
99+
let currentPlayerIndex = this.currentPlayerIndex();
100+
if (currentPlayerIndex !== playerIndex) {
101+
throw "user cannot make move at current state";
102+
}
103+
if (row < 0 || row >= this.board.length || col < 0 || col >= this.board[row].length) {
104+
throw "invalid row|col input";
105+
}
106+
if (this.board[row][col] !== null) {
107+
throw "spot is filled";
108+
}
109+
this.board[row][col] = playerIndex;
110+
111+
let winner = this.winner();
112+
if (winner !== null) {
113+
this.status = GameStatuses.FINISHED;
114+
}
115+
if (this._filledCount() === 9) {
116+
this.status = GameStatuses.FINISHED;
117+
}
118+
}
119+
120+
/**
121+
* @return {Number} currentPlayerIndex 0 or 1
122+
*/
123+
currentPlayerIndex() {
124+
if (this.status !== GameStatuses.STARTED) {
125+
return null;
126+
}
127+
128+
// determine the current player by counting the filled cells
129+
// if even, then it's first player, otherwise it's second player
130+
let filledCount = this._filledCount();
131+
return (filledCount % 2 === 0? 0: 1);
132+
}
133+
134+
/**
135+
* Determine the winner of the game
136+
*
137+
* @return {Number} playerIndex of the winner (0 or 1). null if not finished
138+
*/
139+
winner() {
140+
let board = this.board;
141+
for (let playerIndex = 0; playerIndex < 2; playerIndex++) {
142+
// check rows
143+
for (let r = 0; r < 3; r++) {
144+
let allMarked = true;
145+
for (let c = 0; c < 3; c++) {
146+
if (board[r][c] !== playerIndex) allMarked = false;
147+
}
148+
if (allMarked) return playerIndex;
149+
}
150+
151+
// check cols
152+
for (let c = 0; c < 3; c++) {
153+
let allMarked = true;
154+
for (let r = 0; r < 3; r++) {
155+
if (board[r][c] !== playerIndex) allMarked = false;
156+
}
157+
if (allMarked) return playerIndex;
158+
}
159+
160+
// check diagonals
161+
if (board[0][0] === playerIndex && board[1][1] === playerIndex && board[2][2] === playerIndex) {
162+
return playerIndex;
163+
}
164+
if (board[0][2] === playerIndex && board[1][1] === playerIndex && board[2][0] === playerIndex) {
165+
return playerIndex;
166+
}
167+
}
168+
return null;
169+
}
170+
171+
/**
172+
* Helper method to retrieve the player index of a user
173+
*
174+
* @param {User} user Meteor.user object
175+
* @return {Number} index 0-based index, or null if not found
176+
*/
177+
userIndex(user) {
178+
for (let i = 0; i < this.players.length; i++) {
179+
if (this.players[i].userId === user._id) {
180+
return i;
181+
}
182+
}
183+
return null;
184+
}
185+
186+
_filledCount() {
187+
let filledCount = 0;
188+
for (let r = 0; r < 3; r++) {
189+
for (let c = 0; c < 3; c++) {
190+
if (this.board[r][c] !== null) filledCount++;
191+
}
192+
}
193+
return filledCount;
194+
}
195+
}

imports/ui/GameBoard.jsx

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,52 @@
11
import React, { Component } from 'react';
2+
import {GamesController} from '../api/controllers/gamesController.js';
3+
import {Game, GameStatuses} from '../api/models/game.js';
24

35
export default class GameBoard extends Component {
4-
currentPlayer() {
5-
// determine the current player by counting the filled cells
6-
// if even, then it's first player, otherwise it's second player
7-
let filledCount = 0;
8-
for (let r = 0; r < 3; r++) {
9-
for (let c = 0; c < 3; c++) {
10-
if (this.props.game.board[r][c] !== null) filledCount++;
11-
}
12-
}
13-
return (filledCount % 2 === 0? 0: 1);
14-
}
15-
166
handleCellClick(row, col) {
17-
let currentPlayer = this.currentPlayer();
187
let game = this.props.game;
19-
20-
if (game.players[currentPlayer].userId !== this.props.user._id) return;
21-
22-
game.board[row][col] = currentPlayer;
23-
Games.update(game._id, {
24-
$set: {board: game.board}
25-
});
8+
if (game.currentPlayerIndex() !== game.userIndex(this.props.user)) return;
9+
GamesController.userMarkGame(game._id, this.props.user, row, col);
2610
}
2711

28-
handleBackToGameList() {
12+
handleBackToGameList() {
2913
this.props.backToGameListHandler();
3014
}
3115

32-
renderCell(row, col) {
16+
renderCell(row, col) {
3317
let value = this.props.game.board[row][col];
3418
if (value === 0) return (<td>O</td>);
3519
if (value === 1) return (<td>X</td>);
3620
if (value === null) return (
3721
<td onClick={this.handleCellClick.bind(this, row, col)}></td>
3822
);
3923
}
40-
render() {
24+
25+
renderStatus() {
26+
let game = this.props.game;
27+
let status = "";
28+
if (game.status === GameStatuses.STARTED) {
29+
let playerIndex = game.currentPlayerIndex();
30+
status = `In Progress: current player: ${game.players[playerIndex].username}`;
31+
} else if (game.status === GameStatuses.FINISHED) {
32+
let playerIndex = game.winner();
33+
if (playerIndex === null) {
34+
status = "Finished: tie";
35+
} else {
36+
status = `Finished: winner: ${game.players[playerIndex].username}`;
37+
}
38+
}
39+
40+
return (
41+
<div>{status}</div>
42+
)
43+
}
44+
45+
render() {
4146
return (
4247
<div>
4348
<button onClick={this.handleBackToGameList.bind(this)}>Back</button>
49+
{this.renderStatus()}
4450
<table className="game-board">
4551
<tbody>
4652
<tr>

0 commit comments

Comments
 (0)