generated from wafflestudio/seminar-2024-frontend-template
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* ➕ Setup vitest * ✅ Passed tests on 2048 game rules rule.ts, helpers.ts
- Loading branch information
1 parent
8a81ace
commit 82d330d
Showing
4 changed files
with
731 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,7 +12,9 @@ | |
"lint:check": "eslint .", | ||
"lint:fix": "eslint --fix .", | ||
"unused:check": "knip", | ||
"check-all": "yarn types:check && yarn format:check && yarn lint:check && yarn unused:check" | ||
"test": "vitest run", | ||
"test:watch": "vitest watch", | ||
"check-all": "yarn types:check && yarn format:check && yarn lint:check && yarn unused:check && yarn test" | ||
}, | ||
"dependencies": { | ||
"@vercel/analytics": "1.3.1", | ||
|
@@ -34,7 +36,8 @@ | |
"prettier-plugin-tailwindcss": "0.6.9", | ||
"tailwindcss": "3.4.11", | ||
"typescript": "5.6.2", | ||
"vite": "5.4.5" | ||
"vite": "5.4.5", | ||
"vitest": "2.1.5" | ||
}, | ||
"packageManager": "[email protected]" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
import { describe, expect, test } from 'vitest'; | ||
|
||
import type { Map2048 } from '@/constants'; | ||
import { | ||
addRandomBlock, | ||
moveLeft, | ||
rotateMapCounterClockwise, | ||
validateMapIsNByM, | ||
} from '@/utils/helpers'; | ||
|
||
describe('helpers.ts functions', () => { | ||
const emptyMap: Map2048 = [ | ||
[null, null, null, null], | ||
[null, null, null, null], | ||
[null, null, null, null], | ||
[null, null, null, null], | ||
]; | ||
|
||
describe('addRandomBlock', () => { | ||
test('should add a block to an empty map', () => { | ||
const newMap = addRandomBlock(emptyMap); | ||
const nonNullCells = newMap.flat().filter((cell) => cell !== null); | ||
expect(nonNullCells.length).toBe(1); | ||
expect(nonNullCells[0]).toBe(2); | ||
}); | ||
|
||
test('should not modify a full map', () => { | ||
const fullMap: Map2048 = [ | ||
[2, 4, 2, 4], | ||
[4, 2, 4, 2], | ||
[2, 4, 2, 4], | ||
[4, 2, 4, 2], | ||
]; | ||
const newMap = addRandomBlock(fullMap); | ||
expect(newMap).toEqual(fullMap); | ||
}); | ||
}); | ||
|
||
describe('moveLeft', () => { | ||
test('should move cells to the left', () => { | ||
const map: Map2048 = [ | ||
[null, 2, null, 2], | ||
[4, null, 4, null], | ||
[2, 2, 2, 2], | ||
[null, null, null, null], | ||
]; | ||
const { map: newMap, isMoved, newPoints } = moveLeft(map); | ||
const expectedMap: Map2048 = [ | ||
[4, null, null, null], | ||
[8, null, null, null], | ||
[4, 4, null, null], | ||
[null, null, null, null], | ||
]; | ||
expect(newMap).toEqual(expectedMap); | ||
expect(isMoved).toBe(true); | ||
expect(newPoints).toBe(20); | ||
}); | ||
|
||
test('should not move if no movement is possible', () => { | ||
const map: Map2048 = [ | ||
[2, 4, 8, 16], | ||
[32, 64, 128, 256], | ||
[512, 1024, 2048, 4096], | ||
[8192, 16384, 32768, 65536], | ||
]; | ||
const { map: newMap, isMoved, newPoints } = moveLeft(map); | ||
expect(newMap).toEqual(map); | ||
expect(isMoved).toBe(false); | ||
expect(newPoints).toBe(0); | ||
}); | ||
}); | ||
|
||
describe('validateMapIsNByM', () => { | ||
test('should return true for a valid map', () => { | ||
const isValid = validateMapIsNByM(emptyMap); | ||
expect(isValid).toBe(true); | ||
}); | ||
|
||
test('should return false for an invalid map', () => { | ||
const invalidMap: Map2048 = [ | ||
[null, null, null], | ||
[null, null], | ||
[null, null, null, null], | ||
]; | ||
const isValid = validateMapIsNByM(invalidMap); | ||
expect(isValid).toBe(false); | ||
}); | ||
}); | ||
|
||
describe('rotateMapCounterClockwise', () => { | ||
const map: Map2048 = [ | ||
[1, 2, 3], | ||
[4, 5, 6], | ||
[7, 8, 9], | ||
]; | ||
|
||
test('should rotate 90 degrees', () => { | ||
const rotatedMap = rotateMapCounterClockwise(map, 90); | ||
const expectedMap: Map2048 = [ | ||
[3, 6, 9], | ||
[2, 5, 8], | ||
[1, 4, 7], | ||
]; | ||
expect(rotatedMap).toEqual(expectedMap); | ||
}); | ||
|
||
test('should rotate 180 degrees', () => { | ||
const rotatedMap = rotateMapCounterClockwise(map, 180); | ||
const expectedMap: Map2048 = [ | ||
[9, 8, 7], | ||
[6, 5, 4], | ||
[3, 2, 1], | ||
]; | ||
expect(rotatedMap).toEqual(expectedMap); | ||
}); | ||
|
||
test('should rotate 270 degrees', () => { | ||
const rotatedMap = rotateMapCounterClockwise(map, 270); | ||
const expectedMap: Map2048 = [ | ||
[7, 4, 1], | ||
[8, 5, 2], | ||
[9, 6, 3], | ||
]; | ||
expect(rotatedMap).toEqual(expectedMap); | ||
}); | ||
|
||
test('should return the same map when rotation degree is 0', () => { | ||
const rotatedMap = rotateMapCounterClockwise(map, 0); | ||
expect(rotatedMap).toEqual(map); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,215 @@ | ||
import { describe, expect, test } from 'vitest'; | ||
|
||
import { Direction, type Map2048, type State2048 } from '@/constants'; | ||
import { getRule2048 } from '@/utils/rule'; | ||
|
||
describe('rule.ts functions', () => { | ||
const NUM_ROWS = 4; | ||
const NUM_COLS = 4; | ||
const WINNING_SCORE = 2048; | ||
|
||
const { resetGame, move } = getRule2048({ | ||
NUM_ROWS, | ||
NUM_COLS, | ||
WINNING_SCORE, | ||
}); | ||
|
||
describe('resetGame', () => { | ||
test('should initialize the game state correctly', () => { | ||
const initialState = resetGame(); | ||
expect(initialState.map.length).toBe(NUM_ROWS); | ||
initialState.map.forEach((row) => { | ||
expect(row.length).toBe(NUM_COLS); | ||
}); | ||
const nonNullCells = initialState.map | ||
.flat() | ||
.filter((cell) => cell !== null); | ||
expect(nonNullCells.length).toBe(2); | ||
expect(initialState.score).toBe(0); | ||
expect(initialState.bestScore).toBe(0); | ||
expect(initialState.gameStatus).toBe('playing'); | ||
}); | ||
}); | ||
|
||
describe('move', () => { | ||
describe('valid move for 4 directions', () => { | ||
test('should perform a valid move in direction Up', () => { | ||
const state: State2048 = { | ||
map: [ | ||
[2, null, null, null], | ||
[2, null, null, null], | ||
[null, null, null, null], | ||
[null, null, null, null], | ||
], | ||
score: 0, | ||
bestScore: 0, | ||
gameStatus: 'playing', | ||
}; | ||
const newState = move(state, Direction.Up); | ||
expect(newState.score).toBe(4); | ||
expect(newState.bestScore).toBe(4); | ||
expect(newState.map[0]?.[0]).toBe(4); | ||
const nonNullCells = newState.map | ||
.flat() | ||
.filter((cell) => cell !== null); | ||
expect(nonNullCells.length).toBe(2); // One merged tile + one new random tile | ||
}); | ||
|
||
test('should perform a valid move in direction Right', () => { | ||
const state: State2048 = { | ||
map: [ | ||
[2, 2, null, null], | ||
[null, null, null, null], | ||
[null, null, null, null], | ||
[null, null, null, null], | ||
], | ||
score: 0, | ||
bestScore: 0, | ||
gameStatus: 'playing', | ||
}; | ||
const newState = move(state, Direction.Right); | ||
expect(newState.score).toBe(4); | ||
expect(newState.bestScore).toBe(4); | ||
expect(newState.map[0]?.[3]).toBe(4); | ||
const nonNullCells = newState.map | ||
.flat() | ||
.filter((cell) => cell !== null); | ||
expect(nonNullCells.length).toBe(2); // One merged tile + one new random tile | ||
}); | ||
|
||
test('should perform a valid move in direction Down', () => { | ||
const state: State2048 = { | ||
map: [ | ||
[null, null, null, null], | ||
[null, null, null, null], | ||
[2, null, null, null], | ||
[2, null, null, null], | ||
], | ||
score: 0, | ||
bestScore: 0, | ||
gameStatus: 'playing', | ||
}; | ||
const newState = move(state, Direction.Down); | ||
expect(newState.score).toBe(4); | ||
expect(newState.bestScore).toBe(4); | ||
expect(newState.map[3]?.[0]).toBe(4); | ||
const nonNullCells = newState.map | ||
.flat() | ||
.filter((cell) => cell !== null); | ||
expect(nonNullCells.length).toBe(2); // One merged tile + one new random tile | ||
}); | ||
|
||
test('should perform a valid move in direction Left', () => { | ||
const state: State2048 = { | ||
map: [ | ||
[null, null, null, null], | ||
[null, null, null, null], | ||
[null, null, null, 2], | ||
[null, null, null, 2], | ||
], | ||
score: 0, | ||
bestScore: 0, | ||
gameStatus: 'playing', | ||
}; | ||
const newState = move(state, Direction.Left); | ||
expect(newState.score).toBe(0); | ||
expect(newState.bestScore).toBe(0); | ||
expect(newState.map[2]?.[0]).toBe(2); | ||
expect(newState.map[3]?.[0]).toBe(2); | ||
const nonNullCells = newState.map | ||
.flat() | ||
.filter((cell) => cell !== null); | ||
expect(nonNullCells.length).toBe(3); // One merged tile + one new random tile | ||
}); | ||
}); | ||
|
||
describe('if blocks cannot move', () => { | ||
test('should lose when board is full and map does not change', () => { | ||
const state: State2048 = { | ||
map: [ | ||
[2, 4, 2, 4], | ||
[4, 2, 4, 2], | ||
[2, 4, 2, 4], | ||
[4, 2, 4, 2], | ||
], | ||
score: 0, | ||
bestScore: 0, | ||
gameStatus: 'playing', | ||
}; | ||
const newState = move(state, Direction.Left); | ||
expect(newState.map).toEqual(state.map); | ||
expect(newState.gameStatus).toBe('lose'); | ||
}); | ||
|
||
test('should keep playing when board has empty cells and map does not change', () => { | ||
const state: State2048 = { | ||
map: [ | ||
[2, null, null, null], | ||
[2, null, null, null], | ||
[null, null, null, null], | ||
[null, null, null, null], | ||
], | ||
score: 0, | ||
bestScore: 0, | ||
gameStatus: 'playing', | ||
}; | ||
const newState = move(state, Direction.Left); | ||
expect(newState.map).toEqual(state.map); | ||
expect(newState.gameStatus).toBe('playing'); | ||
}); | ||
}); | ||
|
||
describe('gameStatus check', () => { | ||
test('should set gameStatus to "win" when WINNING_SCORE is reached', () => { | ||
const winningMap: Map2048 = [ | ||
[WINNING_SCORE / 2, WINNING_SCORE / 2, null, null], | ||
[null, null, null, null], | ||
[null, null, null, null], | ||
[null, null, null, null], | ||
]; | ||
const state: State2048 = { | ||
map: winningMap, | ||
score: 0, | ||
bestScore: 0, | ||
gameStatus: 'playing', | ||
}; | ||
const newState = move(state, Direction.Left); | ||
expect(newState.gameStatus).toBe('win'); | ||
}); | ||
|
||
test('should set gameStatus to "lose" when no moves are possible after move', () => { | ||
const noMoveMap: Map2048 = [ | ||
[2, 4, 2, 4], | ||
[4, 2, 4, 8], | ||
[2, 8, 2, 4], | ||
[4, 2, 4, 2], | ||
]; | ||
const state: State2048 = { | ||
map: noMoveMap, | ||
score: 0, | ||
bestScore: 0, | ||
gameStatus: 'playing', | ||
}; | ||
const newState = move(state, Direction.Left); | ||
expect(newState.gameStatus).toBe('lose'); | ||
}); | ||
}); | ||
|
||
describe('invalid map', () => { | ||
test('should throw an error if the map is invalid', () => { | ||
const invalidMap: Map2048 = [ | ||
[2, 2, null], | ||
[null, null], | ||
[null, null, null, null], | ||
]; | ||
const state: State2048 = { | ||
map: invalidMap, | ||
score: 0, | ||
bestScore: 0, | ||
gameStatus: 'playing', | ||
}; | ||
expect(() => move(state, Direction.Left)).toThrow('Map is not N by M'); | ||
}); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.