diff --git a/src/frontend/app.js b/src/frontend/app.js index 11d18ba..1f7f94a 100644 --- a/src/frontend/app.js +++ b/src/frontend/app.js @@ -1,13 +1,15 @@ import Game from './utilities/Game.js'; import Player from './utilities/Player.js'; -const canvas = document.querySelector('canvas'); +const foregroundCanvas = document.getElementById('foreground-layer'); +const playerCanvas = document.getElementById('player-layer'); + const movementKeys = ['w', 'a', 's', 'd', 'ArrowUp', 'ArrowLeft', 'ArrowDown', 'ArrowRight']; -const game = new Game(canvas); +const game = new Game(foregroundCanvas, playerCanvas); const player = new Player(); -await game.loadGameBoard('./maps/dev.json'); // load the gameboard/map from json file +await game.loadGameBoard('./assets/map.json'); // load the gameboard/map from json file game.addPlayer(player); game.start(); diff --git a/src/frontend/assets/map.json b/src/frontend/assets/map.json new file mode 100644 index 0000000..4f210d6 --- /dev/null +++ b/src/frontend/assets/map.json @@ -0,0 +1,566 @@ +{ + "intersections": [ + { + "x": 48, + "y": 48 + }, + { + "x": 208, + "y": 48 + }, + { + "x": 400, + "y": 48 + }, + { + "x": 496, + "y": 48 + }, + { + "x": 688, + "y": 48 + }, + { + "x": 848, + "y": 48 + }, + { + "x": 48, + "y": 176 + }, + { + "x": 208, + "y": 176 + }, + { + "x": 304, + "y": 176 + }, + { + "x": 400, + "y": 176 + }, + { + "x": 496, + "y": 176 + }, + { + "x": 592, + "y": 176 + }, + { + "x": 688, + "y": 176 + }, + { + "x": 848, + "y": 176 + }, + { + "x": 48, + "y": 272 + }, + { + "x": 208, + "y": 272 + }, + { + "x": 304, + "y": 272 + }, + { + "x": 400, + "y": 272 + }, + { + "x": 496, + "y": 272 + }, + { + "x": 592, + "y": 272 + }, + { + "x": 688, + "y": 272 + }, + { + "x": 848, + "y": 272 + }, + { + "x": 304, + "y": 368 + }, + { + "x": 400, + "y": 368 + }, + { + "x": 448, + "y": 368 + }, + { + "x": 496, + "y": 368 + }, + { + "x": 592, + "y": 368 + }, + { + "x": 20, + "y": 464 + }, + { + "x": 208, + "y": 464 + }, + { + "x": 304, + "y": 464 + }, + { + "x": 376, + "y": 464 + }, + { + "x": 412, + "y": 464 + }, + { + "x": 448, + "y": 464 + }, + { + "x": 484, + "y": 464 + }, + { + "x": 520, + "y": 464 + }, + { + "x": 592, + "y": 464 + }, + { + "x": 688, + "y": 464 + }, + { + "x": 868, + "y": 464 + }, + { + "x": 304, + "y": 560 + }, + { + "x": 592, + "y": 560 + }, + { + "x": 48, + "y": 656 + }, + { + "x": 208, + "y": 656 + }, + { + "x": 304, + "y": 656 + }, + { + "x": 400, + "y": 656 + }, + { + "x": 496, + "y": 656 + }, + { + "x": 592, + "y": 656 + }, + { + "x": 688, + "y": 656 + }, + { + "x": 848, + "y": 656 + }, + { + "x": 48, + "y": 752 + }, + { + "x": 112, + "y": 752 + }, + { + "x": 208, + "y": 752 + }, + { + "x": 304, + "y": 752 + }, + { + "x": 400, + "y": 752 + }, + { + "x": 496, + "y": 752 + }, + { + "x": 592, + "y": 752 + }, + { + "x": 688, + "y": 752 + }, + { + "x": 784, + "y": 752 + }, + { + "x": 848, + "y": 752 + }, + { + "x": 48, + "y": 848 + }, + { + "x": 112, + "y": 848 + }, + { + "x": 208, + "y": 848 + }, + { + "x": 304, + "y": 848 + }, + { + "x": 400, + "y": 848 + }, + { + "x": 496, + "y": 848 + }, + { + "x": 592, + "y": 848 + }, + { + "x": 688, + "y": 848 + }, + { + "x": 784, + "y": 848 + }, + { + "x": 848, + "y": 848 + }, + { + "x": 48, + "y": 944 + }, + { + "x": 400, + "y": 944 + }, + { + "x": 496, + "y": 944 + }, + { + "x": 848, + "y": 944 + } + ], + "portals": [ + [ + { + "x": 20, + "y": 464 + }, + { + "x": 868, + "y": 464 + } + ] + ], + "inaccessiblePaths": [ + [ + { + "x": 400, + "y": 48 + }, + { + "x": 496, + "y": 48 + } + ], + [ + { + "x": 208, + "y": 272 + }, + { + "x": 304, + "y": 272 + } + ], + [ + { + "x": 400, + "y": 272 + }, + { + "x": 496, + "y": 272 + } + ], + [ + { + "x": 592, + "y": 272 + }, + { + "x": 688, + "y": 272 + } + ], + [ + { + "x": 400, + "y": 176 + }, + { + "x": 400, + "y": 272 + } + ], + [ + { + "x": 496, + "y": 176 + }, + { + "x": 496, + "y": 272 + } + ], + [ + { + "x": 304, + "y": 272 + }, + { + "x": 304, + "y": 368 + } + ], + [ + { + "x": 592, + "y": 272 + }, + { + "x": 592, + "y": 368 + } + ], + [ + { + "x": 304, + "y": 464 + }, + { + "x": 376, + "y": 464 + } + ], + [ + { + "x": 520, + "y": 464 + }, + { + "x": 592, + "y": 464 + } + ], + [ + { + "x": 400, + "y": 656 + }, + { + "x": 496, + "y": 656 + } + ], + [ + { + "x": 400, + "y": 368 + }, + { + "x": 400, + "y": 656 + } + ], + [ + { + "x": 496, + "y": 368 + }, + { + "x": 496, + "y": 656 + } + ], + [ + { + "x": 112, + "y": 752 + }, + { + "x": 208, + "y": 752 + } + ], + [ + { + "x": 688, + "y": 752 + }, + { + "x": 784, + "y": 752 + } + ], + [ + { + "x": 304, + "y": 656 + }, + { + "x": 304, + "y": 752 + } + ], + [ + { + "x": 592, + "y": 656 + }, + { + "x": 592, + "y": 752 + } + ], + [ + { + "x": 208, + "y": 848 + }, + { + "x": 304, + "y": 848 + } + ], + [ + { + "x": 400, + "y": 848 + }, + { + "x": 496, + "y": 848 + } + ], + [ + { + "x": 592, + "y": 848 + }, + { + "x": 688, + "y": 848 + } + ], + [ + { + "x": 48, + "y": 752 + }, + { + "x": 48, + "y": 848 + } + ], + [ + { + "x": 400, + "y": 752 + }, + { + "x": 400, + "y": 848 + } + ], + [ + { + "x": 496, + "y": 752 + }, + { + "x": 496, + "y": 848 + } + ], + [ + { + "x": 848, + "y": 752 + }, + { + "x": 848, + "y": 848 + } + ], + [ + { + "x": 48, + "y": 272 + }, + { + "x": 48, + "y": 656 + } + ], + [ + { + "x": 848, + "y": 272 + }, + { + "x": 848, + "y": 656 + } + ] + ] +} diff --git a/src/frontend/assets/map.png b/src/frontend/assets/map.png new file mode 100644 index 0000000..b6b2f04 Binary files /dev/null and b/src/frontend/assets/map.png differ diff --git a/src/frontend/index.html b/src/frontend/index.html index fc5d87b..464ff25 100644 --- a/src/frontend/index.html +++ b/src/frontend/index.html @@ -9,7 +9,11 @@

