Skip to content

Commit a4521d0

Browse files
Add class Player and Game (#25)
* Add class Player, Game and their tests * Add class Player, Game and their tests * Replace variables with `Game` class methods and properties * Update `addPlayer`, `removePlayer` to fix the issue with #currentTurnPlayer; Move `gameMode` and `lineLengthLimit` to public * Fix when there's only one player or the current player is leaving the game, a turn change should be triggered; Replace some test methods with jest built-in matchers * Fix bugs of `index.ts` introduced by the last breaking changes; Rewrite tests of `Game` class for more coverage and resilience; Reorganise files * Remove `Game` interface * Remove jest coverage requirements * Update and simplify game tests --------- Co-authored-by: LivingLimes <[email protected]>
1 parent c56d252 commit a4521d0

File tree

10 files changed

+304
-82
lines changed

10 files changed

+304
-82
lines changed

backend/jest.config.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,6 @@ const config: Config = {
1414
coveragePathIgnorePatterns: ['/node_modules/'],
1515
coverageProvider: 'v8',
1616
coverageReporters: ['json', 'text', 'lcov', 'clover', 'text-summary'],
17-
// Jest will fail if there is less than 80% branch, line, and function coverage, or if there are more than 10 uncovered statements:
18-
coverageThreshold: {
19-
global: {
20-
branches: 80,
21-
functions: 80,
22-
lines: 80,
23-
statements: -10,
24-
},
25-
},
2617

2718
// Test Environment
2819
testEnvironment: 'jest-environment-node',

backend/src/__test__/drawing.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import Drawing from '../drawing'
1+
import Drawing from '../models/drawing'
22

33
const canvasHeight = 300
44
const canvasWidth = 300

backend/src/__test__/game.test.ts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import Game from '../models/game'
2+
import Player from '../models/player'
3+
import Drawing from '../models/drawing'
4+
import GameMode from '../game-mode'
5+
6+
describe('Game', () => {
7+
const DRAWING_BUFFER_SIZE = 300 * 300 * 4
8+
const firstPlayerId = 'first player'
9+
const secondPlayerId = 'second player'
10+
11+
describe('Initialise a game', () => {
12+
it('should initialise an empty drawing', () => {
13+
const game = new Game()
14+
15+
expect(game.drawing).toEqual(Drawing.createEmpty())
16+
})
17+
18+
it('should initialise an empty array of players', () => {
19+
const game = new Game()
20+
expect(game.players).toHaveLength(0)
21+
})
22+
23+
it('should initialise no current turn player', () => {
24+
const game = new Game()
25+
expect(game.currentPlayer).toBeNull()
26+
})
27+
28+
it('should initialise an undefined game mode', () => {
29+
const game = new Game()
30+
expect(game.gameMode).toBeUndefined()
31+
})
32+
33+
it('should initialise the default line limit', () => {
34+
const game = new Game()
35+
expect(game.lineLengthLimit).toEqual(150)
36+
})
37+
})
38+
39+
describe('adding a new player', () => {
40+
it('should add player to player list', () => {
41+
const game = new Game()
42+
43+
game.addPlayer(firstPlayerId)
44+
expect(game.players.some((player) => player.id === firstPlayerId)).toBe(
45+
true,
46+
)
47+
})
48+
49+
it('should set the new player as current turn player when there is only player', () => {
50+
const game = new Game()
51+
game.addPlayer(firstPlayerId)
52+
53+
expect(game.currentPlayer?.id).toEqual(firstPlayerId)
54+
})
55+
56+
it('should not trigger a turn change when there is already one player in the game', () => {
57+
const game = new Game()
58+
game.addPlayer(firstPlayerId)
59+
game.addPlayer(secondPlayerId)
60+
61+
expect(game.currentPlayer?.id).not.toEqual(secondPlayerId)
62+
})
63+
})
64+
65+
describe('reset game', () => {
66+
it('should reset the drawing', () => {
67+
const game = new Game()
68+
const emptyDrawing = Drawing.createEmpty()
69+
const newDrawing = Drawing.createFrom(Buffer.alloc(DRAWING_BUFFER_SIZE, 1))
70+
game.updateDrawing(newDrawing)
71+
72+
game.resetGame()
73+
74+
expect(game.drawing).toEqual(emptyDrawing)
75+
})
76+
77+
it('should reset the game mode', () => {
78+
const game = new Game()
79+
game.gameMode = GameMode.LineLengthLimit
80+
81+
game.resetGame()
82+
83+
expect(game.gameMode).toEqual(null)
84+
})
85+
})
86+
87+
describe('remove a given player', () => {
88+
it('should be able to remove when the given player exists', () => {
89+
const game = new Game()
90+
game.addPlayer(firstPlayerId)
91+
92+
game.removePlayer(firstPlayerId)
93+
expect(game.players).not.toContain(new Player(firstPlayerId))
94+
})
95+
96+
it('should throw error when removing a non-existent player', () => {
97+
const game = new Game()
98+
const nonExistentPlayerId = 'ghost-player'
99+
100+
expect(() => game.removePlayer(nonExistentPlayerId)).toThrow()
101+
})
102+
103+
it('should trigger a turn change if the current player leaves the game', () => {
104+
const game = new Game()
105+
game.addPlayer(firstPlayerId)
106+
game.addPlayer(secondPlayerId)
107+
108+
game.removePlayer(firstPlayerId)
109+
110+
expect(game.currentPlayer?.id).toEqual(secondPlayerId)
111+
})
112+
})
113+
114+
describe('next turn', () => {
115+
it('if only one player in the game, current turn player should not change', () => {
116+
const game = new Game()
117+
118+
game.addPlayer(firstPlayerId)
119+
game.nextTurn()
120+
121+
expect(game.currentPlayer?.id).toEqual(firstPlayerId)
122+
})
123+
124+
it('should go to next player in list', () => {
125+
const game = new Game()
126+
127+
game.addPlayer(firstPlayerId)
128+
game.addPlayer(secondPlayerId)
129+
game.nextTurn()
130+
expect(game.currentPlayer?.id).toEqual(secondPlayerId)
131+
132+
game.nextTurn()
133+
expect(game.currentPlayer?.id).toEqual(firstPlayerId)
134+
})
135+
136+
it('should loop back to first player from last player in list', () => {
137+
const game = new Game()
138+
139+
game.addPlayer(firstPlayerId)
140+
game.addPlayer(secondPlayerId)
141+
game.nextTurn()
142+
game.nextTurn()
143+
144+
expect(game.currentPlayer?.id).toEqual(firstPlayerId)
145+
})
146+
})
147+
148+
describe('check if the given player in the list', () => {
149+
it('should return true if the given player is in the list', () => {
150+
const game = new Game()
151+
game.addPlayer(firstPlayerId)
152+
153+
expect(game.hasPlayer(firstPlayerId)).toBe(true)
154+
})
155+
156+
it('should return false if the given player is not in the list', () => {
157+
const game = new Game()
158+
expect(game.hasPlayer(firstPlayerId)).toBe(false)
159+
})
160+
})
161+
})

backend/src/events.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export const SOCKET_EVENTS_OUTBOUND = {
2+
DRAW: 'draw',
3+
CONNECT: 'connect',
4+
INITIAL_DATA: 'initial-data',
5+
UPDATE_TURN: 'update-turn',
6+
CLEAR_CANVAS: 'clear-canvas',
7+
SELECTED_GAME_MODE: 'selected-game-mode',
8+
} as const
9+
10+
export const SOCKET_EVENTS_INBOUND = {
11+
CONNECTION: 'connection',
12+
DISCONNECT: 'disconnect',
13+
14+
DRAW: 'draw',
15+
END_TURN: 'end-turn',
16+
CLEAR_CANVAS: 'clear-canvas',
17+
SELECT_GAME_MODE: 'select-game-mode',
18+
} as const

backend/src/game-mode.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
enum GameMode {
2+
OneLine = 'One Line',
3+
LineLengthLimit = 'Line Length Limit',
4+
}
5+
6+
export default GameMode

backend/src/index.ts

Lines changed: 36 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { createServer } from 'http'
22
import { Server } from 'socket.io'
3-
import Drawing from '@/drawing'
3+
import Drawing from '@/models/drawing'
4+
import { SOCKET_EVENTS_INBOUND, SOCKET_EVENTS_OUTBOUND } from '@/events'
5+
import Game from '@/models/game'
6+
import GameMode from '@/game-mode'
47

58
const PORT = 3001
69

@@ -17,119 +20,80 @@ const io = new Server(httpServer, {
1720
},
1821
})
1922

20-
const SOCKET_EVENTS_OUTBOUND = {
21-
DRAW: 'draw',
22-
CONNECT: 'connect',
23-
INITIAL_DATA: 'initial-data',
24-
UPDATE_TURN: 'update-turn',
25-
CLEAR_CANVAS: 'clear-canvas',
26-
SELECTED_GAME_MODE: 'selected-game-mode',
27-
} as const
28-
29-
const SOCKET_EVENTS_INBOUND = {
30-
CONNECTION: 'connection',
31-
DISCONNECT: 'disconnect',
32-
33-
DRAW: 'draw',
34-
END_TURN: 'end-turn',
35-
CLEAR_CANVAS: 'clear-canvas',
36-
SELECT_GAME_MODE: 'select-game-mode',
37-
} as const
38-
39-
enum GameMode {
40-
OneLine = 'One Line',
41-
LineLengthLimit = 'Line Length Limit',
42-
}
43-
44-
let drawing = Drawing.createEmpty()
45-
let players: Array<string> = []
46-
let gameMode: GameMode | undefined | null = undefined
47-
const lineLengthLimit: number = 150
48-
let turnPlayer: string | undefined | null = undefined
23+
const game = new Game()
4924

5025
io.on(SOCKET_EVENTS_INBOUND.CONNECTION, (socket) => {
5126
console.log('User connected:', socket.id)
5227

53-
players.push(socket.id)
54-
55-
if (players.length === 1) {
56-
turnPlayer = socket.id
57-
}
28+
game.addPlayer(socket.id)
5829

5930
socket.emit(SOCKET_EVENTS_OUTBOUND.UPDATE_TURN, {
60-
turnPlayer,
31+
turnPlayer: game.currentPlayer?.id,
6132
})
6233

6334
socket.emit(SOCKET_EVENTS_OUTBOUND.INITIAL_DATA, {
64-
lineLengthLimit,
65-
drawing: drawing.getValue(),
66-
gameMode,
35+
lineLengthLimit: game.lineLengthLimit,
36+
drawing: game.drawing.getValue(),
37+
gameMode: game.gameMode,
6738
})
6839

69-
socket.on(SOCKET_EVENTS_INBOUND.DRAW, (drawingAsArray) => {
70-
if (turnPlayer !== socket.id || !players.includes(socket.id)) {
71-
return
72-
}
73-
74-
if (!Drawing.canCreate(drawingAsArray)) {
75-
return
76-
}
77-
drawing = Drawing.createFrom(drawingAsArray)
78-
79-
socket.broadcast.emit(SOCKET_EVENTS_OUTBOUND.DRAW, drawing.getValue())
80-
})
40+
socket.on(
41+
SOCKET_EVENTS_INBOUND.DRAW,
42+
(drawingAsArray: Buffer<ArrayBufferLike>) => {
43+
if (game.currentPlayer?.id !== socket.id || !game.hasPlayer(socket.id)) {
44+
return
45+
}
46+
47+
if (!Drawing.canCreate(drawingAsArray)) {
48+
return
49+
}
50+
game.updateDrawing(Drawing.createFrom(drawingAsArray))
51+
52+
socket.broadcast.emit(
53+
SOCKET_EVENTS_OUTBOUND.DRAW,
54+
game.drawing.getValue(),
55+
)
56+
},
57+
)
8158

8259
socket.on(SOCKET_EVENTS_INBOUND.SELECT_GAME_MODE, (mode: GameMode) => {
83-
gameMode = mode
60+
game.gameMode = mode
8461

8562
socket.broadcast.emit(SOCKET_EVENTS_OUTBOUND.SELECTED_GAME_MODE, mode)
8663
})
8764

8865
socket.on(SOCKET_EVENTS_INBOUND.CLEAR_CANVAS, () => {
89-
if (!players.includes(socket.id)) {
66+
if (!game.hasPlayer(socket.id)) {
9067
return
9168
}
9269

93-
drawing = Drawing.createEmpty()
94-
gameMode = null
70+
game.resetGame()
9571

9672
socket.broadcast.emit(SOCKET_EVENTS_OUTBOUND.CLEAR_CANVAS, {
97-
gameMode,
73+
gameMode: game.gameMode,
9874
})
9975
})
10076

10177
socket.on(SOCKET_EVENTS_INBOUND.END_TURN, () => {
102-
if (turnPlayer !== socket.id || !players.includes(socket.id)) {
78+
if (game.currentPlayer?.id !== socket.id || !game.hasPlayer(socket.id)) {
10379
return
10480
}
10581

106-
const nextPlayer = getNextElement(players, turnPlayer)
107-
turnPlayer = nextPlayer ?? players[0] ?? null
82+
game.nextTurn()
10883

10984
io.emit(SOCKET_EVENTS_OUTBOUND.UPDATE_TURN, {
110-
turnPlayer,
85+
turnPlayer: game.currentPlayer.id,
11186
})
11287
})
11388

11489
socket.on(SOCKET_EVENTS_INBOUND.DISCONNECT, (reason) => {
11590
console.log('User disconnected', socket.id, reason)
11691

117-
// Turn player disconnect special case
118-
if (turnPlayer === socket.id) {
119-
const nextPlayer = getNextElement(players, turnPlayer)
120-
turnPlayer = nextPlayer ?? players[0] ?? null
121-
}
122-
12392
// Turn player and viewer disconnect
124-
players = players.filter((p) => p !== socket.id)
93+
game.removePlayer(socket.id)
12594
})
12695
})
12796

12897
httpServer.listen(PORT, () => {
12998
console.log(`Server running on port ${PORT}`)
13099
})
131-
132-
function getNextElement<T>(array: Array<T>, element: T) {
133-
const currentIndex = array.indexOf(element)
134-
return currentIndex === -1 ? null : array[(currentIndex + 1) % array.length]
135-
}
File renamed without changes.

0 commit comments

Comments
 (0)