(key: K, value: PacManViewOptions[K]) => void;
+}
+
+export type Store = StoreState & StoreActions;
diff --git a/src/model/updateEnergizerTimer.ts b/src/model/updateEnergizerTimer.ts
index efb01a1d..54869879 100644
--- a/src/model/updateEnergizerTimer.ts
+++ b/src/model/updateEnergizerTimer.ts
@@ -1,5 +1,42 @@
-import { Game } from './Game';
+import { useGameStore } from './store';
-export const updateEnergizerTimer = (game: Game) => {
- game.energizerTimer.advance(game.lastFrameLength);
+export const updateEnergizerTimer = () => {
+ const store = useGameStore.getState();
+ const timer = store.game.energizerTimer;
+
+ if (!timer.running) {
+ return;
+ }
+
+ const lastFrameLength = store.game.lastFrameLength;
+
+ useGameStore.setState((state) => {
+ state.game.energizerTimer.timeSpent += lastFrameLength;
+ });
+
+ // Check if timed out after the update
+ const updatedStore = useGameStore.getState();
+ const updatedTimer = updatedStore.game.energizerTimer;
+
+ if (updatedTimer.timeSpent >= updatedTimer.duration) {
+ // Handle energizer timed out
+ handleEnergizerTimedOut();
+
+ // Stop the timer
+ useGameStore.setState((state) => {
+ state.game.energizerTimer.running = false;
+ });
+ }
+};
+
+const handleEnergizerTimedOut = () => {
+ const store = useGameStore.getState();
+
+ // Send ENERGIZER_TIMED_OUT to PacMan
+ store.sendPacManEvent('ENERGIZER_TIMED_OUT');
+
+ // Send ENERGIZER_TIMED_OUT to all ghosts
+ store.game.ghosts.forEach((_, index) => {
+ store.sendGhostEvent(index, 'ENERGIZER_TIMED_OUT');
+ });
};
diff --git a/src/model/updateExternalTimeStamp.ts b/src/model/updateExternalTimeStamp.ts
deleted file mode 100644
index 2f305402..00000000
--- a/src/model/updateExternalTimeStamp.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { Game } from './Game';
-import { MilliSeconds } from './Types';
-
-// The typical duration of a frame: 1000ms for 60 frames per second = 17ms.
-export const TYPICAL_FRAME_LENGTH: MilliSeconds = 17;
-
-export const updateExternalTimestamp = ({
- game,
- externalTimeStamp,
-}: {
- game: Game;
- externalTimeStamp: number;
-}) => {
- if (game.externalTimeStamp === null) {
- // The very first frame
- // We cannot measure its duration. Therefore we have to make an assumption.
- game.lastFrameLength = TYPICAL_FRAME_LENGTH;
- } else {
- // A later frame.
- // We can calculate its duration.
- game.lastFrameLength = externalTimeStamp - game.externalTimeStamp;
- }
- game.externalTimeStamp = externalTimeStamp;
-};
diff --git a/src/model/updateGameTimestamp.ts b/src/model/updateGameTimestamp.ts
deleted file mode 100644
index 31a6d518..00000000
--- a/src/model/updateGameTimestamp.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { Game } from './Game';
-
-export const updateGameTimestamp = (game: Game) => {
- game.timestamp += game.lastFrameLength;
- game.frameCount++;
-};
diff --git a/src/model/updateGhostStatePhase.ts b/src/model/updateGhostStatePhase.ts
deleted file mode 100644
index ba7fb5f4..00000000
--- a/src/model/updateGhostStatePhase.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { action } from 'mobx';
-import { Ghost } from './Ghost';
-import { MilliSeconds } from './Types';
-import { StateValue } from 'xstate';
-
-export const CHASE_PHASE_LENGTH = 20 * 1000;
-export const SCATTER_PHASE_LENGTH = 7 * 1000;
-
-export const updateGhostStatePhaseTime = action(
- 'updateGhostStatePhaseTime',
- (ghost: Ghost) => {
- ghost.statePhaseTimer.advance(ghost.game.lastFrameLength);
- }
-);
-
-export const updateGhostStatePhase = action(
- 'updateGhostStatePhase',
- (ghost: Ghost) => {
- if (!ghost.atTileCenter) {
- return;
- }
-
- if (ghost.statePhaseTimer.isTimedOut) {
- ghost.send('PHASE_END');
- ghost.statePhaseTimer.setDuration(getStatePhaseLength(ghost.state));
- ghost.statePhaseTimer.restart();
- }
- }
-);
-
-export const getStatePhaseLength = (state: StateValue): MilliSeconds => {
- switch (state) {
- case 'chase':
- return CHASE_PHASE_LENGTH;
- case 'scatter':
- return SCATTER_PHASE_LENGTH;
- default:
- // Never ends
- return 9999999999;
- }
-};
diff --git a/src/model/updateGhosts.test.ts b/src/model/updateGhosts.test.ts
index 2024e528..7e3f6dec 100644
--- a/src/model/updateGhosts.test.ts
+++ b/src/model/updateGhosts.test.ts
@@ -1,4 +1,3 @@
-import { Game } from './Game';
import { onAnimationFrame } from './onAnimationFrame';
import {
getNewDirection,
@@ -7,220 +6,257 @@ import {
SPEED_FACTOR_SLOW,
} from './updateGhosts';
import { simulateFramesToMoveNTiles, simulateFrames } from './simulateFrames';
-import { Store } from './Store';
import { TILE_FOR_RETURNING_TO_BOX } from './chooseNewTargetTile';
-import { SCREEN_TILE_SIZE } from './Coordinates';
+import { SCREEN_TILE_SIZE, screenFromTile, tileFromScreen } from './Coordinates';
+import { useGameStore, createGhostData, createInitialState } from './store';
const MILLISECONDS_PER_FRAME = 17;
+const resetStore = () => {
+ useGameStore.setState(createInitialState());
+};
+
describe('updateGhost', () => {
+ beforeEach(() => {
+ resetStore();
+ });
+
describe('getNewDirection()', () => {
it('returns the new direction to take', () => {
- const store = new Store();
- const game = new Game(store);
+ const store = useGameStore.getState();
- const ghost = game.ghosts[0];
- ghost.send('PHASE_END');
+ // Set ghost 0 to chase state
+ store.sendGhostEvent(0, 'PHASE_END');
+ let ghost = useGameStore.getState().game.ghosts[0];
expect(ghost.state).toBe('chase');
- ghost.targetTile = { x: 6, y: 1 };
- ghost.setTileCoordinates({ x: 1, y: 1 });
- ghost.direction = 'UP';
- expect(getNewDirection(ghost)).toBe('RIGHT');
+
+ // Set target tile and position
+ useGameStore.setState((state) => {
+ state.game.ghosts[0].targetTile = { x: 6, y: 1 };
+ state.game.ghosts[0].screenCoordinates = screenFromTile({ x: 1, y: 1 });
+ state.game.ghosts[0].direction = 'UP';
+ });
+
+ const ghostData = createGhostData(0);
+ expect(getNewDirection(ghostData)).toBe('RIGHT');
});
it('walks throught the tunnel to the RIGHT', () => {
// Arrange
+ const store = useGameStore.getState();
- const store = new Store();
- const game = new Game(store);
-
- const ghost = game.ghosts[0];
- ghost.send('PHASE_END');
+ store.sendGhostEvent(0, 'PHASE_END');
+ let ghost = useGameStore.getState().game.ghosts[0];
expect(ghost.state).toBe('chase');
- ghost.targetTile = { x: 4, y: 14 };
- ghost.setTileCoordinates({ x: 27, y: 14 });
- ghost.direction = 'RIGHT';
+
+ useGameStore.setState((state) => {
+ state.game.ghosts[0].targetTile = { x: 4, y: 14 };
+ state.game.ghosts[0].screenCoordinates = screenFromTile({ x: 27, y: 14 });
+ state.game.ghosts[0].direction = 'RIGHT';
+ });
// Act / Asset
- expect(getNewDirection(ghost)).toBe('RIGHT');
+ let ghostData = createGhostData(0);
+ expect(getNewDirection(ghostData)).toBe('RIGHT');
// Arrange
-
- ghost.setTileCoordinates({ x: 26, y: 14 });
- ghost.direction = 'RIGHT';
- expect(getNewDirection(ghost)).toBe('RIGHT');
+ useGameStore.setState((state) => {
+ state.game.ghosts[0].screenCoordinates = screenFromTile({ x: 26, y: 14 });
+ state.game.ghosts[0].direction = 'RIGHT';
+ });
+ ghostData = createGhostData(0);
+ expect(getNewDirection(ghostData)).toBe('RIGHT');
});
it('walks throught the tunnel to the LEFT', () => {
// Arrange
+ const store = useGameStore.getState();
- const store = new Store();
- const game = new Game(store);
-
- const ghost = game.ghosts[0];
- ghost.send('PHASE_END');
+ store.sendGhostEvent(0, 'PHASE_END');
+ let ghost = useGameStore.getState().game.ghosts[0];
expect(ghost.state).toBe('chase');
- ghost.targetTile = { x: 26, y: 14 };
- ghost.setTileCoordinates({ x: 0, y: 14 });
- ghost.direction = 'LEFT';
+
+ useGameStore.setState((state) => {
+ state.game.ghosts[0].targetTile = { x: 26, y: 14 };
+ state.game.ghosts[0].screenCoordinates = screenFromTile({ x: 0, y: 14 });
+ state.game.ghosts[0].direction = 'LEFT';
+ });
// Act / Asset
- expect(getNewDirection(ghost)).toBe('LEFT');
+ let ghostData = createGhostData(0);
+ expect(getNewDirection(ghostData)).toBe('LEFT');
// Arrange
-
- ghost.setTileCoordinates({ x: 1, y: 14 });
- ghost.direction = 'LEFT';
- expect(getNewDirection(ghost)).toBe('LEFT');
+ useGameStore.setState((state) => {
+ state.game.ghosts[0].screenCoordinates = screenFromTile({ x: 1, y: 14 });
+ state.game.ghosts[0].direction = 'LEFT';
+ });
+ ghostData = createGhostData(0);
+ expect(getNewDirection(ghostData)).toBe('LEFT');
});
});
describe('updateGhost()', () => {
it('advances ghost positions', () => {
// Arrange
+ useGameStore.setState((state) => {
+ state.game.pacMan.screenCoordinates = screenFromTile({ x: 1, y: 1 });
+ state.game.pacMan.direction = 'LEFT';
+ state.game.pacMan.nextDirection = 'LEFT';
+ });
- const store = new Store();
- const game = new Game(store);
+ const pacMan = useGameStore.getState().game.pacMan;
+ expect(pacMan.screenCoordinates.x).toBe(20);
- game.pacMan.setTileCoordinates({ x: 1, y: 1 });
- expect(game.pacMan.screenCoordinates.x).toBe(20);
- game.pacMan.direction = 'LEFT';
- game.pacMan.nextDirection = 'LEFT';
+ const store = useGameStore.getState();
+ store.sendGhostEvent(0, 'PHASE_END');
+ expect(useGameStore.getState().game.ghosts[0].state).toBe('chase');
- const ghost = game.ghosts[0];
+ useGameStore.setState((state) => {
+ state.game.ghosts[0].screenCoordinates = screenFromTile({ x: 1, y: 3 });
+ state.game.ghosts[0].direction = 'UP';
+ });
- ghost.send('PHASE_END');
- expect(ghost.state).toBe('chase');
- ghost.setTileCoordinates({ x: 1, y: 3 });
- ghost.direction = 'UP';
+ const ghost = useGameStore.getState().game.ghosts[0];
expect(ghost.screenCoordinates).toEqual({ x: 20, y: 60 });
// Act
- simulateFrames(1, game);
+ simulateFrames(1);
// Assert
- expect(ghost.screenCoordinates).toEqual({ x: 20, y: 58 });
+ const updatedGhost = useGameStore.getState().game.ghosts[0];
+ expect(updatedGhost.screenCoordinates).toEqual({ x: 20, y: 58 });
});
describe('in chase state', () => {
it('lets ghost 0 chase pacman', () => {
// Arrange
+ useGameStore.setState((state) => {
+ state.game.pacMan.screenCoordinates = screenFromTile({ x: 1, y: 8 });
+ state.game.pacMan.direction = 'LEFT';
+ state.game.pacMan.nextDirection = 'LEFT';
+ });
- const store = new Store();
- const game = new Game(store);
-
- game.pacMan.setTileCoordinates({ x: 1, y: 8 });
+ const store = useGameStore.getState();
+ store.sendGhostEvent(0, 'PHASE_END');
- game.pacMan.direction = 'LEFT';
- game.pacMan.nextDirection = 'LEFT';
+ useGameStore.setState((state) => {
+ state.game.ghosts[0].direction = 'LEFT';
+ state.game.ghosts[0].screenCoordinates = screenFromTile({ x: 3, y: 5 });
+ });
- const ghost = game.ghosts[0];
- ghost.send('PHASE_END');
- ghost.direction = 'LEFT';
+ let ghost = useGameStore.getState().game.ghosts[0];
expect(ghost.state).toBe('chase');
-
- ghost.setTileCoordinates({ x: 3, y: 5 });
-
expect(ghost.direction).toBe('LEFT');
// Act
- simulateFramesToMoveNTiles(1, game);
+ simulateFramesToMoveNTiles(1);
+ ghost = useGameStore.getState().game.ghosts[0];
expect(ghost.direction).toBe('LEFT');
- expect(ghost.tileCoordinates).toEqual({ x: 2, y: 5 });
+ expect(tileFromScreen(ghost.screenCoordinates)).toEqual({ x: 2, y: 5 });
expect(ghost.targetTile).toEqual({ x: 1, y: 8 });
- expect(ghost.wayPoints).toEqual([
- { x: 2, y: 5 },
- { x: 1, y: 5 },
- { x: 1, y: 6 },
- { x: 1, y: 7 },
- { x: 1, y: 8 },
- ]);
+
expect(ghost.direction).toBe('LEFT');
expect(ghost.state).toBe('chase');
- simulateFramesToMoveNTiles(1, game);
- expect(ghost.tileCoordinates).toEqual({ x: 1, y: 5 });
+ simulateFramesToMoveNTiles(1);
+ ghost = useGameStore.getState().game.ghosts[0];
+ expect(tileFromScreen(ghost.screenCoordinates)).toEqual({ x: 1, y: 5 });
- simulateFramesToMoveNTiles(1, game);
- expect(ghost.tileCoordinates).toEqual({ x: 1, y: 6 });
+ simulateFramesToMoveNTiles(1);
+ ghost = useGameStore.getState().game.ghosts[0];
+ expect(tileFromScreen(ghost.screenCoordinates)).toEqual({ x: 1, y: 6 });
- simulateFramesToMoveNTiles(1, game);
- expect(ghost.tileCoordinates).toEqual({ x: 1, y: 7 });
+ simulateFramesToMoveNTiles(1);
+ ghost = useGameStore.getState().game.ghosts[0];
+ expect(tileFromScreen(ghost.screenCoordinates)).toEqual({ x: 1, y: 7 });
- simulateFramesToMoveNTiles(1, game);
- expect(ghost.tileCoordinates).toEqual({ x: 1, y: 7 });
+ simulateFramesToMoveNTiles(1);
+ ghost = useGameStore.getState().game.ghosts[0];
+ expect(tileFromScreen(ghost.screenCoordinates)).toEqual({ x: 1, y: 7 });
// Assert
// We had a collision with pac man
- expect(game.pacMan.state).toBe('dead');
+ const pacMan = useGameStore.getState().game.pacMan;
+ expect(pacMan.state).toBe('dead');
+ ghost = useGameStore.getState().game.ghosts[0];
expect(ghost.state).toBe('scatter');
});
it('lets ghost 0 go through the tunnel', () => {
// Arrange
+ useGameStore.setState((state) => {
+ state.game.pacMan.screenCoordinates = screenFromTile({ x: 27, y: 14 });
+ state.game.pacMan.direction = 'RIGHT';
+ state.game.pacMan.nextDirection = 'RIGHT';
+ });
- const store = new Store();
- const game = new Game(store);
-
- game.pacMan.setTileCoordinates({ x: 27, y: 14 });
+ const store = useGameStore.getState();
+ store.sendGhostEvent(0, 'PHASE_END');
+ expect(useGameStore.getState().game.ghosts[0].state).toBe('chase');
- game.pacMan.direction = 'RIGHT';
- game.pacMan.nextDirection = 'RIGHT';
-
- const ghost = game.ghosts[0];
- ghost.send('PHASE_END');
- expect(ghost.state).toBe('chase');
-
- ghost.setTileCoordinates({ x: 25, y: 14 });
- ghost.direction = 'RIGHT';
+ useGameStore.setState((state) => {
+ state.game.ghosts[0].screenCoordinates = screenFromTile({ x: 25, y: 14 });
+ state.game.ghosts[0].direction = 'RIGHT';
+ });
// Act
- onAnimationFrame({ game, timestamp: MILLISECONDS_PER_FRAME });
+ onAnimationFrame(MILLISECONDS_PER_FRAME);
// Assert
+ let ghost = useGameStore.getState().game.ghosts[0];
expect(ghost.targetTile).toEqual({ x: 27, y: 14 });
- expect(ghost.tileCoordinates).toEqual({ x: 25, y: 14 });
+ expect(tileFromScreen(ghost.screenCoordinates)).toEqual({ x: 25, y: 14 });
expect(ghost.direction).toBe('RIGHT');
// Act
- simulateFramesToMoveNTiles(1, game);
+ simulateFramesToMoveNTiles(1);
// Assert
+ ghost = useGameStore.getState().game.ghosts[0];
expect(ghost.direction).toBe('RIGHT');
- expect(ghost.tileCoordinates).toEqual({ x: 25, y: 14 });
+ expect(tileFromScreen(ghost.screenCoordinates)).toEqual({ x: 25, y: 14 });
// Act
- simulateFramesToMoveNTiles(2, game);
+ simulateFramesToMoveNTiles(2);
// Assert
- expect(ghost.tileCoordinates).toEqual({ x: 26, y: 14 });
+ ghost = useGameStore.getState().game.ghosts[0];
+ expect(tileFromScreen(ghost.screenCoordinates)).toEqual({ x: 26, y: 14 });
// Act
+ // Make sure we stay in the current state phase, which is "chase"
+ useGameStore.setState((state) => {
+ const timer = state.game.ghosts[0].statePhaseTimer;
+ timer.timeSpent = 0;
+ });
- // Make sure we stay in the current state phase, which is "chase"
- ghost.statePhaseTimer.restart();
-
- simulateFramesToMoveNTiles(1, game);
- simulateFramesToMoveNTiles(1, game);
+ simulateFramesToMoveNTiles(1);
+ simulateFramesToMoveNTiles(1);
// Assert
- expect(ghost.tileCoordinates).toEqual({ x: 27, y: 14 });
+ ghost = useGameStore.getState().game.ghosts[0];
+ const pacMan = useGameStore.getState().game.pacMan;
+ expect(tileFromScreen(ghost.screenCoordinates)).toEqual({ x: 27, y: 14 });
expect(ghost.direction).toBe('RIGHT');
- expect(game.pacMan.tileCoordinates).toEqual({ x: 4, y: 14 });
+ expect(tileFromScreen(pacMan.screenCoordinates)).toEqual({ x: 4, y: 14 });
expect(ghost.targetTile).toEqual({ x: 3, y: 14 });
// Act
- simulateFramesToMoveNTiles(2, game);
+ simulateFramesToMoveNTiles(2);
+ ghost = useGameStore.getState().game.ghosts[0];
expect(ghost.state).toBe('chase');
- expect(game.pacMan.tileCoordinates).toEqual({ x: 6, y: 14 });
+ const pacManAfter = useGameStore.getState().game.pacMan;
+ expect(tileFromScreen(pacManAfter.screenCoordinates)).toEqual({ x: 6, y: 14 });
// Assert
+ ghost = useGameStore.getState().game.ghosts[0];
expect(ghost.targetTile).toEqual({ x: 5, y: 14 });
- expect(ghost.tileCoordinates).toEqual({ x: 0, y: 14 });
+ expect(tileFromScreen(ghost.screenCoordinates)).toEqual({ x: 0, y: 14 });
expect(ghost.direction).toBe('RIGHT');
- expect(game.pacMan.tileCoordinates).toEqual({ x: 6, y: 14 });
+ expect(tileFromScreen(pacManAfter.screenCoordinates)).toEqual({ x: 6, y: 14 });
expect(ghost.state).toBe('chase');
});
});
@@ -228,33 +264,31 @@ describe('updateGhost', () => {
describe('in scatter state', () => {
it('lets ghost 0 go to the lower right corner', () => {
// Arrange
+ useGameStore.setState((state) => {
+ state.game.pacMan.screenCoordinates = screenFromTile({ x: 1, y: 8 });
+ state.game.pacMan.direction = 'LEFT';
+ state.game.pacMan.nextDirection = 'LEFT';
+ });
- const store = new Store();
- const game = new Game(store);
-
- game.pacMan.setTileCoordinates({ x: 1, y: 8 });
- game.pacMan.direction = 'LEFT';
- game.pacMan.nextDirection = 'LEFT';
-
- const ghost = game.ghosts[0];
+ let ghost = useGameStore.getState().game.ghosts[0];
expect(ghost.state).toBe('scatter');
- ghost.setTileCoordinates({ x: 24, y: 1 });
- ghost.direction = 'RIGHT';
+
+ useGameStore.setState((state) => {
+ state.game.ghosts[0].screenCoordinates = screenFromTile({ x: 24, y: 1 });
+ state.game.ghosts[0].direction = 'RIGHT';
+ });
// Act
- onAnimationFrame({ game, timestamp: MILLISECONDS_PER_FRAME });
+ onAnimationFrame(MILLISECONDS_PER_FRAME);
+ ghost = useGameStore.getState().game.ghosts[0];
expect(ghost.targetTile).toEqual({ x: 26, y: 1 });
- expect(ghost.wayPoints).toEqual([
- { x: 24, y: 1 },
- { x: 25, y: 1 },
- { x: 26, y: 1 },
- ]);
- expect(ghost.tileCoordinates).toEqual({ x: 24, y: 1 });
- simulateFramesToMoveNTiles(2, game);
+ expect(tileFromScreen(ghost.screenCoordinates)).toEqual({ x: 24, y: 1 });
+ simulateFramesToMoveNTiles(2);
- expect(ghost.tileCoordinates).toEqual({ x: 26, y: 1 });
+ ghost = useGameStore.getState().game.ghosts[0];
+ expect(tileFromScreen(ghost.screenCoordinates)).toEqual({ x: 26, y: 1 });
expect(ghost.direction).toBe('DOWN');
});
});
@@ -262,62 +296,54 @@ describe('updateGhost', () => {
describe('in dead state', () => {
it('lets ghost go into the box', () => {
// Arrange
+ useGameStore.setState((state) => {
+ state.game.pacMan.screenCoordinates = screenFromTile({ x: 1, y: 8 });
+ state.game.pacMan.direction = 'LEFT';
+ state.game.pacMan.nextDirection = 'LEFT';
+ state.game.ghosts[0].direction = 'RIGHT';
+ });
- const store = new Store();
- const game = new Game(store);
- const ghost = game.ghosts[0];
-
- game.pacMan.setTileCoordinates({ x: 1, y: 8 });
- game.pacMan.direction = 'LEFT';
- game.pacMan.nextDirection = 'LEFT';
+ const store = useGameStore.getState();
+ store.sendGhostEvent(0, 'ENERGIZER_EATEN');
+ store.sendGhostEvent(0, 'COLLISION_WITH_PAC_MAN');
+ expect(useGameStore.getState().game.ghosts[0].state).toBe('dead');
- ghost.direction = 'RIGHT';
-
- ghost.send('ENERGIZER_EATEN');
- ghost.send('COLLISION_WITH_PAC_MAN');
- expect(ghost.state).toBe('dead');
- ghost.setTileCoordinates({ x: 12, y: 11 });
+ useGameStore.setState((state) => {
+ state.game.ghosts[0].screenCoordinates = screenFromTile({ x: 12, y: 11 });
+ });
- expect(ghost.atTileCenter).toBeTruthy();
+ let ghostData = createGhostData(0);
+ expect(ghostData.atTileCenter).toBeTruthy();
const simulateFramesToMoveGhostOneTile = () => {
- simulateFramesToMoveNTiles(0.5, game);
- expect(ghost.atTileCenter).toBeTruthy();
+ simulateFramesToMoveNTiles(0.5);
+ ghostData = createGhostData(0);
+ expect(ghostData.atTileCenter).toBeTruthy();
};
// Act
simulateFramesToMoveGhostOneTile();
- expect(ghost.tileCoordinates).toEqual({ x: 13, y: 11 });
+ let ghost = useGameStore.getState().game.ghosts[0];
+ expect(tileFromScreen(ghost.screenCoordinates)).toEqual({ x: 13, y: 11 });
simulateFramesToMoveGhostOneTile();
- expect(ghost.tileCoordinates).toEqual({ x: 13, y: 12 });
+ ghost = useGameStore.getState().game.ghosts[0];
+ expect(tileFromScreen(ghost.screenCoordinates)).toEqual({ x: 13, y: 12 });
expect(ghost.targetTile).toEqual(TILE_FOR_RETURNING_TO_BOX);
- expect(ghost.wayPoints).toEqual([
- {
- x: 13,
- y: 12,
- },
- {
- x: 13,
- y: 13,
- },
- {
- x: 13,
- y: 14,
- },
- TILE_FOR_RETURNING_TO_BOX,
- ]);
// Act
simulateFramesToMoveGhostOneTile();
- expect(ghost.tileCoordinates).toEqual({ x: 13, y: 13 });
+ ghost = useGameStore.getState().game.ghosts[0];
+ expect(tileFromScreen(ghost.screenCoordinates)).toEqual({ x: 13, y: 13 });
simulateFramesToMoveGhostOneTile();
- expect(ghost.tileCoordinates).toEqual({ x: 13, y: 14 });
+ ghost = useGameStore.getState().game.ghosts[0];
+ expect(tileFromScreen(ghost.screenCoordinates)).toEqual({ x: 13, y: 14 });
simulateFramesToMoveGhostOneTile();
- expect(ghost.tileCoordinates).toEqual(TILE_FOR_RETURNING_TO_BOX);
+ ghost = useGameStore.getState().game.ghosts[0];
+ expect(tileFromScreen(ghost.screenCoordinates)).toEqual(TILE_FOR_RETURNING_TO_BOX);
expect(ghost.state).toBe('dead');
});
@@ -326,9 +352,7 @@ describe('updateGhost', () => {
describe('speed factors', () => {
const isValidSpeedFactor = (speedFactor: number): boolean => {
- const store = new Store();
- const game = new Game(store);
- const gameSpeed = game.speed;
+ const gameSpeed = useGameStore.getState().game.speed;
const ghostSpeed = gameSpeed * speedFactor;
return SCREEN_TILE_SIZE % ghostSpeed === 0;
};
diff --git a/src/model/updateGhosts.ts b/src/model/updateGhosts.ts
index da41afd0..c65fe234 100644
--- a/src/model/updateGhosts.ts
+++ b/src/model/updateGhosts.ts
@@ -1,81 +1,183 @@
-import { chooseNewTargetTile } from './chooseNewTargetTile';
+import { chooseNewTargetTile, GhostTargetingContext } from './chooseNewTargetTile';
import { chooseNextTile } from './chooseNextTile';
import {
TileCoordinates,
MAZE_WIDTH_IN_SCREEN_COORDINATES,
MAZE_HEIGHT_IN_SCREEN_COORDINATES,
- assertValidTileCoordinates,
+ tileFromScreen,
} from './Coordinates';
import { getDirectionFromTileToTile } from './getDirectionFromTileToTile';
-import { Ghost } from './Ghost';
import { Direction } from './Types';
-import { directionToVector } from './Ways';
-import {
- updateGhostStatePhaseTime,
- updateGhostStatePhase,
-} from './updateGhostStatePhase';
+import { directionToVector, DIRECTION_TO_OPPOSITE_DIRECTION } from './Ways';
import { Vector } from './Vector';
-import { Game } from './Game';
-import { action } from 'mobx';
-
-export const updateGhosts = (game: Game) => {
- for (const ghost of game.ghosts) {
- updateGhost({ ghost });
+import {
+ useGameStore,
+ createGhostData,
+ GhostData,
+ getStatePhaseLength,
+} from './store';
+
+export const updateGhosts = () => {
+ const store = useGameStore.getState();
+ const ghostCount = store.game.ghosts.length;
+
+ for (let i = 0; i < ghostCount; i++) {
+ updateGhost(i);
}
};
-const updateGhost = ({ ghost }: { ghost: Ghost }) => {
+const updateGhost = (ghostIndex: number) => {
+ const store = useGameStore.getState();
+ const ghost = store.game.ghosts[ghostIndex];
+
if (ghost.ghostPaused) {
return;
}
- updateGhostStatePhaseTime(ghost);
- updateDeadWaitingTimeInBoxLeft(ghost);
+ updateGhostStatePhaseTime(ghostIndex);
+ updateDeadWaitingTimeInBoxLeft(ghostIndex);
+ updateGhostStatePhase(ghostIndex);
+ routeAndMoveGhost(ghostIndex);
+};
- updateGhostStatePhase(ghost);
+const updateGhostStatePhaseTime = (ghostIndex: number) => {
+ const store = useGameStore.getState();
+ const lastFrameLength = store.game.lastFrameLength;
- routeAndMoveGhost(ghost);
+ useGameStore.setState((state) => {
+ const timer = state.game.ghosts[ghostIndex].statePhaseTimer;
+ if (timer.running) {
+ timer.timeSpent += lastFrameLength;
+ }
+ });
};
-const updateDeadWaitingTimeInBoxLeft = (ghost: Ghost) => {
- if (ghost.dead && ghost.deadWaitingTimeInBoxLeft > 0) {
- ghost.deadWaitingTimeInBoxLeft -= ghost.game.lastFrameLength;
+const updateDeadWaitingTimeInBoxLeft = (ghostIndex: number) => {
+ const store = useGameStore.getState();
+ const ghost = store.game.ghosts[ghostIndex];
+ const isDead = ghost.state === 'dead';
+
+ if (isDead && ghost.deadWaitingTimeInBoxLeft > 0) {
+ useGameStore.setState((state) => {
+ state.game.ghosts[ghostIndex].deadWaitingTimeInBoxLeft -= state.game.lastFrameLength;
+ });
}
};
-export const routeAndMoveGhost = (ghost: Ghost) => {
- if (ghost.game.pacMan.dead) {
+const updateGhostStatePhase = (ghostIndex: number) => {
+ const ghostData = createGhostData(ghostIndex);
+
+ if (!ghostData.atTileCenter) {
+ return;
+ }
+
+ const timer = ghostData.statePhaseTimer;
+ const isTimedOut = timer.timeSpent >= timer.duration;
+
+ if (isTimedOut) {
+ // Send PHASE_END event - this will trigger direction change for scatter<->chase
+ const store = useGameStore.getState();
+ const ghost = store.game.ghosts[ghostIndex];
+ const currentState = ghost.state;
+
+ store.sendGhostEvent(ghostIndex, 'PHASE_END');
+
+ // If transitioning between scatter and chase, change direction to opposite
+ const updatedGhost = useGameStore.getState().game.ghosts[ghostIndex];
+ if (
+ (currentState === 'scatter' && updatedGhost.state === 'chase') ||
+ (currentState === 'chase' && updatedGhost.state === 'scatter')
+ ) {
+ useGameStore.setState((state) => {
+ const g = state.game.ghosts[ghostIndex];
+ g.direction = DIRECTION_TO_OPPOSITE_DIRECTION[g.direction];
+ });
+ }
+
+ // Restart timer with new duration
+ const newDuration = getStatePhaseLength(updatedGhost.state);
+ useGameStore.setState((state) => {
+ const timer = state.game.ghosts[ghostIndex].statePhaseTimer;
+ timer.duration = newDuration;
+ timer.running = true;
+ timer.timeSpent = 0;
+ });
+ }
+};
+
+const routeAndMoveGhost = (ghostIndex: number) => {
+ const store = useGameStore.getState();
+ const pacManDead = store.game.pacMan.state === 'dead';
+
+ if (pacManDead) {
return;
}
- if (ghost.atTileCenter) {
- reRouteGhost(ghost);
+ const ghostData = createGhostData(ghostIndex);
+
+ if (ghostData.atTileCenter) {
+ reRouteGhost(ghostIndex, ghostData);
}
- moveGhost(ghost);
+ moveGhost(ghostIndex);
};
-const reRouteGhost = (ghost: Ghost) => {
- ghost.targetTile = chooseNewTargetTile(ghost);
- updateDirection(ghost);
- updateSpeed(ghost);
+const reRouteGhost = (ghostIndex: number, ghostData: GhostData) => {
+ const newTargetTile = chooseNewTargetTileForGhost(ghostIndex, ghostData);
+
+ useGameStore.setState((state) => {
+ state.game.ghosts[ghostIndex].targetTile = newTargetTile;
+ });
+
+ updateDirection(ghostIndex);
+ updateSpeed(ghostIndex);
};
-const updateDirection = (ghost: Ghost) => {
- const newDirection = getNewDirection(ghost);
- ghost.direction = newDirection;
+const chooseNewTargetTileForGhost = (ghostIndex: number, ghostData: GhostData): TileCoordinates => {
+ const store = useGameStore.getState();
+ const game = store.game;
+
+ const ctx: GhostTargetingContext = {
+ ghost: {
+ state: ghostData.state,
+ ghostNumber: ghostData.ghostNumber,
+ tileCoordinates: ghostData.tileCoordinates,
+ direction: ghostData.direction,
+ isInsideBoxWalls: ghostData.isInsideBoxWalls,
+ },
+ pacMan: {
+ tileCoordinates: tileFromScreen(game.pacMan.screenCoordinates),
+ direction: game.pacMan.direction,
+ },
+ blinkyTileCoordinates: tileFromScreen(game.ghosts[0].screenCoordinates),
+ };
+
+ return chooseNewTargetTile(ctx);
};
-const updateSpeed = (ghost: Ghost) => {
- const newSpeedFactor = getNewSpeedFactor(ghost);
- ghost.speedFactor = newSpeedFactor;
+const updateDirection = (ghostIndex: number) => {
+ const ghostData = createGhostData(ghostIndex);
+ const newDirection = getNewDirection(ghostData);
+
+ useGameStore.setState((state) => {
+ state.game.ghosts[ghostIndex].direction = newDirection;
+ });
};
-export const getNewDirection = (ghost: Ghost): Direction => {
- const currentTile = ghost.tileCoordinates;
- const currentDirection = ghost.direction;
- const targetTile = ghost.targetTile;
- const boxDoorIsOpen = ghost.canPassThroughBoxDoor;
+const updateSpeed = (ghostIndex: number) => {
+ const ghostData = createGhostData(ghostIndex);
+ const newSpeedFactor = getNewSpeedFactor(ghostData);
+
+ useGameStore.setState((state) => {
+ state.game.ghosts[ghostIndex].speedFactor = newSpeedFactor;
+ });
+};
+
+export const getNewDirection = (ghostData: GhostData): Direction => {
+ const currentTile = ghostData.tileCoordinates;
+ const currentDirection = ghostData.direction;
+ const targetTile = ghostData.targetTile;
+ const boxDoorIsOpen = ghostData.canPassThroughBoxDoor;
const nextTile: TileCoordinates = chooseNextTile({
currentTile,
@@ -87,40 +189,35 @@ export const getNewDirection = (ghost: Ghost): Direction => {
return getDirectionFromTileToTile(currentTile, nextTile);
};
-const moveGhost = (ghost: Ghost) => {
- const vector: Vector = getGhostMovementVector(ghost);
- moveGhostBy(ghost, vector);
+const moveGhost = (ghostIndex: number) => {
+ const store = useGameStore.getState();
+ const ghost = store.game.ghosts[ghostIndex];
+ const speed = store.game.speed * ghost.speedFactor;
+ const vector: Vector = directionToVector(ghost.direction, speed);
+
+ useGameStore.setState((state) => {
+ const g = state.game.ghosts[ghostIndex];
+ g.screenCoordinates.x =
+ (g.screenCoordinates.x + vector.x + MAZE_WIDTH_IN_SCREEN_COORDINATES) %
+ MAZE_WIDTH_IN_SCREEN_COORDINATES;
+ g.screenCoordinates.y =
+ (g.screenCoordinates.y + vector.y + MAZE_HEIGHT_IN_SCREEN_COORDINATES) %
+ MAZE_HEIGHT_IN_SCREEN_COORDINATES;
+ });
};
-const moveGhostBy = action((ghost: Ghost, vector: Vector) => {
- ghost.screenCoordinates.x =
- (ghost.screenCoordinates.x + vector.x + MAZE_WIDTH_IN_SCREEN_COORDINATES) %
- MAZE_WIDTH_IN_SCREEN_COORDINATES;
- ghost.screenCoordinates.y =
- (ghost.screenCoordinates.y + vector.y + MAZE_HEIGHT_IN_SCREEN_COORDINATES) %
- MAZE_HEIGHT_IN_SCREEN_COORDINATES;
-
- assertValidTileCoordinates(ghost.tileCoordinates);
-});
-
const isInTunnel = (tile: TileCoordinates) =>
tile.y === 14 && (tile.x >= 22 || tile.x <= 5);
-const getGhostMovementVector = (ghost: Ghost): Vector => {
- const speed = ghost.game.speed * ghost.speedFactor;
- const velocity = directionToVector(ghost.direction, speed);
- return velocity;
-};
-
export const SPEED_FACTOR_HIGH = 2;
export const SPEED_FACTOR_NORMAL = 1;
export const SPEED_FACTOR_SLOW = 0.5;
-const getNewSpeedFactor = (ghost: Ghost): number => {
- if (ghost.dead) {
+const getNewSpeedFactor = (ghostData: GhostData): number => {
+ if (ghostData.dead) {
return SPEED_FACTOR_HIGH;
}
- if (isInTunnel(ghost.tileCoordinates) || ghost.state === 'frightened') {
+ if (isInTunnel(ghostData.tileCoordinates) || ghostData.state === 'frightened') {
return SPEED_FACTOR_SLOW;
}
return SPEED_FACTOR_NORMAL;
diff --git a/src/model/updatePacMan.test.ts b/src/model/updatePacMan.test.ts
index 57c6085f..8e1afa0b 100644
--- a/src/model/updatePacMan.test.ts
+++ b/src/model/updatePacMan.test.ts
@@ -1,127 +1,164 @@
import { screenFromTile } from './Coordinates';
import { BASIC_PILL_POINTS, ghostCollidesWithPacMan } from './detectCollisions';
-import { Game, DEFAULT_SPEED } from './Game';
-import { Ghost } from './Ghost';
import { BASIC_PILL_ID, EMPTY_TILE_ID } from './MazeData';
import { simulateFrames, simulateFrame, simulateTime } from './simulateFrames';
import { DELAY_TO_REVIVE_PAC_MAN } from './updatePacMan';
-import { Store } from './Store';
-import { TYPICAL_FRAME_LENGTH } from './updateExternalTimeStamp';
+import { TYPICAL_FRAME_LENGTH } from './onAnimationFrame';
import {
PacManDyingPhaseLength,
getPacManDyingPhase,
} from './pacManDyingPhase';
+import { useGameStore, createInitialState } from './store';
+
+// Add DEFAULT_SPEED to store constants
+const GAME_DEFAULT_SPEED = 2;
+
+// Helper to reset store before each test
+const resetStore = () => {
+ useGameStore.setState(createInitialState());
+};
+
+// Helper to set pacman position
+const setPacManTileCoordinates = (tile: { x: number; y: number }) => {
+ useGameStore.setState((state) => {
+ state.game.pacMan.screenCoordinates = screenFromTile(tile);
+ });
+};
+
+// Helper to set ghost position
+const setGhostTileCoordinates = (ghostIndex: number, tile: { x: number; y: number }) => {
+ useGameStore.setState((state) => {
+ state.game.ghosts[ghostIndex].screenCoordinates = screenFromTile(tile);
+ });
+};
+
+// Helper to get game state
+const getGame = () => useGameStore.getState().game;
+
+// Helper to get PacMan state
+const getPacMan = () => useGameStore.getState().game.pacMan;
+
+// Helper to get timestamp
+const getTimestamp = () => useGameStore.getState().game.timestamp;
+
+// Helper to compute time since death
+const getTimeSinceDeath = () => {
+ const state = useGameStore.getState();
+ const pacMan = state.game.pacMan;
+ if (pacMan.state !== 'dead') return 0;
+ return state.game.timestamp - pacMan.diedAtTimestamp;
+};
+
+// Helper to check if game is over
+const isGameOver = () => {
+ const pacMan = getPacMan();
+ return pacMan.state === 'dead' && pacMan.extraLivesLeft === 0;
+};
describe('updatePacMan()', () => {
+ beforeEach(() => {
+ resetStore();
+ });
+
it('advances PacMans position', () => {
// Arrange
- const store = new Store();
- const game = new Game(store);
- game.pacMan.setTileCoordinates({ x: 1, y: 1 });
- expect(game.pacMan.screenCoordinates.x).toBe(20);
- game.pacMan.direction = 'RIGHT';
- game.pacMan.nextDirection = 'RIGHT';
+ setPacManTileCoordinates({ x: 1, y: 1 });
+ expect(getPacMan().screenCoordinates.x).toBe(20);
+ useGameStore.setState((state) => {
+ state.game.pacMan.direction = 'RIGHT';
+ state.game.pacMan.nextDirection = 'RIGHT';
+ });
// Act
- simulateFrames(1, game);
+ simulateFrames(1);
// Assert
- expect(game.pacMan.screenCoordinates.x).toBe(20 + DEFAULT_SPEED);
+ expect(getPacMan().screenCoordinates.x).toBe(20 + GAME_DEFAULT_SPEED);
// Act
- simulateFrames(1, game);
+ simulateFrames(1);
// Assert
- expect(game.pacMan.screenCoordinates.x).toBe(20 + 2 * DEFAULT_SPEED);
+ expect(getPacMan().screenCoordinates.x).toBe(20 + 2 * GAME_DEFAULT_SPEED);
});
it('stops pac man once he is dead', () => {
// Arrange
- const store = new Store();
- const game = new Game(store);
- game.pacMan.setTileCoordinates({ x: 1, y: 1 });
- expect(game.pacMan.screenCoordinates.x).toBe(20);
- game.pacMan.direction = 'RIGHT';
- game.pacMan.nextDirection = 'RIGHT';
+ setPacManTileCoordinates({ x: 1, y: 1 });
+ expect(getPacMan().screenCoordinates.x).toBe(20);
+ useGameStore.setState((state) => {
+ state.game.pacMan.direction = 'RIGHT';
+ state.game.pacMan.nextDirection = 'RIGHT';
+ });
// Act
- game.pacMan.send('COLLISION_WITH_GHOST');
- simulateFrames(1, game);
+ useGameStore.getState().sendPacManEvent('COLLISION_WITH_GHOST');
+ simulateFrames(1);
// Assert
- expect(game.pacMan.screenCoordinates.x).toBe(20);
+ expect(getPacMan().screenCoordinates.x).toBe(20);
});
it('stops PacMan when he hits a wall', () => {
// Arrange
- const store = new Store();
- const game = new Game(store);
- game.pacMan.setTileCoordinates({ x: 1, y: 1 });
- expect(game.pacMan.screenCoordinates.x).toBe(20);
- game.pacMan.direction = 'LEFT';
- game.pacMan.nextDirection = 'LEFT';
+ setPacManTileCoordinates({ x: 1, y: 1 });
+ expect(getPacMan().screenCoordinates.x).toBe(20);
+ useGameStore.setState((state) => {
+ state.game.pacMan.direction = 'LEFT';
+ state.game.pacMan.nextDirection = 'LEFT';
+ });
// Act
- simulateFrames(1, game);
+ simulateFrames(1);
// Assert
- expect(game.pacMan.screenCoordinates.x).toBe(20);
+ expect(getPacMan().screenCoordinates.x).toBe(20);
});
- it('changes PacMans direction once he reachs a tile center and the the way is free', () => {
+ it('changes PacMans direction once he reachs a tile center and the way is free', () => {
// Arrange
- const store = new Store();
- const game = new Game(store);
- game.pacMan.screenCoordinates = { x: 22, y: 20 };
- game.pacMan.direction = 'LEFT';
- game.pacMan.nextDirection = 'DOWN';
+ useGameStore.setState((state) => {
+ state.game.pacMan.screenCoordinates = { x: 22, y: 20 };
+ state.game.pacMan.direction = 'LEFT';
+ state.game.pacMan.nextDirection = 'DOWN';
+ });
// Act
- simulateFrames(1, game);
+ simulateFrames(1);
// Assert
- expect(game.pacMan.screenCoordinates.x).toBe(20);
- expect(game.pacMan.screenCoordinates.x).toBe(20);
- expect(game.pacMan.direction).toBe('LEFT');
+ expect(getPacMan().screenCoordinates.x).toBe(20);
+ expect(getPacMan().direction).toBe('LEFT');
// Act
- simulateFrames(1, game);
+ simulateFrames(1);
// Assert
- expect(game.pacMan.direction).toBe('DOWN');
-
- expect(game.pacMan.screenCoordinates.x).toBe(20);
- expect(game.pacMan.screenCoordinates.y).toBe(22);
+ expect(getPacMan().direction).toBe('DOWN');
+ expect(getPacMan().screenCoordinates.x).toBe(20);
+ expect(getPacMan().screenCoordinates.y).toBe(22);
});
it('lets pac man eat basic pills', () => {
// Arrange
const BASIC_PILL_TILE = { x: 9, y: 20 };
- const store = new Store();
- const game = new Game(store);
- game.pacMan.setTileCoordinates(BASIC_PILL_TILE);
- game.pacMan.direction = 'DOWN';
- game.pacMan.nextDirection = 'DOWN';
-
- expect(game.maze.pills[BASIC_PILL_TILE.y][BASIC_PILL_TILE.x]).toBe(
- BASIC_PILL_ID
- );
+ setPacManTileCoordinates(BASIC_PILL_TILE);
+ useGameStore.setState((state) => {
+ state.game.pacMan.direction = 'DOWN';
+ state.game.pacMan.nextDirection = 'DOWN';
+ });
- expect(game.score).toBe(0);
+ expect(getGame().maze.pills[BASIC_PILL_TILE.y][BASIC_PILL_TILE.x]).toBe(BASIC_PILL_ID);
+ expect(getGame().score).toBe(0);
// Act
- simulateFrames(1, game);
+ simulateFrames(1);
// Assert
- expect(game.score).toBe(BASIC_PILL_POINTS);
- expect(game.pacMan.screenCoordinates).toEqual(
- screenFromTile(BASIC_PILL_TILE)
- );
-
- expect(game.maze.pills[BASIC_PILL_TILE.y][BASIC_PILL_TILE.x]).toBe(
- EMPTY_TILE_ID
- );
+ expect(getGame().score).toBe(BASIC_PILL_POINTS);
+ expect(getPacMan().screenCoordinates).toEqual(screenFromTile(BASIC_PILL_TILE));
+ expect(getGame().maze.pills[BASIC_PILL_TILE.y][BASIC_PILL_TILE.x]).toBe(EMPTY_TILE_ID);
});
it('lets pac man die from meeting a ghost', () => {
@@ -129,103 +166,107 @@ describe('updatePacMan()', () => {
const GHOST_TX = 1;
const GHOST_TY = 1;
- const store = new Store();
- const game = new Game(store);
- const ghost: Ghost = game.ghosts[0];
- ghost.setTileCoordinates({ x: GHOST_TX, y: GHOST_TY });
- ghost.ghostPaused = true;
- game.pacMan.setTileCoordinates({ x: GHOST_TX, y: GHOST_TY + 1 });
- game.pacMan.direction = 'UP';
- game.pacMan.nextDirection = 'UP';
+ setGhostTileCoordinates(0, { x: GHOST_TX, y: GHOST_TY });
+ useGameStore.setState((state) => {
+ state.game.ghosts[0].ghostPaused = true;
+ });
+ setPacManTileCoordinates({ x: GHOST_TX, y: GHOST_TY + 1 });
+ useGameStore.setState((state) => {
+ state.game.pacMan.direction = 'UP';
+ state.game.pacMan.nextDirection = 'UP';
+ });
// Act
- simulateFrames(10, game);
+ simulateFrames(10);
// Assert
- expect(game.pacMan.state).toBe('dead');
- expect(game.pacMan.timeSinceDeath > 0).toBeTruthy();
+ expect(getPacMan().state).toBe('dead');
+ expect(getTimeSinceDeath() > 0).toBeTruthy();
});
it('animates pac mans death', () => {
// Arrange
- const store = new Store();
- const game = new Game(store);
- const { pacMan } = game;
- pacMan.setTileCoordinates({ x: 1, y: 1 });
- pacMan.direction = 'UP';
- pacMan.nextDirection = 'UP';
- killPacMan(game);
+ setPacManTileCoordinates({ x: 1, y: 1 });
+ useGameStore.setState((state) => {
+ state.game.pacMan.direction = 'UP';
+ state.game.pacMan.nextDirection = 'UP';
+ });
+ killPacMan();
- expect(getPacManDyingPhase(pacMan)).toBe(0);
+ expect(getPacManDyingPhase(getTimeSinceDeath())).toBe(0);
- simulateFrame(game);
+ simulateFrame();
- expect(getPacManDyingPhase(pacMan)).toBe(0);
+ expect(getPacManDyingPhase(getTimeSinceDeath())).toBe(0);
// Act
- expect(game.timestamp).toBe(TYPICAL_FRAME_LENGTH);
- simulateTime(game, PacManDyingPhaseLength - TYPICAL_FRAME_LENGTH);
+ expect(getTimestamp()).toBe(TYPICAL_FRAME_LENGTH);
+ simulateTime(PacManDyingPhaseLength - TYPICAL_FRAME_LENGTH);
- expect(game.timestamp).toBe(PacManDyingPhaseLength);
+ expect(getTimestamp()).toBe(PacManDyingPhaseLength);
// Assert
- expect(getPacManDyingPhase(pacMan)).toBe(1);
+ expect(getPacManDyingPhase(getTimeSinceDeath())).toBe(1);
// Act
- simulateTime(game, PacManDyingPhaseLength);
+ simulateTime(PacManDyingPhaseLength);
// Assert
- expect(getPacManDyingPhase(pacMan)).toBe(2);
+ expect(getPacManDyingPhase(getTimeSinceDeath())).toBe(2);
});
describe('with some lives left', () => {
it('revives pac man after his death', () => {
// Arrange
- const store = new Store();
- const game = new Game(store);
- game.timestamp = 1;
- game.pacMan.setTileCoordinates({ x: 1, y: 1 });
- game.pacMan.direction = 'UP';
- game.pacMan.nextDirection = 'UP';
- game.pacMan.extraLivesLeft = 2;
- killPacMan(game);
+ useGameStore.setState((state) => {
+ state.game.timestamp = 1;
+ state.game.pacMan.extraLivesLeft = 2;
+ });
+ setPacManTileCoordinates({ x: 1, y: 1 });
+ useGameStore.setState((state) => {
+ state.game.pacMan.direction = 'UP';
+ state.game.pacMan.nextDirection = 'UP';
+ });
+ killPacMan();
// Act
- simulateTime(game, DELAY_TO_REVIVE_PAC_MAN);
+ simulateTime(DELAY_TO_REVIVE_PAC_MAN);
// Assert
- expect(game.pacMan.state).not.toBe('dead');
- expect(game.pacMan.state).toBe('eating');
- expect(game.pacMan.diedAtTimestamp).toBe(-1);
- expect(game.ghosts[0].ghostPaused).toBeFalsy();
- expect(game.ghosts[1].ghostPaused).toBeFalsy();
- expect(game.pacMan.extraLivesLeft).toBe(1);
+ expect(getPacMan().state).not.toBe('dead');
+ expect(getPacMan().state).toBe('eating');
+ expect(getPacMan().diedAtTimestamp).toBe(-1);
+ expect(getGame().ghosts[0].ghostPaused).toBeFalsy();
+ expect(getGame().ghosts[1].ghostPaused).toBeFalsy();
+ expect(getPacMan().extraLivesLeft).toBe(1);
});
});
describe('with all lives lost', () => {
it('hides pac man and the ghosts and shows game over', () => {
// Arrange
- const store = new Store();
- const game = new Game(store);
- game.timestamp = 1;
- game.pacMan.setTileCoordinates({ x: 1, y: 1 });
- game.pacMan.direction = 'UP';
- game.pacMan.nextDirection = 'UP';
- game.pacMan.extraLivesLeft = 0;
+ useGameStore.setState((state) => {
+ state.game.timestamp = 1;
+ state.game.pacMan.extraLivesLeft = 0;
+ });
+ setPacManTileCoordinates({ x: 1, y: 1 });
+ useGameStore.setState((state) => {
+ state.game.pacMan.direction = 'UP';
+ state.game.pacMan.nextDirection = 'UP';
+ });
// Act
- killPacMan(game);
- simulateFrames(TYPICAL_FRAME_LENGTH + DELAY_TO_REVIVE_PAC_MAN, game);
+ killPacMan();
+ simulateFrames(TYPICAL_FRAME_LENGTH + DELAY_TO_REVIVE_PAC_MAN);
// Assert
- expect(game.pacMan.state).toBe('dead');
- expect(game.gameOver).toBeTruthy();
+ expect(getPacMan().state).toBe('dead');
+ expect(isGameOver()).toBeTruthy();
});
});
});
-const killPacMan = (game: Game) => {
- ghostCollidesWithPacMan(game.ghosts[0]);
- expect(game.pacMan.state).toBe('dead');
+const killPacMan = () => {
+ ghostCollidesWithPacMan(0);
+ expect(getPacMan().state).toBe('dead');
};
diff --git a/src/model/updatePacMan.ts b/src/model/updatePacMan.ts
index 48af5f19..6fe12489 100644
--- a/src/model/updatePacMan.ts
+++ b/src/model/updatePacMan.ts
@@ -1,7 +1,10 @@
-import { ScreenCoordinates, tileFromScreen } from './Coordinates';
-import { Game } from './Game';
-import { movePacManBy } from './movePacManBy';
-import { PacMan } from './PacMan';
+import {
+ ScreenCoordinates,
+ tileFromScreen,
+ MAZE_WIDTH_IN_SCREEN_COORDINATES,
+ screenFromTile,
+} from './Coordinates';
+import { useGameStore, INITIAL_PACMAN_STATE } from './store';
import { MilliSeconds } from './Types';
import {
directionToVector as directionAsVector,
@@ -9,19 +12,28 @@ import {
isWayFreeInDirection,
} from './Ways';
import { TotalPacManDyingAnimationLength } from './pacManDyingPhase';
+import { Vector } from './Vector';
+import { getStatePhaseLength } from './store/gameStore';
+import { INITIAL_GHOST_STATE } from './store/types';
export const DELAY_TO_REVIVE_PAC_MAN: MilliSeconds = TotalPacManDyingAnimationLength;
-export const updatePacMan = (game: Game): void => {
- const pacMan = game.pacMan;
- if (pacMan.alive) {
- updateLivingPacMan(pacMan);
+export const updatePacMan = (): void => {
+ const store = useGameStore.getState();
+ const pacMan = store.game.pacMan;
+ const isDead = pacMan.state === 'dead';
+
+ if (!isDead) {
+ updateLivingPacMan();
} else {
- updateDeadPacMan(pacMan);
+ updateDeadPacMan();
}
};
-const updateLivingPacMan = (pacMan: PacMan) => {
+const updateLivingPacMan = () => {
+ const store = useGameStore.getState();
+ const pacMan = store.game.pacMan;
+
if (isTileCenter(pacMan.screenCoordinates)) {
const tile = tileFromScreen(pacMan.screenCoordinates);
@@ -30,34 +42,96 @@ const updateLivingPacMan = (pacMan: PacMan) => {
pacMan.direction !== pacMan.nextDirection &&
isWayFreeInDirection(tile, pacMan.nextDirection)
) {
- pacMan.direction = pacMan.nextDirection;
+ useGameStore.setState((state) => {
+ state.game.pacMan.direction = state.game.pacMan.nextDirection;
+ });
}
+ // Get updated direction after potential change
+ const updatedPacMan = useGameStore.getState().game.pacMan;
+
// Move
- if (isWayFreeInDirection(tile, pacMan.direction)) {
- movePacMan(pacMan);
+ if (isWayFreeInDirection(tile, updatedPacMan.direction)) {
+ movePacMan();
}
} else {
- movePacMan(pacMan);
+ movePacMan();
}
};
-const movePacMan = (pacMan: PacMan): void => {
- const speed = pacMan.game.speed;
+const movePacMan = (): void => {
+ const store = useGameStore.getState();
+ const pacMan = store.game.pacMan;
+ const speed = store.game.speed;
const delta: ScreenCoordinates = directionAsVector(pacMan.direction, speed);
- movePacManBy(pacMan, delta);
+ movePacManBy(delta);
};
-const updateDeadPacMan = (pacMan: PacMan) => {
- if (pacMan.timeSinceDeath >= TotalPacManDyingAnimationLength) {
- revivePacMan(pacMan);
+const movePacManBy = (vector: Vector) => {
+ useGameStore.setState((state) => {
+ const pacMan = state.game.pacMan;
+ pacMan.screenCoordinates.x =
+ (pacMan.screenCoordinates.x + vector.x + MAZE_WIDTH_IN_SCREEN_COORDINATES) %
+ MAZE_WIDTH_IN_SCREEN_COORDINATES;
+ pacMan.screenCoordinates.y += vector.y;
+ });
+};
+
+const updateDeadPacMan = () => {
+ const store = useGameStore.getState();
+ const pacMan = store.game.pacMan;
+ const timeSinceDeath = store.game.timestamp - pacMan.diedAtTimestamp;
+
+ if (timeSinceDeath >= TotalPacManDyingAnimationLength) {
+ revivePacMan();
}
- return;
};
-const revivePacMan = (pacMan: PacMan) => {
+const revivePacMan = () => {
+ const store = useGameStore.getState();
+ const pacMan = store.game.pacMan;
+
if (pacMan.extraLivesLeft > 0) {
- pacMan.extraLivesLeft -= 1;
- pacMan.game.revivePacMan();
+ useGameStore.setState((state) => {
+ state.game.pacMan.extraLivesLeft -= 1;
+ });
+ doRevivePacMan();
}
};
+
+const doRevivePacMan = () => {
+ const store = useGameStore.getState();
+
+ // Send REVIVED event to pacman
+ store.sendPacManEvent('REVIVED');
+
+ // Reset game timestamp and pacman position
+ useGameStore.setState((state) => {
+ state.game.timestamp = 0;
+ // Reset pacman
+ state.game.pacMan.diedAtTimestamp = -1;
+ state.game.pacMan.state = INITIAL_PACMAN_STATE;
+ state.game.pacMan.screenCoordinates = screenFromTile({ x: 14, y: 23 });
+ state.game.pacMan.nextDirection = 'LEFT';
+ state.game.pacMan.direction = 'LEFT';
+
+ // Reset ghosts
+ const ghostPositions = [
+ { x: 12, y: 14, direction: 'LEFT' as const },
+ { x: 13, y: 14, direction: 'RIGHT' as const },
+ { x: 14, y: 14, direction: 'LEFT' as const },
+ { x: 15, y: 14, direction: 'RIGHT' as const },
+ ];
+
+ state.game.ghosts.forEach((ghost, index) => {
+ ghost.screenCoordinates = screenFromTile(ghostPositions[index]);
+ ghost.direction = ghostPositions[index].direction;
+ ghost.ghostPaused = false;
+ ghost.state = INITIAL_GHOST_STATE;
+ ghost.statePhaseTimer.duration = getStatePhaseLength(INITIAL_GHOST_STATE);
+ ghost.statePhaseTimer.running = true;
+ ghost.statePhaseTimer.timeSpent = 0;
+ ghost.stateChanges++;
+ });
+ });
+};
diff --git a/src/model/useGameLoop.ts b/src/model/useGameLoop.ts
index 2ce7ab07..db210d80 100644
--- a/src/model/useGameLoop.ts
+++ b/src/model/useGameLoop.ts
@@ -1,13 +1,9 @@
-import { useStore } from '../components/StoreContext';
import { onAnimationFrame } from './onAnimationFrame';
import { useAnimationLoop } from './useAnimationLoop';
export const useGameLoop = () => {
- const store = useStore();
-
const animationStep = (timestamp: number) => {
- const { game } = store;
- onAnimationFrame({ game, timestamp });
+ onAnimationFrame(timestamp);
};
useAnimationLoop(animationStep);
diff --git a/src/pages/GamePage/GamePage.tsx b/src/pages/GamePage/GamePage.tsx
index acb7a7a5..7619f0ff 100644
--- a/src/pages/GamePage/GamePage.tsx
+++ b/src/pages/GamePage/GamePage.tsx
@@ -1,5 +1,4 @@
import { Row } from 'antd';
-import { observer } from 'mobx-react-lite';
import React, { useEffect } from 'react';
import styled from 'styled-components/macro';
import { Board } from '../../components/Board';
@@ -11,17 +10,19 @@ import { MazeView } from './components/MazeView';
import { PacManView } from './components/PacManView';
import { PillsView } from './components/PillsView';
import { Score } from './components/Score';
-import { useStore } from '../../components/StoreContext';
+import { useGameStore } from '../../model/store';
import { useKeyboardActions } from './components/useKeyboardActions';
import { VSpace } from '../../components/Spacer';
import { useGameLoop } from '../../model/useGameLoop';
-export const GamePage: React.FC = observer(() => {
- const store = useStore();
+export const GamePage: React.FC = () => {
+ const resetGame = useGameStore((state) => state.resetGame);
+ const setGamePaused = useGameStore((state) => state.setGamePaused);
+
useEffect(() => {
- store.resetGame();
+ resetGame();
return () => {
- store.game.gamePaused = true;
+ setGamePaused(true);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -59,7 +60,7 @@ export const GamePage: React.FC = observer(() => {
);
-});
+};
const Layout = styled.div`
margin-left: 16px;
diff --git a/src/pages/GamePage/components/EnergizerDebugView.tsx b/src/pages/GamePage/components/EnergizerDebugView.tsx
index 23a34452..dbd61506 100644
--- a/src/pages/GamePage/components/EnergizerDebugView.tsx
+++ b/src/pages/GamePage/components/EnergizerDebugView.tsx
@@ -1,8 +1,7 @@
import { Card, Button, Row, Col } from 'antd';
-import { observer } from 'mobx-react-lite';
-import React from 'react';
+import React, { FC } from 'react';
import styled from 'styled-components/macro';
-import { useGame } from '../../../components/StoreContext';
+import { useGameStore } from '../../../model/store';
import { eatEnergizer } from '../../../model/eatEnergizer';
const formatter = new Intl.NumberFormat('en-US', {
@@ -10,42 +9,40 @@ const formatter = new Intl.NumberFormat('en-US', {
maximumFractionDigits: 1,
});
-export const EnergizerDebugView = observer<{ className?: string }>(
- ({ className }) => {
- const game = useGame();
- return (
-
-
-
-
-
- Time left:{' '}
- {formatter.format(
- Math.abs(game.energizerTimer.timeLeft) / 1000
- )}{' '}
- seconds
-
-
+export const EnergizerDebugView: FC<{ className?: string }> = ({ className }) => {
+ const energizerTimer = useGameStore((state) => state.game.energizerTimer);
+ const timeLeft = energizerTimer.duration - energizerTimer.timeSpent;
-
+ return (
+
+
+
+
+
+ Time left:{' '}
+ {formatter.format(Math.abs(timeLeft) / 1000)}{' '}
+ seconds
+
+
-
- {
- eatEnergizer(game);
- }}
- >
- Eat
-
-
-
-
-
- );
- }
-);
+
+
+
+ {
+ eatEnergizer();
+ }}
+ >
+ Eat
+
+
+
+
+
+ );
+};
const Layout = styled.div``;
diff --git a/src/pages/GamePage/components/ExtraLives.tsx b/src/pages/GamePage/components/ExtraLives.tsx
index 0f3fe575..474cda8e 100644
--- a/src/pages/GamePage/components/ExtraLives.tsx
+++ b/src/pages/GamePage/components/ExtraLives.tsx
@@ -1,18 +1,18 @@
-import { observer } from 'mobx-react-lite';
-import React from 'react';
-import { useGame } from '../../../components/StoreContext';
+import React, { FC } from 'react';
+import { useGameStore } from '../../../model/store';
import classNames from 'classnames';
import styled from 'styled-components/macro';
import { PacManSprite } from './PacManView';
import { times } from 'lodash';
import { SCALE_FACTOR } from '../../../model/Coordinates';
-export const ExtraLives = observer<{ className?: string }>(({ className }) => {
- const game = useGame();
+export const ExtraLives: FC<{ className?: string }> = ({ className }) => {
+ const extraLivesLeft = useGameStore((state) => state.game.pacMan.extraLivesLeft);
+
return (
- {times(game.pacMan.extraLivesLeft, n => (
+ {times(extraLivesLeft, n => (
(({ className }) => {
);
-});
+};
const Layout = styled.div`
display: inline-flex;
diff --git a/src/pages/GamePage/components/FPS.tsx b/src/pages/GamePage/components/FPS.tsx
index 6030fb08..3a4f90dd 100644
--- a/src/pages/GamePage/components/FPS.tsx
+++ b/src/pages/GamePage/components/FPS.tsx
@@ -1,16 +1,15 @@
-import { observer } from 'mobx-react-lite';
-import React from 'react';
-import { useGame } from '../../../components/StoreContext';
+import React, { FC } from 'react';
+import { useGameStore } from '../../../model/store';
import styled from 'styled-components/macro';
-export const FPS = observer<{ className?: string }>(({ className }) => {
- const store = useGame();
+export const FPS: FC<{ className?: string }> = ({ className }) => {
+ const lastFrameLength = useGameStore((state) => state.game.lastFrameLength);
return (
- {Math.round(1000 / store.lastFrameLength)} FPS
+ {Math.round(1000 / lastFrameLength)} FPS
);
-});
+};
const Layout = styled.div`
margin-top: 12px;
diff --git a/src/pages/GamePage/components/GameDebugView.tsx b/src/pages/GamePage/components/GameDebugView.tsx
index 2b8f9757..9b67b3b9 100644
--- a/src/pages/GamePage/components/GameDebugView.tsx
+++ b/src/pages/GamePage/components/GameDebugView.tsx
@@ -1,56 +1,52 @@
/* eslint-disable react/display-name */
import { Button, Card, Col, Row, Switch, Typography } from 'antd';
-import { observer } from 'mobx-react-lite';
-import React from 'react';
+import React, { FC } from 'react';
import styled from 'styled-components/macro';
-import { useGame, useStore } from '../../../components/StoreContext';
-import { action } from 'mobx';
+import { useGameStore } from '../../../model/store';
const { Text } = Typography;
-export const GameDebugView = observer<{ className?: string }>(
- ({ className }) => {
- const store = useStore();
- const game = useGame();
- return (
-
-
-
-
- (store.debugState.gameViewOptions.hitBox = checked)
- )}
- />
-
-
- Show Hit Boxes
-
-
+export const GameDebugView: FC<{ className?: string }> = ({ className }) => {
+ const showHitBox = useGameStore((state) => state.debugState.gameViewOptions.hitBox);
+ const gamePaused = useGameStore((state) => state.game.gamePaused);
+ const setGameViewOption = useGameStore((state) => state.setGameViewOption);
+ const setGamePaused = useGameStore((state) => state.setGamePaused);
+ const resetGame = useGameStore((state) => state.resetGame);
-
- {
- game.gamePaused = checked;
- }}
- />
-
-
- Paused
-
-
+ return (
+
+
+
+
+ setGameViewOption('hitBox', checked)}
+ />
+
+
+ Show Hit Boxes
+
+
-
- Restart
-
-
-
-
- );
- }
-);
+
+ setGamePaused(checked)}
+ />
+
+
+ Paused
+
+
+
+ resetGame()} shape="round">
+ Restart
+
+
+
+
+ );
+};
const Layout = styled.div``;
diff --git a/src/pages/GamePage/components/GameOver.tsx b/src/pages/GamePage/components/GameOver.tsx
index dd32229e..08506687 100644
--- a/src/pages/GamePage/components/GameOver.tsx
+++ b/src/pages/GamePage/components/GameOver.tsx
@@ -1,19 +1,21 @@
-import { observer } from 'mobx-react-lite';
import React, { FC } from 'react';
import './GameOver.css';
-import { useGame } from '../../../components/StoreContext';
+import { useGameStore } from '../../../model/store';
import { Message } from './Message';
import { TotalPacManDyingAnimationLength } from '../../../model/pacManDyingPhase';
export const TOTAL_TIME_TO_GAME_OVER_MESSAGE = TotalPacManDyingAnimationLength;
-export const GameOver: FC<{ className?: string }> = observer(
- ({ className }) => {
- const game = useGame();
- const { pacMan } = game;
- const gameOverMessageVisible =
- game.gameOver && pacMan.timeSinceDeath >= TOTAL_TIME_TO_GAME_OVER_MESSAGE;
+export const GameOver: FC<{ className?: string }> = ({ className }) => {
+ const pacManState = useGameStore((state) => state.game.pacMan.state);
+ const extraLivesLeft = useGameStore((state) => state.game.pacMan.extraLivesLeft);
+ const diedAtTimestamp = useGameStore((state) => state.game.pacMan.diedAtTimestamp);
+ const timestamp = useGameStore((state) => state.game.timestamp);
- return gameOverMessageVisible ? : null;
- }
-);
+ const isDead = pacManState === 'dead';
+ const isGameOver = isDead && extraLivesLeft === 0;
+ const timeSinceDeath = isDead ? timestamp - diedAtTimestamp : 0;
+ const gameOverMessageVisible = isGameOver && timeSinceDeath >= TOTAL_TIME_TO_GAME_OVER_MESSAGE;
+
+ return gameOverMessageVisible ? : null;
+};
diff --git a/src/pages/GamePage/components/GhostDebugControls.tsx b/src/pages/GamePage/components/GhostDebugControls.tsx
index 46f00f9f..fb9c3384 100644
--- a/src/pages/GamePage/components/GhostDebugControls.tsx
+++ b/src/pages/GamePage/components/GhostDebugControls.tsx
@@ -1,22 +1,21 @@
import { Col, Row, Switch, Typography } from 'antd';
-import { action } from 'mobx';
-import { observer } from 'mobx-react-lite';
-import React from 'react';
-import { useStore } from '../../../components/StoreContext';
+import React, { FC } from 'react';
+import { useGameStore } from '../../../model/store';
const { Text } = Typography;
-export const GhostDebugControls = observer(() => {
- const store = useStore();
+export const GhostDebugControls: FC = () => {
+ const showTarget = useGameStore((state) => state.debugState.ghostViewOptions.target);
+ const showWayPoints = useGameStore((state) => state.debugState.ghostViewOptions.wayPoints);
+ const setGhostViewOption = useGameStore((state) => state.setGhostViewOption);
+
return (
{
- store.debugState.ghostViewOptions.target = checked;
- })}
+ checked={showTarget}
+ onChange={(checked) => setGhostViewOption('target', checked)}
/>
@@ -26,10 +25,8 @@ export const GhostDebugControls = observer(() => {
{
- store.debugState.ghostViewOptions.wayPoints = checked;
- })}
+ checked={showWayPoints}
+ onChange={(checked) => setGhostViewOption('wayPoints', checked)}
/>
@@ -39,4 +36,4 @@ export const GhostDebugControls = observer(() => {
);
-});
+};
diff --git a/src/pages/GamePage/components/GhostDebugTable.tsx b/src/pages/GamePage/components/GhostDebugTable.tsx
index 317c483b..80284bce 100644
--- a/src/pages/GamePage/components/GhostDebugTable.tsx
+++ b/src/pages/GamePage/components/GhostDebugTable.tsx
@@ -1,16 +1,31 @@
/* eslint-disable react/display-name */
import { Button, Row, Switch, Table } from 'antd';
import { ColumnsType } from 'antd/lib/table';
-import { action } from 'mobx';
-import { observer, Observer } from 'mobx-react-lite';
import React, { FC } from 'react';
import styled from 'styled-components';
import { ghostCollidesWithPacMan } from '../../../model/detectCollisions';
-import { Ghost } from '../../../model/Ghost';
-import { routeAndMoveGhost } from '../../../model/updateGhosts';
-import { useGame } from '../../../components/StoreContext';
+import { useGameStore, GhostState } from '../../../model/store';
+import { tileFromScreen } from '../../../model/Coordinates';
-const columns: ColumnsType = [
+// Create a wrapper component for each ghost row to get fresh data
+const GhostStateCell: FC<{ ghostIndex: number; render: (ghost: GhostState) => React.ReactNode }> = ({ ghostIndex, render }) => {
+ const ghost = useGameStore((state) => state.game.ghosts[ghostIndex]);
+ return <>{render(ghost)}>;
+};
+
+const TileXCell: FC<{ ghostIndex: number }> = ({ ghostIndex }) => {
+ const ghost = useGameStore((state) => state.game.ghosts[ghostIndex]);
+ const tile = tileFromScreen(ghost.screenCoordinates);
+ return <>{tile.x}>;
+};
+
+const TileYCell: FC<{ ghostIndex: number }> = ({ ghostIndex }) => {
+ const ghost = useGameStore((state) => state.game.ghosts[ghostIndex]);
+ const tile = tileFromScreen(ghost.screenCoordinates);
+ return <>{tile.y}>;
+};
+
+const columns: ColumnsType = [
{
title: 'Nr',
dataIndex: 'ghostNumber',
@@ -20,7 +35,7 @@ const columns: ColumnsType = [
{
title: 'Name',
width: 80,
- render: (ghost: Ghost) => (
+ render: (_, ghost: GhostState) => (
@@ -32,98 +47,118 @@ const columns: ColumnsType = [
title: 'State',
width: 80,
align: 'center',
- render: ghost => {() => ghost.state.toString()},
+ render: (_, ghost: GhostState) => (
+ g.state.toString()}
+ />
+ ),
},
{
title: '# Changes',
width: 80,
align: 'right',
- render: ghost => {() => ghost.stateChanges.toString()},
+ render: (_, ghost: GhostState) => (
+ g.stateChanges.toString()}
+ />
+ ),
},
{
title: 'X',
width: 32,
align: 'right',
- render: ghost => {(): any => ghost.tileCoordinates.x},
+ render: (_, ghost: GhostState) => ,
},
{
title: 'Y',
width: 32,
align: 'right',
- render: ghost => {(): any => ghost.tileCoordinates.y},
+ render: (_, ghost: GhostState) => ,
},
{
title: 'Paused',
align: 'center',
- render: ghost => ,
+ render: (_, ghost: GhostState) => ,
},
{
title: '',
align: 'center',
width: 60,
- render: ghost => ,
+ render: (_, ghost: GhostState) => ,
},
{
title: '',
align: 'center',
width: 60,
- render: record => ,
+ render: (_, ghost: GhostState) => ,
},
{
title: '',
- render: record => null,
+ render: () => null,
},
];
-const PausedSwitch: FC<{ ghost: Ghost }> = observer(({ ghost }) => (
- {
- ghost.ghostPaused = checked;
- }}
- />
-));
+const PausedSwitch: FC<{ ghostIndex: number }> = ({ ghostIndex }) => {
+ const ghostPaused = useGameStore((state) => state.game.ghosts[ghostIndex].ghostPaused);
+ const setGhostPaused = useGameStore((state) => state.setGhostPaused);
-const KillButton = observer<{ ghost: Ghost }>(({ ghost }) => (
-
-));
+ return (
+ {
+ setGhostPaused(ghostIndex, checked);
+ }}
+ />
+ );
+};
-const MoveButton = observer<{ ghost: Ghost }>(({ ghost }) => (
-
-));
+const KillButton: FC<{ ghostIndex: number }> = ({ ghostIndex }) => {
+ const isFrightened = useGameStore((state) => state.game.ghosts[ghostIndex].state === 'frightened');
-export const GhostsDebugTable = observer<{ className?: string }>(
- ({ className }) => {
- const store = useGame();
- return (
-
- );
- }
-);
+ return (
+
+ );
+};
+
+const MoveButton: FC<{ ghostIndex: number }> = ({ ghostIndex }) => {
+ // Move button is disabled for now since routeAndMoveGhost is no longer exported
+ // This is a debug feature that can be re-implemented if needed
+ return (
+
+ );
+};
+
+export const GhostsDebugTable: FC<{ className?: string }> = ({ className }) => {
+ const ghosts = useGameStore((state) => state.game.ghosts);
+
+ return (
+
+ );
+};
interface DotProps {
color: string;
diff --git a/src/pages/GamePage/components/GhostsDebugView.tsx b/src/pages/GamePage/components/GhostsDebugView.tsx
index a0cc131f..1f87a8b3 100644
--- a/src/pages/GamePage/components/GhostsDebugView.tsx
+++ b/src/pages/GamePage/components/GhostsDebugView.tsx
@@ -1,11 +1,10 @@
import { Card } from 'antd';
-import { observer } from 'mobx-react-lite';
import React, { FC } from 'react';
import { GhostDebugControls } from './GhostDebugControls';
import { GhostsDebugTable } from './GhostDebugTable';
import { VSpace } from '../../../components/Spacer';
-export const GhostsDebugView: FC = observer(() => {
+export const GhostsDebugView: FC = () => {
return (
@@ -16,4 +15,4 @@ export const GhostsDebugView: FC = observer(() => {
);
-});
+};
diff --git a/src/pages/GamePage/components/GhostsView.tsx b/src/pages/GamePage/components/GhostsView.tsx
index ac38825f..2b28af2c 100644
--- a/src/pages/GamePage/components/GhostsView.tsx
+++ b/src/pages/GamePage/components/GhostsView.tsx
@@ -1,23 +1,24 @@
-import { observer } from 'mobx-react-lite';
import React, { FC } from 'react';
import {
SCREEN_TILE_SIZE,
SCREEN_TILE_CENTER,
+ tileFromScreen,
} from '../../../model/Coordinates';
import { getGhostHitBox } from '../../../model/detectCollisions';
-import {
- Ghost,
- GhostAnimationPhase,
- FrightenedGhostTime,
-} from '../../../model/Ghost';
import { Direction } from '../../../model/Types';
import { WayPoints } from '../../WayFindingPage/WayPoints';
import { Box } from '../../../components/Box';
import { Sprite } from '../../../components/Sprite';
-import { useGame, useStore } from '../../../components/StoreContext';
+import { useGameStore, GhostState, FRIGHTENED_ABOUT_TO_END_DURATION } from '../../../model/store';
import { Target } from './Target';
import { GhostViewOptions } from '../../../model/GhostViewOptions';
import { GameViewOptions } from '../../../model/GameViewOptions';
+import { findWayPoints } from '../../../model/findWayPoints';
+import { canGhostPassThroughBoxDoor } from '../../../model/store/ghostHelpers';
+
+// Ghost types
+export type GhostAnimationPhase = 0 | 1;
+export type FrightenedGhostTime = 0 | 1;
const GHOST_WIDTH = SCREEN_TILE_SIZE * 2;
const GHOST_HEIGHT = SCREEN_TILE_SIZE * 2;
@@ -25,9 +26,9 @@ const GHOST_HEIGHT = SCREEN_TILE_SIZE * 2;
const GHOST_OFFSET_X = GHOST_WIDTH / 2 - 0;
const GHOST_OFFSET_Y = GHOST_HEIGHT / 2;
-export const GhostsGameView = observer(() => {
- const store = useStore();
- const { ghostViewOptions, gameViewOptions } = store.debugState;
+export const GhostsGameView: FC = () => {
+ const ghostViewOptions = useGameStore((state) => state.debugState.ghostViewOptions);
+ const gameViewOptions = useGameStore((state) => state.debugState.gameViewOptions);
return (
{
gameViewOptions={gameViewOptions}
/>
);
-});
+};
export const GhostsView: FC<{
ghostViewOptions?: GhostViewOptions;
gameViewOptions?: GameViewOptions;
-}> = observer(
- ({
- ghostViewOptions = DefaultGhostViewOptions,
- gameViewOptions = DefaultGameViewOptions,
- }) => {
- const store = useGame();
-
- return (
- <>
- {store.ghosts.map(ghost => (
-
- ))}
- >
- );
- }
-);
+}> = ({
+ ghostViewOptions = DefaultGhostViewOptions,
+ gameViewOptions = DefaultGameViewOptions,
+}) => {
+ const ghosts = useGameStore((state) => state.game.ghosts);
+
+ return (
+ <>
+ {ghosts.map((ghost, index) => (
+
+ ))}
+ >
+ );
+};
const DefaultGhostViewOptions: GhostViewOptions = {
target: false,
@@ -72,11 +71,25 @@ const DefaultGameViewOptions: GameViewOptions = {
};
export const GhostCompositeView: FC<{
- ghost: Ghost;
+ ghostIndex: number;
ghostViewOptions: GhostViewOptions;
gameViewOptions: GameViewOptions;
-}> = observer(({ ghost, ghostViewOptions, gameViewOptions }) => {
- const { screenCoordinates } = ghost;
+}> = ({ ghostIndex, ghostViewOptions, gameViewOptions }) => {
+ const ghost = useGameStore((state) => state.game.ghosts[ghostIndex]);
+ const timestamp = useGameStore((state) => state.game.timestamp);
+ const energizerTimeLeft = useGameStore((state) => {
+ const timer = state.game.energizerTimer;
+ return timer.duration - timer.timeSpent;
+ });
+
+ const { screenCoordinates, targetTile, direction, colorCode } = ghost;
+ const tileCoordinates = tileFromScreen(screenCoordinates);
+ const boxDoorIsOpen = canGhostPassThroughBoxDoor(ghost, timestamp);
+
+ const wayPoints = ghostViewOptions.wayPoints
+ ? findWayPoints(tileCoordinates, targetTile, direction, boxDoorIsOpen)
+ : null;
+
return (
<>
{gameViewOptions.hitBox && (
@@ -86,27 +99,51 @@ export const GhostCompositeView: FC<{
color="green"
/>
)}
-
- {ghostViewOptions.wayPoints && (
-
+
+ {ghostViewOptions.wayPoints && wayPoints && (
+
)}
{ghostViewOptions.target && (
-
+
)}
>
);
-});
+};
+
+const getGhostAnimationPhase = (timestamp: number, ghostNumber: number): GhostAnimationPhase => {
+ return Math.round((timestamp + ghostNumber * 100) / 300) % 2 === 0 ? 0 : 1;
+};
+
+const getFrightenedGhostTime = (
+ timestamp: number,
+ energizerTimeLeft: number
+): FrightenedGhostTime => {
+ const frightenedAboutToEnd = energizerTimeLeft < FRIGHTENED_ABOUT_TO_END_DURATION;
+ if (!frightenedAboutToEnd) {
+ return 0;
+ }
+ // Blink every 0.5 seconds
+ return timestamp % 1000 < 500 ? 0 : 1;
+};
export const GhostView: FC<{
- ghost: Ghost;
-}> = observer(({ ghost }) => {
- const { screenCoordinates, animationPhase, direction, ghostNumber } = ghost;
- // TODO
- switch (ghost.state) {
+ ghost: GhostState;
+ timestamp: number;
+ energizerTimeLeft: number;
+}> = ({ ghost, timestamp, energizerTimeLeft }) => {
+ const { screenCoordinates, direction, ghostNumber, state } = ghost;
+ const animationPhase = getGhostAnimationPhase(timestamp, ghostNumber);
+ const frightenedGhostTime = getFrightenedGhostTime(timestamp, energizerTimeLeft);
+
+ switch (state) {
case 'frightened':
return (
);
}
-});
+};
type GhostSpriteProps = {
direction: Direction;
diff --git a/src/pages/GamePage/components/Message.tsx b/src/pages/GamePage/components/Message.tsx
index f6eee203..616b1295 100644
--- a/src/pages/GamePage/components/Message.tsx
+++ b/src/pages/GamePage/components/Message.tsx
@@ -1,12 +1,9 @@
-import { observer } from 'mobx-react-lite';
-import React from 'react';
+import React, { FC } from 'react';
import styled from 'styled-components/macro';
-export const Message = observer<{ className?: string; text: string }>(
- ({ className, text }) => {
- return {text};
- }
-);
+export const Message: FC<{ className?: string; text: string }> = ({ className, text }) => {
+ return {text};
+};
const MessageStyled = styled.span`
font-family: Joystix;
diff --git a/src/pages/GamePage/components/PacManDebugView.tsx b/src/pages/GamePage/components/PacManDebugView.tsx
index 615d4219..73ac061b 100644
--- a/src/pages/GamePage/components/PacManDebugView.tsx
+++ b/src/pages/GamePage/components/PacManDebugView.tsx
@@ -1,51 +1,59 @@
import { Button, Card, Space, Row, Col } from 'antd';
-import { observer } from 'mobx-react-lite';
-import React from 'react';
+import React, { FC } from 'react';
import styled from 'styled-components/macro';
import { ghostCollidesWithPacMan } from '../../../model/detectCollisions';
-import { useGame } from '../../../components/StoreContext';
-
-export const PacManDebugView = observer<{ className?: string }>(
- ({ className }) => {
- const game = useGame();
- return (
-
-
-
- {`State: ${game.pacMan.state}`}
-
-
-
-
-
- {game.pacMan.alive && (
- {
- ghostCollidesWithPacMan(game.ghosts[0]);
- }}
- >
- Kill
-
- )}
- {game.pacMan.dead && (
-
- Revive
-
- )}
-
-
-
-
-
- );
- }
-);
+import { useGameStore } from '../../../model/store';
+
+export const PacManDebugView: FC<{ className?: string }> = ({ className }) => {
+ const pacManState = useGameStore((state) => state.game.pacMan.state);
+ const sendPacManEvent = useGameStore((state) => state.sendPacManEvent);
+
+ const isDead = pacManState === 'dead';
+ const isAlive = !isDead;
+
+ const handleKill = () => {
+ ghostCollidesWithPacMan(0); // Collide with first ghost
+ };
+
+ const handleRevive = () => {
+ sendPacManEvent('REVIVED');
+ };
+
+ return (
+
+
+
+ {`State: ${pacManState}`}
+
+
+
+
+
+ {isAlive && (
+
+ Kill
+
+ )}
+ {isDead && (
+
+ Revive
+
+ )}
+
+
+
+
+
+ );
+};
const Layout = styled.div``;
diff --git a/src/pages/GamePage/components/PacManView.tsx b/src/pages/GamePage/components/PacManView.tsx
index 1dd60441..26859f85 100644
--- a/src/pages/GamePage/components/PacManView.tsx
+++ b/src/pages/GamePage/components/PacManView.tsx
@@ -1,14 +1,12 @@
import React, { FC, CSSProperties } from 'react';
import { Sprite } from '../../../components/Sprite';
import { Direction } from '../../../model/Types';
-import { observer } from 'mobx-react-lite';
-import { useGame, useStore } from '../../../components/StoreContext';
+import { useGameStore } from '../../../model/store';
import {
SCREEN_TILE_SIZE,
SCREEN_TILE_CENTER,
} from '../../../model/Coordinates';
import { Box } from '../../../components/Box';
-import { PacMan } from '../../../model/PacMan';
import { getPacManHitBox } from '../../../model/detectCollisions';
import {
PacManDyingPhase,
@@ -25,23 +23,28 @@ const PAC_MAN_HEIGHT = SCREEN_TILE_SIZE * 2;
const PAC_MAN_OFFSET_X = PAC_MAN_WIDTH / 2 - 2;
const PAC_MAN_OFFSET_Y = PAC_MAN_HEIGHT / 2 - 2;
-export const PacManView: FC = observer(() => {
- const store = useStore();
- const game = useGame();
- const pacMan = game.pacMan;
- const { dead, alive, screenCoordinates, direction } = pacMan;
- const { gameViewOptions } = store.debugState;
- const pacManAnimationPhase = getPacManAnimationPhase(pacMan);
- const dyingPhase = getPacManDyingPhase(pacMan);
+export const PacManView: FC = () => {
+ const pacMan = useGameStore((state) => state.game.pacMan);
+ const timestamp = useGameStore((state) => state.game.timestamp);
+ const showHitBox = useGameStore((state) => state.debugState.gameViewOptions.hitBox);
+
+ const isDead = pacMan.state === 'dead';
+ const isAlive = !isDead;
+ const { screenCoordinates, direction, diedAtTimestamp } = pacMan;
+ const timeSinceDeath = isDead ? timestamp - diedAtTimestamp : 0;
+
+ const pacManAnimationPhase = getPacManAnimationPhaseFromTimestamp(timestamp);
+ const dyingPhase = getPacManDyingPhase(timeSinceDeath);
+
return (
<>
- {gameViewOptions.hitBox && (
+ {showHitBox && (
)}
- {alive && (
+ {isAlive && (
{
y={screenCoordinates.y + SCREEN_TILE_CENTER - PAC_MAN_OFFSET_Y}
/>
)}
- {dead && (
+ {isDead && (
{
)}
>
);
-});
+};
-const getPacManAnimationPhase = (pacMan: PacMan): PacManAnimationPhase => {
- const step = Math.round(pacMan.game.timestamp / 200) % 4;
+const getPacManAnimationPhaseFromTimestamp = (timestamp: number): PacManAnimationPhase => {
+ const step = Math.round(timestamp / 200) % 4;
const phase = step === 3 ? 1 : step;
return phase as PacManAnimationPhase;
};
diff --git a/src/pages/GamePage/components/PillsView.tsx b/src/pages/GamePage/components/PillsView.tsx
index a6958352..2c7b2349 100644
--- a/src/pages/GamePage/components/PillsView.tsx
+++ b/src/pages/GamePage/components/PillsView.tsx
@@ -1,4 +1,3 @@
-import { observer } from 'mobx-react-lite';
import React, { FC, memo } from 'react';
import { Box } from '../../../components/Box';
import { Sprite } from '../../../components/Sprite';
@@ -17,7 +16,7 @@ import {
MAZE_WIDTH_IN_TILES,
EMPTY_TILE_ID,
} from '../../../model/MazeData';
-import { useGame } from '../../../components/StoreContext';
+import { useGameStore } from '../../../model/store';
const BasicPillView: FC<{ position: ScreenCoordinates }> = ({ position }) => (
@@ -32,47 +31,45 @@ export const BasicPillHitBox: FC = () => {
return ;
};
-const PillView = observer<{ tile: TileCoordinates }>(
- ({ tile }: { tile: TileCoordinates }) => {
- const game = useGame();
- const { x, y } = tile;
- const tileId = game.maze.pills[y][x];
- if (tileId === BASIC_PILL_ID) {
- return (
-
- );
- }
- if (tileId === ENERGIZER_ID) {
- return (
-
- );
- }
- return null;
+const PillView: FC<{ tile: TileCoordinates }> = ({ tile }) => {
+ const { x, y } = tile;
+ const tileId = useGameStore((state) => state.game.maze.pills[y][x]);
+
+ if (tileId === BASIC_PILL_ID) {
+ return (
+
+ );
}
-);
+ if (tileId === ENERGIZER_ID) {
+ return (
+
+ );
+ }
+ return null;
+};
// Performance tricks used here:
-// Make each PillView an observer, so that we don't have to rerender PillsView.
+// Make each PillView subscribe to only its specific tile.
// Make PillsView a React.memo to prevent any rerenders.
// Also: Create PillView only for those coordinates where there is a pill on first render.
export const PillsView: FC = memo(() => {
- const game = useGame();
+ const pills = useGameStore((state) => state.game.maze.pills);
return (
<>
{Array.from({ length: MAZE_HEIGHT_IN_TILES }).map((_, y) =>
Array.from({ length: MAZE_WIDTH_IN_TILES }).map((_, x) => {
- const pillFound = game.maze.pills[y][x] !== EMPTY_TILE_ID;
+ const pillFound = pills[y][x] !== EMPTY_TILE_ID;
return pillFound && ;
})
)}
diff --git a/src/pages/GamePage/components/Score.tsx b/src/pages/GamePage/components/Score.tsx
index 5bd058cc..4a0c429b 100644
--- a/src/pages/GamePage/components/Score.tsx
+++ b/src/pages/GamePage/components/Score.tsx
@@ -1,15 +1,14 @@
-import { observer } from 'mobx-react-lite';
-import React from 'react';
-import { useGame } from '../../../components/StoreContext';
+import React, { FC } from 'react';
+import { useGameStore } from '../../../model/store';
import './Score.css';
import classNames from 'classnames';
-export const Score = observer<{ className?: string }>(({ className }) => {
- const store = useGame();
+export const Score: FC<{ className?: string }> = ({ className }) => {
+ const score = useGameStore((state) => state.game.score);
return (
Score
- {store.score}
+ {score}
);
-});
+};
diff --git a/src/pages/GamePage/components/useKeyboardActions.ts b/src/pages/GamePage/components/useKeyboardActions.ts
index 6d69091f..b0427aa9 100644
--- a/src/pages/GamePage/components/useKeyboardActions.ts
+++ b/src/pages/GamePage/components/useKeyboardActions.ts
@@ -1,34 +1,35 @@
import { useCallback, useEffect } from 'react';
-import { useStore } from '../../../components/StoreContext';
+import { useGameStore } from '../../../model/store';
/* eslint-disable react-hooks/exhaustive-deps */
export const useKeyboardActions = (): void => {
- const store = useStore();
+ const setPacManNextDirection = useGameStore((state) => state.setPacManNextDirection);
+ const setGamePaused = useGameStore((state) => state.setGamePaused);
const onKeyDown = useCallback((event: KeyboardEvent) => {
- const { game } = store;
const pressedKey = event.key;
- const pacMan = game.pacMan;
+ const gamePaused = useGameStore.getState().game.gamePaused;
+
switch (pressedKey) {
case 'ArrowLeft':
- pacMan.nextDirection = 'LEFT';
+ setPacManNextDirection('LEFT');
break;
case 'ArrowRight':
- pacMan.nextDirection = 'RIGHT';
+ setPacManNextDirection('RIGHT');
break;
case 'ArrowUp':
- pacMan.nextDirection = 'UP';
+ setPacManNextDirection('UP');
break;
case 'ArrowDown':
- pacMan.nextDirection = 'DOWN';
+ setPacManNextDirection('DOWN');
break;
case ' ':
- game.gamePaused = !game.gamePaused;
+ setGamePaused(!gamePaused);
break;
default:
break;
}
- }, []);
+ }, [setPacManNextDirection, setGamePaused]);
useEffect(() => {
document.addEventListener('keydown', onKeyDown);
@@ -36,5 +37,5 @@ export const useKeyboardActions = (): void => {
return () => {
document.removeEventListener('keydown', onKeyDown);
};
- }, []);
+ }, [onKeyDown]);
};
diff --git a/src/pages/SpritePage/SpritePage.tsx b/src/pages/SpritePage/SpritePage.tsx
index 6299e5ec..cc06b32a 100644
--- a/src/pages/SpritePage/SpritePage.tsx
+++ b/src/pages/SpritePage/SpritePage.tsx
@@ -18,7 +18,7 @@ import {
GhostAnimationPhase,
GhostNumber,
FrightenedGhostTimes,
-} from '../../model/Ghost';
+} from '../../model/store/types';
import styled from 'styled-components/macro';
import {
PacManDyingPhaseCount,
diff --git a/src/pages/WayFindingPage/WayFindingPage.tsx b/src/pages/WayFindingPage/WayFindingPage.tsx
index 740c1c9c..9180df75 100644
--- a/src/pages/WayFindingPage/WayFindingPage.tsx
+++ b/src/pages/WayFindingPage/WayFindingPage.tsx
@@ -1,5 +1,5 @@
/* eslint-disable react/no-unescaped-entities */
-import React from 'react';
+import React, { useState, FC } from 'react';
import { Sprite } from '../../components/Sprite';
import { GridWithHoverCoordinates } from '../../components/Grid';
@@ -10,29 +10,17 @@ import {
TileCoordinates,
SCREEN_TILE_CENTER,
} from '../../model/Coordinates';
-import { useLocalStore, observer } from 'mobx-react-lite';
-import { action } from 'mobx';
import { WayPoints } from './WayPoints';
import { findWayPoints } from '../../model/findWayPoints';
import styled from 'styled-components/macro';
import { Row } from 'antd';
-export const WayFindingPage = observer(() => {
- const localStore = useLocalStore(() => ({
- origin: { x: 1, y: 1 } as TileCoordinates,
- destination: { x: 6, y: 15 } as TileCoordinates,
- setOrigin: action((value: TileCoordinates) => {
- localStore.origin = value;
- }),
- setDestination: action((value: TileCoordinates) => {
- localStore.destination = value;
- }),
- }));
+export const WayFindingPage: FC = () => {
+ const [origin, setOrigin] = useState({ x: 1, y: 1 });
+ const [destination, setDestination] = useState({ x: 6, y: 15 });
- const wayPoints =
- findWayPoints(localStore.origin, localStore.destination, 'RIGHT', true) ??
- [];
+ const wayPoints = findWayPoints(origin, destination, 'RIGHT', true) ?? [];
return (
@@ -47,9 +35,9 @@ export const WayFindingPage = observer(() => {
event: React.MouseEvent
) => {
if (event.shiftKey) {
- localStore.setOrigin(coordinates);
+ setOrigin(coordinates);
} else {
- localStore.setDestination(coordinates);
+ setDestination(coordinates);
}
}}
/>
@@ -58,11 +46,11 @@ export const WayFindingPage = observer(() => {
direction="RIGHT"
ghostAnimationPhase={1}
x={
- screenFromTileCoordinate(localStore.origin.x - 1) +
+ screenFromTileCoordinate(origin.x - 1) +
SCREEN_TILE_CENTER
}
y={
- screenFromTileCoordinate(localStore.origin.y - 1) +
+ screenFromTileCoordinate(origin.y - 1) +
SCREEN_TILE_CENTER
}
ghostNumber={0}
@@ -72,11 +60,11 @@ export const WayFindingPage = observer(() => {
direction="RIGHT"
pacManAnimationPhase={1}
x={
- screenFromTileCoordinate(localStore.destination.x - 1) +
+ screenFromTileCoordinate(destination.x - 1) +
SCREEN_TILE_CENTER
}
y={
- screenFromTileCoordinate(localStore.destination.y - 1) +
+ screenFromTileCoordinate(destination.y - 1) +
SCREEN_TILE_CENTER
}
style={{}}
@@ -91,7 +79,7 @@ export const WayFindingPage = observer(() => {
);
-});
+};
const Layout = styled.div`
margin-top: 32px;
diff --git a/src/pages/WayFindingPage/WayPoints.tsx b/src/pages/WayFindingPage/WayPoints.tsx
index 3695ddab..9b302763 100644
--- a/src/pages/WayFindingPage/WayPoints.tsx
+++ b/src/pages/WayFindingPage/WayPoints.tsx
@@ -1,16 +1,15 @@
/* eslint-disable react/no-unescaped-entities */
-import { observer } from 'mobx-react-lite';
-import React from 'react';
+import React, { FC } from 'react';
import { screenFromTile, TileCoordinates } from '../../model/Coordinates';
import { WayPoint } from './WayPoint';
import { getDirectionFromTileToTile } from '../../model/getDirectionFromTileToTile';
import { Direction } from '../../model/Types';
import { assert } from '../../util/assert';
-export const WayPoints = observer<{
+export const WayPoints: FC<{
wayPoints: TileCoordinates[];
color: string;
-}>(({ wayPoints, color }) => (
+}> = ({ wayPoints, color }) => (
<>
{wayPoints.map((wayPoint, index) => {
const screenCoordinates = screenFromTile(wayPoint);
@@ -25,7 +24,7 @@ export const WayPoints = observer<{
);
})}
>
-));
+);
const getDirection = (
wayPoints: TileCoordinates[],
diff --git a/src/test-util/TestApp.tsx b/src/test-util/TestApp.tsx
index fb08f7f6..b81088a4 100644
--- a/src/test-util/TestApp.tsx
+++ b/src/test-util/TestApp.tsx
@@ -1,15 +1,19 @@
-import React, { FC } from 'react';
+import React, { FC, useEffect } from 'react';
import { MemoryRouter } from 'react-router-dom';
-import { Store } from '../model/Store';
import App from '../App';
+import { useGameStore } from '../model/store';
-export const TestApp: FC<{ store?: Store; route?: string }> = ({
- store = new Store(),
+export const TestApp: FC<{ route?: string }> = ({
route = '/',
}) => {
+ // Reset store state before each test render
+ useEffect(() => {
+ useGameStore.getState().resetGame();
+ }, []);
+
const Router: FC> = ({ children }) => (
{children}
);
- return ;
+ return ;
};
diff --git a/yarn.lock b/yarn.lock
index 8266f697..26112727 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5656,6 +5656,11 @@ ignore@^5.2.0:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
+immer@^11.1.3:
+ version "11.1.3"
+ resolved "https://registry.yarnpkg.com/immer/-/immer-11.1.3.tgz#78681e1deb6cec39753acf04eb16d7576c04f4d6"
+ integrity sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==
+
immer@^9.0.7:
version "9.0.21"
resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176"
@@ -7122,18 +7127,6 @@ mkdirp@~0.5.1:
dependencies:
minimist "^1.2.6"
-mobx-react-lite@^4.0.0:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-4.1.1.tgz#725d74b025235f73dc2ab815766ffe010be2cf8a"
- integrity sha512-iUxiMpsvNraCKXU+yPotsOncNNmyeS2B5DKL+TL6Tar/xm+wwNJAubJmtRSeAoYawdZqwv8Z/+5nPRHeQxTiXg==
- dependencies:
- use-sync-external-store "^1.4.0"
-
-mobx@^6.12.0:
- version "6.15.0"
- resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.15.0.tgz#78b9b82d383724eebb4b6e50c2eb4ae2da861cb5"
- integrity sha512-UczzB+0nnwGotYSgllfARAqWCJ5e/skuV2K/l+Zyck/H6pJIhLXuBnz+6vn2i211o7DtbE78HQtsYEKICHGI+g==
-
moment@^2.24.0, moment@^2.29.2:
version "2.30.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae"
@@ -10420,11 +10413,6 @@ url-parse@^1.5.3:
querystringify "^2.1.1"
requires-port "^1.0.0"
-use-sync-external-store@^1.4.0:
- version "1.6.0"
- resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz#b174bfa65cb2b526732d9f2ac0a408027876f32d"
- integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==
-
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
@@ -10974,11 +10962,6 @@ xmlchars@^2.2.0:
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
-xstate@^4.7.5:
- version "4.38.3"
- resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.38.3.tgz#4e15e7ad3aa0ca1eea2010548a5379966d8f1075"
- integrity sha512-SH7nAaaPQx57dx6qvfcIgqKRXIh4L0A1iYEqim4s1u7c9VoCgzZc+63FY90AKU4ZzOC2cfJzTnpO4zK7fCUzzw==
-
y18n@^5.0.5:
version "5.0.8"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
@@ -11016,3 +10999,8 @@ yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
+
+zustand@^5.0.10:
+ version "5.0.10"
+ resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.10.tgz#4db510c0c4c25a5f1ae43227b307ddf1641a3210"
+ integrity sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==