Multiplayer Pacman

- Your browser does not support HTML5. +
+ Your browser does not support HTML5. + Your browser does not support HTML5. + Your browser does not support HTML5. +
diff --git a/src/frontend/maps/dev.json b/src/frontend/maps/dev.json deleted file mode 100644 index fc6dc4a..0000000 --- a/src/frontend/maps/dev.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "intersections": [ - { - "x": 20, - "y": 20 - }, - { - "x": 250, - "y": 20 - }, - { - "x": 480, - "y": 20 - }, - { - "x": 20, - "y": 250 - }, - { - "x": 250, - "y": 250 - }, - { - "x": 120, - "y": 280 - }, - { - "x": 20, - "y": 480 - }, - { - "x": 480, - "y": 480 - } - ] -} diff --git a/src/frontend/utilities/Game.js b/src/frontend/utilities/Game.js index adfbc87..b8d4238 100644 --- a/src/frontend/utilities/Game.js +++ b/src/frontend/utilities/Game.js @@ -1,41 +1,48 @@ import Intersection from './Intersection.js'; import Path from './Path.js'; +import Portal from './Portal.js'; export default class Game { - constructor(canvas) { - this.canvas = canvas; - this.ctx = canvas.getContext('2d'); + constructor(foregroundCanvas, playerCanvas) { + this.foregroundCtx = foregroundCanvas.getContext('2d'); + this.playerCtx = playerCanvas.getContext('2d'); this.players = []; this.intersections = []; this.paths = []; this.interval = undefined; - this.ctx.fillStyle = '#FFFFFF'; - } - - addPlayer(player) { - this.players.push(player); + this.board = { + width: foregroundCanvas.width, + height: foregroundCanvas.height + }; - // Spawn the player at the first safe path - for (const path of this.paths) { - if (path.isSafe) { - player.spawn(path); - } - } + // TODO: change this + this.foregroundCtx.fillStyle = '#FFFFFF'; + this.playerCtx.fillStyle = '#FFFFFF'; } async loadGameBoard(path) { const res = await fetch(path); const map = await res.json(); - for (const position of map.intersections) { - this.intersections.push(new Intersection(position)); + for (const configuration of map.intersections) { + this.intersections.push(new Intersection(configuration)); + } + + this.#generatePaths(map); + + // draw intersections (dev purposes only, will change) + for (const intersection of this.intersections) { + intersection.draw(this.foregroundCtx); } - this.generatePaths(); + // draw paths (dev purposes only, will change) + for (const path of this.paths) { + path.draw(this.foregroundCtx); + } } - generatePaths() { + #generatePaths({ inaccessiblePaths, portals }) { for (const start of this.intersections) { for (const end of this.intersections) { if (start === end || start.position.x > end.position.x || start.position.y > end.position.y) continue; @@ -53,16 +60,48 @@ export default class Game { }); } - if (isBestPath) { - const path = new Path(start, end); - start.addPath(path); - end.addPath(path); - this.paths.push(path); + const isPortal = portals.find((intersections) => { + const startPortal = (intersections[0].x === start.position.x && intersections[0].y === start.position.y); + let endPortal = (intersections[1].x === end.position.x && intersections[1].y === end.position.y); + return startPortal && endPortal; + }); + + if (isBestPath && !isPortal) { + const isInaccessiblePath = inaccessiblePaths.find((intersections) => { + const containsStart = intersections.find((intersection) => { + return intersection.x === start.position.x && intersection.y === start.position.y; + }); + + const containsEnd = intersections.find((intersection) => { + return intersection.x === end.position.x && intersection.y === end.position.y; + }); + + return containsStart && containsEnd; + }); + + if (!isInaccessiblePath) { + this.paths.push(new Path(start, end)); + } + } + + if (isPortal) { + this.paths.push(new Portal(start, end)); } } } } + addPlayer(player) { + this.players.push(player); + + // Spawn the player at the first safe path + for (const path of this.paths) { + if (path.isSafe) { + player.spawn(path); + } + } + } + removePlayer(playerToRemove) { this.players = this.players.filter((player) => player.id !== playerToRemove.id); } @@ -80,23 +119,13 @@ export default class Game { } update() { - this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - - // draw intersections (dev purposes only, will change) - for (const intersection of this.intersections) { - intersection.draw(this.ctx); - } - - // draw paths (dev purposes only, will change) - for (const path of this.paths) { - path.draw(this.ctx); - } + this.playerCtx.clearRect(0, 0, this.board.width, this.board.height); - // draw players + // move and draw players for (let i = 0; i < this.players.length; i++) { const player = this.players[i]; player.move(); - player.draw(this.ctx); + player.draw(this.playerCtx); } } } diff --git a/src/frontend/utilities/Intersection.js b/src/frontend/utilities/Intersection.js index c3aee3c..bb801f5 100644 --- a/src/frontend/utilities/Intersection.js +++ b/src/frontend/utilities/Intersection.js @@ -1,3 +1,5 @@ +import Portal from './Portal.js'; + export default class Intersection { constructor(position) { this.position = position; @@ -5,20 +7,40 @@ export default class Intersection { } addPath(path) { - if (path.isHorizontal) { - if (path.start === this) { - this.paths.right = path; + if (path instanceof Portal) { + if (path.isHorizontal) { + if (path.start === this) { + this.paths.left = path; + } + else if (path.end === this) { + this.paths.right = path; + } } - else if (path.end === this) { - this.paths.left = path; + else { + if (path.start === this) { + this.paths.up = path; + } + else if (path.end === this) { + this.paths.down = path; + } } } - else { - if (path.start === this) { - this.paths.down = path; + else { + if (path.isHorizontal) { + if (path.start === this) { + this.paths.right = path; + } + else if (path.end === this) { + this.paths.left = path; + } } - else if (path.end === this) { - this.paths.up = path; + else { + if (path.start === this) { + this.paths.down = path; + } + else if (path.end === this) { + this.paths.up = path; + } } } } @@ -43,6 +65,7 @@ export default class Intersection { } draw(ctx) { + ctx.strokeStyle = '#FFFFFF'; // TODO: revert ctx.beginPath(); ctx.arc(this.position.x, this.position.y, 8, 0, 2 * Math.PI); ctx.stroke(); diff --git a/src/frontend/utilities/Player.js b/src/frontend/utilities/Player.js index 995cca6..ef1bf40 100644 --- a/src/frontend/utilities/Player.js +++ b/src/frontend/utilities/Player.js @@ -1,5 +1,6 @@ import Path from './Path.js'; import Intersection from './Intersection.js'; +import Portal from './Portal.js'; export default class Player { constructor() { @@ -44,6 +45,7 @@ export default class Player { ...this.movement, ...movement }; + this.nextMovement = {}; // clear next movement } else { this.nextMovement = movement; @@ -70,6 +72,10 @@ export default class Player { const newPath = this.currentPath.traverse(this.movement); if (newPath) { + if (newPath instanceof Portal) { + return newPath.travel(this); // travel through the portal + } + this.currentPath = newPath; } else return this.stop(); @@ -82,7 +88,6 @@ export default class Player { if (this.currentPath && this.currentPath instanceof Path) { if (this.currentPath.start.position.x === this.position.x && this.currentPath.start.position.y === this.position.y) { this.currentPath = this.currentPath.start; - } else if (this.currentPath.end.position.x === this.position.x && this.currentPath.end.position.y === this.position.y) { this.currentPath = this.currentPath.end; @@ -92,6 +97,11 @@ export default class Player { } } + teleport(intersection) { + this.position = { ...intersection.position }; + this.currentPath = intersection; + } + stop() { this.movement = { x: 0, y: 0 }; } diff --git a/src/frontend/utilities/Portal.js b/src/frontend/utilities/Portal.js new file mode 100644 index 0000000..20566c8 --- /dev/null +++ b/src/frontend/utilities/Portal.js @@ -0,0 +1,26 @@ +import Path from './Path.js'; + +export default class Portal extends Path { + constructor(start, end) { + super(start, end); + } + + travel(player) { + if (this.isHorizontal) { + if (player.position.x === this.start.position.x) { + player.teleport(this.end); + } + else { + player.teleport(this.start); + } + } + else { + if (player.position.y === this.start.position.y) { + player.teleport(this.end); + } + else { + player.teleport(this.start); + } + } + } +} diff --git a/test/frontend/utilities/Game.test.js b/test/frontend/utilities/Game.test.js index 313f988..f6f12d0 100644 --- a/test/frontend/utilities/Game.test.js +++ b/test/frontend/utilities/Game.test.js @@ -4,31 +4,43 @@ import Chance from 'chance'; const chance = new Chance(); + describe('Game', () => { - let game, ctxMock; + let game, foregroundCtxMock, playerCtxMock; beforeEach(() => { - ctxMock = { + foregroundCtxMock = { fillStyle: '#000000', clearRect: jest.fn(), fillRect: jest.fn() }; - const mockCanvas = { + playerCtxMock = { + fillStyle: '#000000', + clearRect: jest.fn(), + fillRect: jest.fn() + }; + + const generateMockCanvas = (ctxMock) => ({ getContext: () => ctxMock, width: chance.integer({ min: 100 }), height: chance.integer({ min: 100 }) - }; + }); - game = new Game(mockCanvas); + game = new Game(generateMockCanvas(foregroundCtxMock), generateMockCanvas(playerCtxMock)); }); it('creates a game with all default values', () => { - expect(game.ctx).toMatchObject(ctxMock); + expect(game.foregroundCtx).toMatchObject(foregroundCtxMock); + expect(game.playerCtx).toMatchObject(playerCtxMock); expect(game.players).toEqual([]); + expect(game.paths).toEqual([]); + expect(game.intersections).toEqual([]); expect(game.interval).toBeUndefined(); }); + describe('loadGameBoard()', () => {}); + describe('addPlayer() and removePlayer()', () => { let player; @@ -73,7 +85,7 @@ describe('Game', () => { }); it('clears the canvas', () => { - expect(ctxMock.clearRect).toBeCalled(); + expect(playerCtxMock.clearRect).toBeCalled(); }); it('moves all players', () => { diff --git a/test/frontend/utilities/Intersection.test.js b/test/frontend/utilities/Intersection.test.js index 51367f1..0e0361b 100644 --- a/test/frontend/utilities/Intersection.test.js +++ b/test/frontend/utilities/Intersection.test.js @@ -1,4 +1,5 @@ import Intersection from '@/frontend/utilities/Intersection.js'; +import Portal from '@/frontend/utilities/Portal.js'; import Chance from 'chance'; const chance = new Chance(); @@ -22,43 +23,93 @@ describe('Intersection', () => { describe('addPath()', () => { let path; - describe('given a horizontal path', () => { - beforeEach(() => { - path = { isHorizontal: true }; - }); - - it('it properly configures the left path', () => { - path.end = intersection; - intersection.addPath(path); - expect(intersection.paths.left).toMatchObject(path); + describe('given a regular (non-portal) path', () => { + describe('given a horizontal path', () => { + beforeEach(() => { + path = { isHorizontal: true }; + }); + + it('it properly configures the left path', () => { + path.end = intersection; + + intersection.addPath(path); + expect(intersection.paths.left).toMatchObject(path); + }); + + it('it properly configures the right path', () => { + path.start = intersection; + + intersection.addPath(path); + expect(intersection.paths.right).toMatchObject(path); + }); }); - - it('it properly configures the right path', () => { - path.start = intersection; - - intersection.addPath(path); - expect(intersection.paths.right).toMatchObject(path); + + describe('given a vertical path', () => { + beforeEach(() => { + path = { isVertical: true }; + }); + + it('it properly configures the up path', () => { + path.end = intersection; + + intersection.addPath(path); + expect(intersection.paths.up).toMatchObject(path); + }); + + it('it properly configures the down path', () => { + path.end = intersection; + + intersection.addPath(path); + expect(intersection.paths.up).toMatchObject(path); + }); }); }); - - describe('given a vertical path', () => { - beforeEach(() => { - path = { isVertical: true }; - }); - - it('it properly configures the up path', () => { - path.end = intersection; - intersection.addPath(path); - expect(intersection.paths.up).toMatchObject(path); + describe('given a portal path', () => { + let start, end; + describe('given a horizontal path', () => { + beforeEach(() => { + start = new Intersection({ x: chance.integer({ min: 0, max: 99 }), y: 10 }); + end = new Intersection({ x: chance.integer({ min: 100, max: 500 }), y: 10 }); + path = new Portal(start, end); + }); + + it('it properly configures the left path', () => { + path.start = intersection; + + intersection.addPath(path); + expect(intersection.paths.left).toMatchObject(path); + }); + + it('it properly configures the right path', () => { + path.end = intersection; + + intersection.addPath(path); + expect(intersection.paths.right).toMatchObject(path); + }); }); - - it('it properly configures the down path', () => { - path.end = intersection; - - intersection.addPath(path); - expect(intersection.paths.up).toMatchObject(path); + + describe('given a vertical path', () => { + beforeEach(() => { + start = new Intersection({ x: 10, y: chance.integer({ min: 0, max: 99 }) }); + end = new Intersection({ x: 10, y: chance.integer({ min: 100, max: 500 }) }); + path = new Portal(start, end); + }); + + it('it properly configures the up path', () => { + path.start = intersection; + + intersection.addPath(path); + expect(intersection.paths.up).toMatchObject(path); + }); + + it('it properly configures the down path', () => { + path.start = intersection; + + intersection.addPath(path); + expect(intersection.paths.up).toMatchObject(path); + }); }); }); }); diff --git a/test/frontend/utilities/Player.test.js b/test/frontend/utilities/Player.test.js index 279cb33..e2a549e 100644 --- a/test/frontend/utilities/Player.test.js +++ b/test/frontend/utilities/Player.test.js @@ -138,6 +138,27 @@ describe('Player', () => { }); }); + describe('teleport()', () => { + let intersection; + + beforeEach(() => { + intersection = new Intersection({ + x: chance.integer(), + y: chance.integer() + }); + + player.teleport(intersection); + }); + + it('changes the player position to that of the intersection provided', () => { + expect(player.position).toMatchObject(intersection.position); + }); + + it('changes the player currentPath to that of the intersection provided', () => { + expect(player.currentPath).toMatchObject(intersection); + }); + }); + describe('draw()', () => { let ctxMock; diff --git a/test/frontend/utilities/Portal.test.js b/test/frontend/utilities/Portal.test.js new file mode 100644 index 0000000..cb5ae3c --- /dev/null +++ b/test/frontend/utilities/Portal.test.js @@ -0,0 +1,53 @@ +import Player from '@/frontend/utilities/Player.js'; +import Portal from '@/frontend/utilities/Portal.js'; +import Intersection from '@/frontend/utilities/Intersection.js'; +import Chance from 'chance'; + +const chance = new Chance(); + +describe('Portal', () => { + let portal, start, end; + + beforeEach(() => { + start = new Intersection({ x: chance.integer({ min: 0, max: 100 }), y: 10 }); + end = new Intersection({ x: chance.integer({ min: 0, max: 100 }), y: 10 }); + + + start.addPath = jest.fn(); + end.addPath = jest.fn(); + + portal = new Portal(start, end); + }); + + it('creates a portal correctly', () => { + expect(portal.isHorizontal).toBeTruthy(); + expect(portal.isVertical).not.toBeTruthy(); + expect(portal.isSafe).toBeTruthy(); + expect(portal.start).toBe(start); + expect(portal.end).toBe(end); + }); + + it('adds itself to both intersections list of paths', () => { + expect(start.addPath).toBeCalled(); + expect(end.addPath).toBeCalled(); + + expect(start.addPath).toBeCalledWith(portal); + expect(end.addPath).toBeCalledWith(portal); + }); + + describe('travel()', () => { + let player; + + beforeEach(() => { + player = new Player(); + player.spawn(portal); + player.teleport = jest.fn(); + + portal.travel(player); + }); + + it('teleports the player', () => { + expect(player.teleport).toBeCalled(); + }); + }); +});