caro5 / client /src /game /__tests__ /GameEngine.test.ts
Pedro de Carvalho
Add more test
bd42552
Raw
History Blame Contribute Delete
10.7 kB
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { GameEngine } from '../GameEngine.ts'
import { BoardStore } from '../BoardStore.ts'
import { GameManager } from 'shared/GameManager.ts'
vi.mock('pixi.js', () => ({
Application: vi.fn(() => ({
init: vi.fn(),
renderer: { background: { color: 0 }, resize: vi.fn() },
stage: { addChild: vi.fn() },
canvas: {} as HTMLCanvasElement,
})),
Container: vi.fn(() => ({ eventMode: 'none' })),
Graphics: vi.fn(() => ({
rect: vi.fn(),
fill: vi.fn(),
stroke: vi.fn(),
clear: vi.fn(),
})),
Ticker: { shared: { add: vi.fn(), remove: vi.fn(), deltaMS: 16 } },
}))
vi.mock('../CameraController.ts', () => ({
CameraController: vi.fn(() => ({
setInitialZoom: vi.fn(),
enable: vi.fn(),
onChange: null,
onCellClick: null,
onCellHover: null,
ensureCellsInView: vi.fn(),
resize: vi.fn(),
})),
}))
vi.mock('../GridRenderer.ts', () => ({
GridRenderer: vi.fn(() => ({
container: { addChild: vi.fn() },
draw: vi.fn(),
})),
}))
vi.mock('../SymbolRenderer.ts', () => ({
SymbolRenderer: vi.fn(() => ({
container: { addChild: vi.fn() },
update: vi.fn(),
clear: vi.fn(),
getSymbol: vi.fn(),
})),
}))
vi.mock('../effects/AudioManager.ts', () => ({
AudioManager: vi.fn(() => ({
play: vi.fn(),
playHover: vi.fn(),
startMusic: vi.fn(),
stopMusic: vi.fn(),
destroy: vi.fn(),
toggleMute: vi.fn(() => false),
playVictoryMusic: vi.fn(),
playDefeatMusic: vi.fn(),
muted: false,
})),
audioManager: {
play: vi.fn(),
playHover: vi.fn(),
startMusic: vi.fn(),
stopMusic: vi.fn(),
destroy: vi.fn(),
toggleMute: vi.fn(() => false),
playVictoryMusic: vi.fn(),
playDefeatMusic: vi.fn(),
muted: false,
},
}))
vi.mock('../effects/ParticleSystem.ts', () => ({
ParticleSystem: vi.fn(() => ({
update: vi.fn(),
clear: vi.fn(),
})),
}))
vi.mock('../effects/PlacementEffect.ts', () => ({
PlacementEffect: vi.fn(() => ({ play: vi.fn() })),
}))
vi.mock('../effects/WinEffect.ts', () => ({
WinEffect: vi.fn(() => ({ play: vi.fn() })),
}))
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
function createEngine(): GameEngine {
const engine = new GameEngine()
engine.board = new BoardStore()
engine.status = 'active'
engine.localMode = true
engine.currentPlayer = 0
engine.playerIndex = 0
engine.playerSymbols = ['X', 'O']
engine.players = ['P1', 'P2']
engine.manager = new GameManager('P1', 'P2', ['X', 'O'])
engine.onStateChanged = vi.fn()
engine.onError = vi.fn()
engine['redraw'] = vi.fn()
engine['playPlacementEffect'] = vi.fn()
engine['winEffect'] = { play: vi.fn() } as any
engine.audio = {
play: vi.fn(),
stopMusic: vi.fn(),
startMusic: vi.fn(),
playVictoryMusic: vi.fn(),
playDefeatMusic: vi.fn(),
} as any
return engine
}
describe('GameEngine local mode', () => {
describe('turn switching', () => {
it('starts with player 0', () => {
const engine = createEngine()
expect(engine.currentPlayer).toBe(0)
})
it('switches to player 1 after a non-winning move', () => {
const engine = createEngine()
engine['handleCellClick'](0, 0)
expect(engine.currentPlayer).toBe(1)
expect(engine.onStateChanged).toHaveBeenCalled()
})
it('switches back to player 0 after second move', () => {
const engine = createEngine()
engine['handleCellClick'](0, 0)
engine['handleCellClick'](1, 0)
expect(engine.currentPlayer).toBe(0)
})
it('sets currentPlayer to -1 during win check', () => {
const engine = createEngine()
engine.manager.addMove(0, 0, 0, 'X')
engine.manager.addMove(1, 0, 0, 'X')
engine.manager.addMove(2, 0, 0, 'X')
engine.manager.addMove(3, 0, 0, 'X')
engine['handleCellClick'](4, 0)
vi.advanceTimersByTime(100)
expect(engine.winnerPlayerId).toBe('0')
})
it('does not switch turn when game is not active', () => {
const engine = createEngine()
engine.status = 'waiting'
engine['handleCellClick'](0, 0)
expect(engine.currentPlayer).toBe(0)
})
})
describe('win detection', () => {
it('detects a horizontal win and calls applyWin', () => {
const engine = createEngine()
engine.manager.addMove(0, 0, 0, 'X')
engine.manager.addMove(1, 0, 0, 'X')
engine.manager.addMove(2, 0, 0, 'X')
engine.manager.addMove(3, 0, 0, 'X')
engine['handleCellClick'](4, 0)
vi.advanceTimersByTime(100)
expect(engine.status).toBe('win')
expect(engine.winnerPlayerId).toBe('0')
})
it('detects a vertical win', () => {
const engine = createEngine()
engine.manager.addMove(0, 0, 1, 'O')
engine.manager.addMove(0, 1, 1, 'O')
engine.manager.addMove(0, 2, 1, 'O')
engine.manager.addMove(0, 3, 1, 'O')
engine.manager.setCurrentPlayer(1)
engine['handleCellClick'](0, 4)
vi.advanceTimersByTime(100)
expect(engine.status).toBe('win')
expect(engine.winnerPlayerId).toBe('1')
})
it('detects a diagonal win (top-left to bottom-right)', () => {
const engine = createEngine()
engine.manager.addMove(0, 0, 0, 'X')
engine.manager.addMove(1, 1, 0, 'X')
engine.manager.addMove(2, 2, 0, 'X')
engine.manager.addMove(3, 3, 0, 'X')
engine['handleCellClick'](4, 4)
vi.advanceTimersByTime(100)
expect(engine.status).toBe('win')
expect(engine.winnerPlayerId).toBe('0')
})
it('detects a diagonal win (top-right to bottom-left)', () => {
const engine = createEngine()
engine.manager.addMove(4, 0, 0, 'X')
engine.manager.addMove(3, 1, 0, 'X')
engine.manager.addMove(2, 2, 0, 'X')
engine.manager.addMove(1, 3, 0, 'X')
engine['handleCellClick'](0, 4)
vi.advanceTimersByTime(100)
expect(engine.status).toBe('win')
expect(engine.winnerPlayerId).toBe('0')
})
it('does not trigger win for 4 in a row', () => {
const engine = createEngine()
engine.manager.addMove(0, 0, 0, 'X')
engine.manager.addMove(1, 0, 0, 'X')
engine.manager.addMove(2, 0, 0, 'X')
engine['handleCellClick'](3, 0)
expect(engine.status).toBe('active')
expect(engine.currentPlayer).toBe(1)
})
it('does not trigger win when blocked on both ends', () => {
const engine = createEngine()
engine.manager.addMove(0, 0, 1, 'O')
engine.manager.addMove(1, 0, 0, 'X')
engine.manager.addMove(2, 0, 0, 'X')
engine.manager.addMove(3, 0, 0, 'X')
engine.manager.addMove(4, 0, 0, 'X')
engine.manager.addMove(5, 0, 0, 'X')
engine.manager.setCurrentPlayer(1)
engine['handleCellClick'](6, 0)
vi.advanceTimersByTime(100)
expect(engine.status).not.toBe('win')
})
})
describe('draw detection', () => {
it('detects a draw when the board is full', () => {
const engine = createEngine()
for (let x = 0; x < 15; x++) {
for (let y = 0; y < 15; y++) {
if (x === 14 && y === 14) break
engine.manager.addMove(x, y, (x + y) % 2, (x + y) % 2 === 0 ? 'X' : 'O')
}
}
engine.manager.setCurrentPlayer(1)
engine['handleCellClick'](14, 14)
vi.advanceTimersByTime(100)
expect(engine.status).toBe('draw')
})
it('does not trigger draw when board is not full', () => {
const engine = createEngine()
engine['handleCellClick'](0, 0)
expect(engine.status).toBe('active')
})
})
describe('cell placement', () => {
it('places a cell on the board', () => {
const engine = createEngine()
engine['handleCellClick'](5, 5)
const cell = engine.board.getCell(5, 5)
expect(cell).toBeDefined()
expect(cell!.playerId).toBe('0')
})
it('does not place a cell on an occupied cell', () => {
const engine = createEngine()
engine.manager.addMove(5, 5, 0, 'X')
engine['handleCellClick'](5, 5)
expect(engine.board.getCell(5, 5)).toBeUndefined()
expect(engine.currentPlayer).toBe(0)
})
it('does not place a cell outside the board', () => {
const engine = createEngine()
engine['handleCellClick'](-1, 0)
engine['handleCellClick'](0, -1)
engine['handleCellClick'](15, 0)
engine['handleCellClick'](0, 15)
expect(engine.board.cells.size).toBe(0)
expect(engine.manager?.getBoard().size).toBe(0)
expect(engine.currentPlayer).toBe(0)
})
it('does not place a cell on malformed coordinates', () => {
const engine = createEngine()
engine['handleCellClick'](1.5, 1)
engine['handleCellClick'](Number.NaN, 1)
expect(engine.board.cells.size).toBe(0)
expect(engine.manager?.getBoard().size).toBe(0)
expect(engine.currentPlayer).toBe(0)
})
it('ignores out-of-bounds cells from remote updates', () => {
const engine = createEngine()
const cell = { playerId: '0', symbol: 'X', timestamp: 0 }
engine.symbols = { clear: vi.fn() } as any
engine.camera = { ensureCellsInView: vi.fn() } as any
engine.addCell(15, 0, cell)
engine.setCells({
'14:14': cell,
'15:0': cell,
'0:15': cell,
'-1:0': cell,
})
expect(engine.board.getCell(14, 14)).toEqual(cell)
expect(engine.board.getCell(15, 0)).toBeUndefined()
expect(engine.board.getCell(0, 15)).toBeUndefined()
expect(engine.board.getCell(-1, 0)).toBeUndefined()
})
it('uses correct symbol for each player', () => {
const engine = createEngine()
engine['handleCellClick'](0, 0)
const cell0 = engine.board.getCell(0, 0)
expect(cell0!.symbol).toBe('X')
engine['handleCellClick'](1, 0)
const cell1 = engine.board.getCell(1, 0)
expect(cell1!.symbol).toBe('O')
})
})
describe('swap2 rendering', () => {
it('clears existing symbols before redrawing swapped owner icons', () => {
const engine = createEngine()
engine.manager = new GameManager('P1', 'P2', ['X', 'O'], { swap2: true, noOverlines: true, boardSize: 15 })
engine.symbols = { clear: vi.fn() } as any
engine.manager.placeMove(7, 7)
engine.manager.placeMove(8, 7)
engine.manager.placeMove(9, 7)
engine.manager.chooseSwap(true)
engine.applySwapFromManager()
expect(engine.symbols.clear).toHaveBeenCalled()
expect(engine.board.getCell(7, 7)).toMatchObject({ playerId: '1', symbol: 'X' })
expect(engine.board.getCell(9, 7)).toMatchObject({ playerId: '0', symbol: 'O' })
})
})
})