Skip to content

Commit

Permalink
✅ Test/util functions (#10)
Browse files Browse the repository at this point in the history
* ➕ Setup vitest

* ✅ Passed tests on 2048 game rules

rule.ts, helpers.ts
  • Loading branch information
thisisWooyeol authored Nov 23, 2024
1 parent 8a81ace commit 82d330d
Show file tree
Hide file tree
Showing 4 changed files with 731 additions and 3 deletions.
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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]"
}
132 changes: 132 additions & 0 deletions src/utils/helpers.spec.ts
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);
});
});
});
215 changes: 215 additions & 0 deletions src/utils/rule.spec.ts
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');
});
});
});
});
Loading

0 comments on commit 82d330d

Please sign in to comment.