From 4497a0a2f2b94dfee2c4f8ebb7211ba31a86ec06 Mon Sep 17 00:00:00 2001 From: MiguelMLorente Date: Sat, 23 Nov 2024 15:02:09 +0100 Subject: [PATCH 1/3] Created baseline for game page grid layout --- web/.eslintrc.js | 9 ++- web/src/App.scss | 5 +- web/src/App.tsx | 9 +-- web/src/pages/game/GamePage.scss | 5 ++ web/src/pages/game/GamePage.tsx | 15 ++++ web/src/pages/game/components/GameBoard.scss | 79 ++++++++++++++++++++ web/src/pages/game/components/GameBoard.tsx | 76 +++++++++++++++++++ 7 files changed, 189 insertions(+), 9 deletions(-) create mode 100644 web/src/pages/game/GamePage.scss create mode 100644 web/src/pages/game/GamePage.tsx create mode 100644 web/src/pages/game/components/GameBoard.scss create mode 100644 web/src/pages/game/components/GameBoard.tsx diff --git a/web/.eslintrc.js b/web/.eslintrc.js index 9700d32..7fd62a1 100644 --- a/web/.eslintrc.js +++ b/web/.eslintrc.js @@ -13,5 +13,12 @@ module.exports = { jest: true, }, ignorePatterns: [".eslintrc.js", "dist/"], - rules: {}, + rules: { + "prettier/prettier": [ + "error", + { + endOfLine: "auto", + }, + ], + }, }; diff --git a/web/src/App.scss b/web/src/App.scss index 56dbdc3..55aefe1 100644 --- a/web/src/App.scss +++ b/web/src/App.scss @@ -1,3 +1,6 @@ @use "node_modules/@picocss/pico/scss/pico" with ( $theme-color: "pumpkin" -); \ No newline at end of file +); + +@import "./pages/game/GamePage.scss"; +@import "./pages/game/components/GameBoard.scss"; diff --git a/web/src/App.tsx b/web/src/App.tsx index 9e26c1a..8778670 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,20 +1,15 @@ -import React from "react"; import { io } from "socket.io-client"; +import GamePage from "./pages/game/GamePage"; function App() { const socket = io("http://localhost:3010"); - const emitData = () => - console.log(socket.emit("example-request", "custom-request")); socket.on("example-response", (data) => console.log(`Received response in front end with data: ${data}`), ); return (
-
- - Hello World! Front -
+
); } diff --git a/web/src/pages/game/GamePage.scss b/web/src/pages/game/GamePage.scss new file mode 100644 index 0000000..1cbf756 --- /dev/null +++ b/web/src/pages/game/GamePage.scss @@ -0,0 +1,5 @@ +h1 { + text-align: center; + padding: 10px; + margin: 0; +} diff --git a/web/src/pages/game/GamePage.tsx b/web/src/pages/game/GamePage.tsx new file mode 100644 index 0000000..663aefc --- /dev/null +++ b/web/src/pages/game/GamePage.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import GameBoard from "./components/GameBoard"; + +const GamePage = () => { + return ( + +

Game Page Title

+
+ +
+
+ ); +}; + +export default GamePage; diff --git a/web/src/pages/game/components/GameBoard.scss b/web/src/pages/game/components/GameBoard.scss new file mode 100644 index 0000000..477f4cc --- /dev/null +++ b/web/src/pages/game/components/GameBoard.scss @@ -0,0 +1,79 @@ +.game-board { + height: 700px; + width: 700px; + display: grid; + grid-template-columns: repeat(11, auto); + gap: auto; + padding: 10px; + + .cell { + width: 60px; + height: 60px; + border: 2px solid black; + border-radius: 10%; + } + + .v-exit { + width: 60px; + height: 30px; + + > div { + width: 20px; + height: 100%; + margin: auto; + border: solid black; + border-width: 0 1px; + + .dotted { + margin: auto; + border: dashed black; + border-width: 0 1px; + height: 100%; + width: 0; + } + } + + &:has(.v-exit-up) { + margin-bottom: -9px; + } + + &:has(.v-exit-down) { + margin-top: -9px; + } + } + + .h-exit { + height: 60px; + width: 30px; + + > div { + height: 20px; + width: 100%; + margin: auto; + border: solid black; + border-width: 1px 0; + position: relative; + top: 50%; + transform: translateY(-50%); + + .dotted { + margin: auto; + border: dashed black; + border-width: 1px 0; + width: 100%; + height: 0; + position: relative; + top: 50%; + transform: translateY(-50%); + } + } + + &:has(.h-exit-left) { + margin-right: -9px; + } + + &:has(.h-exit-right) { + margin-left: -9px; + } + } +} \ No newline at end of file diff --git a/web/src/pages/game/components/GameBoard.tsx b/web/src/pages/game/components/GameBoard.tsx new file mode 100644 index 0000000..31078b1 --- /dev/null +++ b/web/src/pages/game/components/GameBoard.tsx @@ -0,0 +1,76 @@ +const GameBoard = () => { + const range = (n: number) => Array.from(Array(n).keys()); + const cells = range(11).flatMap((rowIndex) => { + return range(11).map((colIndex) => { + return { rowIndex, colIndex }; + }); + }); + + return ( +
+ {cells.map(({ rowIndex, colIndex }) => { + if ( + (rowIndex === 0 || rowIndex === 10) && + (colIndex === 0 || colIndex === 10) + ) { + return
; + } + if (rowIndex === 0) { + return ( +
+
+
+
+
+ ); + } + if (rowIndex === 10) { + return ( +
+
+
+
+
+ ); + } + if (colIndex === 0) { + return ( +
+
+
+
+
+ ); + } + if (colIndex === 10) { + return ( +
+
+
+
+
+ ); + } + return
; + })} +
+ ); + + /* + {range(9).map(() => ( +
+
+
+ ))} + {range(81).map(() => ( +
+ ))} + {range(9).map(() => ( +
+
+
+ ))} + */ +}; + +export default GameBoard; From e9305893cbc5cfc805ad1e8815c299fd27074086 Mon Sep 17 00:00:00 2001 From: MiguelMLorente Date: Sat, 23 Nov 2024 22:49:51 +0100 Subject: [PATCH 2/3] Create board object builder and supporting classes and types --- interface/BoardBuilder.ts | 110 +++++++++++++++++++++++++++++++ interface/constants/CellType.ts | 6 ++ interface/constants/Direction.ts | 6 ++ interface/constants/ExitType.ts | 5 ++ interface/index.ts | 9 +++ interface/types/Border.ts | 22 +++++++ interface/types/Cell.ts | 24 +++++++ interface/types/Exit.ts | 9 +++ interface/types/ExternalNode.ts | 24 +++++++ interface/types/Node.ts | 9 +++ 10 files changed, 224 insertions(+) create mode 100644 interface/BoardBuilder.ts create mode 100644 interface/constants/CellType.ts create mode 100644 interface/constants/Direction.ts create mode 100644 interface/constants/ExitType.ts create mode 100644 interface/types/Border.ts create mode 100644 interface/types/Cell.ts create mode 100644 interface/types/Exit.ts create mode 100644 interface/types/ExternalNode.ts create mode 100644 interface/types/Node.ts diff --git a/interface/BoardBuilder.ts b/interface/BoardBuilder.ts new file mode 100644 index 0000000..c4ed092 --- /dev/null +++ b/interface/BoardBuilder.ts @@ -0,0 +1,110 @@ +import { CellType } from "./constants/CellType"; +import { Direction } from "./constants/Direction"; +import { ExitType } from "./constants/ExitType"; +import { Cell } from "./types/Cell"; +import { Exit } from "./types/Exit"; + +const boardSize = 7; +const universityLocations = [ + [2, 0], + [4, 1], + [3, 3], +]; +const factoryLocations = [ + [0, 0], + [5, 2], + [4, 6], +]; +const houseLocations = [ + [0, 2], + [1, 5], + [2, 4], + [5, 4], +]; +const mapPosition = (position: number[], type: CellType) => { + return { + row: position[0], + col: position[1], + type: type, + }; +}; +const specialUniversityCells = universityLocations.map((position) => + mapPosition(position, CellType.UNIVERSITY), +); +const specialFactoryCells = factoryLocations.map((position) => + mapPosition(position, CellType.FACTORY), +); +const specialHouseCells = houseLocations.map((position) => + mapPosition(position, CellType.HOUSE), +); +const specialCells = specialUniversityCells + .concat(specialFactoryCells) + .concat(specialHouseCells); + +const specialExitIndexes1 = [1, 5]; +const specialExitIndexes2 = [3]; + +function createBoard(): Cell[][] { + const indexes = Array.from(Array(boardSize).keys()); + return indexes.map((rowIndex) => + indexes.map((colIndex) => { + const specialCell = specialCells.find( + (specialCell) => + specialCell.row === rowIndex && specialCell.col === colIndex, + ); + return new Cell(specialCell ? specialCell.type : CellType.NORMAL); + }), + ); +} + +function connectAdjacentCells(board: Cell[][]) { + const indexes = Array.from(Array(boardSize).keys()); + for (const rowIndex of indexes.slice(0, -1)) { + for (const colIndex of indexes.slice(0, -1)) { + const cell = board[rowIndex][colIndex]; + const rightCell = board[rowIndex][colIndex + 1]; + const bottomCell = board[rowIndex + 1][colIndex]; + + cell + .getNodeAt(Direction.SOUTH) + .linkToNode(bottomCell.getNodeAt(Direction.NORTH)); + cell + .getNodeAt(Direction.EAST) + .linkToNode(rightCell.getNodeAt(Direction.WEST)); + } + } +} + +function addExits(board: Cell[][]) { + // Add exits to top row + board[0].forEach((cell, colIndex) => { + let exitType = ExitType.AMBIVALENT; + if (specialExitIndexes1.includes(colIndex)) exitType = ExitType.ROAD; + if (specialExitIndexes2.includes(colIndex)) exitType = ExitType.RAIL; + cell.getNodeAt(Direction.NORTH).linkToNode(new Exit(exitType)); + }); + + // Add exits to bottom row + board[boardSize - 1].forEach((cell, colIndex) => { + let exitType = ExitType.AMBIVALENT; + if (specialExitIndexes1.includes(colIndex)) exitType = ExitType.ROAD; + if (specialExitIndexes2.includes(colIndex)) exitType = ExitType.RAIL; + cell.getNodeAt(Direction.SOUTH).linkToNode(new Exit(exitType)); + }); + + // Add exits to left and right columns + board.forEach((row, rowIndex) => { + let exitType = ExitType.AMBIVALENT; + if (specialExitIndexes1.includes(rowIndex)) exitType = ExitType.RAIL; + if (specialExitIndexes2.includes(rowIndex)) exitType = ExitType.ROAD; + row[0].getNodeAt(Direction.WEST).linkToNode(new Exit(exitType)); + row[boardSize - 1].getNodeAt(Direction.EAST).linkToNode(new Exit(exitType)); + }); +} + +export function buildBoard(): Cell[][] { + const board: Cell[][] = createBoard(); + connectAdjacentCells(board); + addExits(board); + return board; +} diff --git a/interface/constants/CellType.ts b/interface/constants/CellType.ts new file mode 100644 index 0000000..a22a68e --- /dev/null +++ b/interface/constants/CellType.ts @@ -0,0 +1,6 @@ +export enum CellType { + NORMAL = "NORMAL", + HOUSE = "HOUSE", + FACTORY = "FACTORY", + UNIVERSITY = "UNIVERSITY", +} diff --git a/interface/constants/Direction.ts b/interface/constants/Direction.ts new file mode 100644 index 0000000..1f54f0c --- /dev/null +++ b/interface/constants/Direction.ts @@ -0,0 +1,6 @@ +export enum Direction { + NORTH = "NORTH", + SOUTH = "SOUTH", + EAST = "EAST", + WEST = "WEST", +} diff --git a/interface/constants/ExitType.ts b/interface/constants/ExitType.ts new file mode 100644 index 0000000..2a6bf4c --- /dev/null +++ b/interface/constants/ExitType.ts @@ -0,0 +1,5 @@ +export enum ExitType { + RAIL = "RAIL", + ROAD = "ROAD", + AMBIVALENT = "AMBIVALENT", +} diff --git a/interface/index.ts b/interface/index.ts index ac13701..a686c28 100644 --- a/interface/index.ts +++ b/interface/index.ts @@ -1 +1,10 @@ +export * from "./constants/CellType"; +export * from "./constants/Direction"; +export * from "./constants/ExitType"; export * from "./constants/TrackType"; +export * from "./types/Border"; +export * from "./types/Cell"; +export * from "./types/Exit"; +export * from "./types/ExternalNode"; +export * from "./types/Node"; +export * from "./BoardBuilder"; diff --git a/interface/types/Border.ts b/interface/types/Border.ts new file mode 100644 index 0000000..f5618c9 --- /dev/null +++ b/interface/types/Border.ts @@ -0,0 +1,22 @@ +import { Exit } from "./Exit"; +import { ExternalNode } from "./ExternalNode"; + +export class Border { + private readonly firstNode: ExternalNode; + private readonly secondNode: ExternalNode | Exit; + + constructor(firstNode: ExternalNode, secondNode: ExternalNode | Exit) { + this.firstNode = firstNode; + this.secondNode = secondNode; + } + + public traverseFrom(node: ExternalNode): ExternalNode | Exit { + if (node === this.firstNode) { + return this.secondNode; + } + if (node === this.secondNode) { + return this.firstNode; + } + throw Error("Unable to traverse border"); + } +} diff --git a/interface/types/Cell.ts b/interface/types/Cell.ts new file mode 100644 index 0000000..822ce34 --- /dev/null +++ b/interface/types/Cell.ts @@ -0,0 +1,24 @@ +import { CellType } from "../constants/CellType"; +import { Direction } from "../constants/Direction"; +import { ExternalNode } from "./ExternalNode"; + +export class Cell { + public readonly externalNodes: Map; + public readonly cellType: CellType; + + constructor(cellType: CellType) { + this.externalNodes = new Map([ + [Direction.NORTH, new ExternalNode(this, Direction.NORTH)], + [Direction.SOUTH, new ExternalNode(this, Direction.SOUTH)], + [Direction.EAST, new ExternalNode(this, Direction.EAST)], + [Direction.WEST, new ExternalNode(this, Direction.WEST)], + ]); + this.cellType = cellType; + } + + public getNodeAt(direction: Direction): ExternalNode { + const node = this.externalNodes.get(direction); + if (!node) throw Error(`Could not find node at ${direction}`); + return this.externalNodes.get(direction) as ExternalNode; + } +} diff --git a/interface/types/Exit.ts b/interface/types/Exit.ts new file mode 100644 index 0000000..38fe187 --- /dev/null +++ b/interface/types/Exit.ts @@ -0,0 +1,9 @@ +import { ExitType } from "../constants/ExitType"; + +export class Exit { + public readonly type: ExitType; + + constructor(type: ExitType) { + this.type = type; + } +} diff --git a/interface/types/ExternalNode.ts b/interface/types/ExternalNode.ts new file mode 100644 index 0000000..c951c54 --- /dev/null +++ b/interface/types/ExternalNode.ts @@ -0,0 +1,24 @@ +import { Direction } from "../constants/Direction"; +import { Border } from "./Border"; +import { Cell } from "./Cell"; +import { Exit } from "./Exit"; +import { Node } from "./Node"; + +export class ExternalNode extends Node { + public readonly direction: Direction; + private border?: Border; + + constructor(cell: Cell, direction: Direction) { + super(cell); + this.direction = direction; + } + + public linkToNode(other: ExternalNode | Exit) { + this.border = new Border(this, other); + } + + public traverseBorder(): ExternalNode | Exit { + if (!this.border) throw Error(`Missing border for node`); + return (this.border as Border).traverseFrom(this); + } +} diff --git a/interface/types/Node.ts b/interface/types/Node.ts new file mode 100644 index 0000000..b185ad2 --- /dev/null +++ b/interface/types/Node.ts @@ -0,0 +1,9 @@ +import { Cell } from "./Cell"; + +export abstract class Node { + public readonly cell: Cell; + + constructor(cell: Cell) { + this.cell = cell; + } +} From 0945336463ba8e1853f6db96af7aa748a526d752 Mon Sep 17 00:00:00 2001 From: MiguelMLorente Date: Sun, 24 Nov 2024 15:07:10 +0100 Subject: [PATCH 3/3] Implement board view based on board builder --- interface/BoardBuilder.ts | 11 +- interface/constants/Direction.ts | 7 + interface/types/ExternalNode.ts | 7 +- web/src/pages/game/components/GameBoard.scss | 150 ++++++++++++------- web/src/pages/game/components/GameBoard.tsx | 89 +++-------- 5 files changed, 141 insertions(+), 123 deletions(-) diff --git a/interface/BoardBuilder.ts b/interface/BoardBuilder.ts index c4ed092..e2e79c7 100644 --- a/interface/BoardBuilder.ts +++ b/interface/BoardBuilder.ts @@ -60,14 +60,19 @@ function createBoard(): Cell[][] { function connectAdjacentCells(board: Cell[][]) { const indexes = Array.from(Array(boardSize).keys()); for (const rowIndex of indexes.slice(0, -1)) { - for (const colIndex of indexes.slice(0, -1)) { + for (const colIndex of indexes) { const cell = board[rowIndex][colIndex]; - const rightCell = board[rowIndex][colIndex + 1]; const bottomCell = board[rowIndex + 1][colIndex]; - cell .getNodeAt(Direction.SOUTH) .linkToNode(bottomCell.getNodeAt(Direction.NORTH)); + } + } + + for (const rowIndex of indexes) { + for (const colIndex of indexes.slice(0, -1)) { + const cell = board[rowIndex][colIndex]; + const rightCell = board[rowIndex][colIndex + 1]; cell .getNodeAt(Direction.EAST) .linkToNode(rightCell.getNodeAt(Direction.WEST)); diff --git a/interface/constants/Direction.ts b/interface/constants/Direction.ts index 1f54f0c..6bc4347 100644 --- a/interface/constants/Direction.ts +++ b/interface/constants/Direction.ts @@ -4,3 +4,10 @@ export enum Direction { EAST = "EAST", WEST = "WEST", } + +export const directions: Direction[] = [ + Direction.NORTH, + Direction.SOUTH, + Direction.EAST, + Direction.WEST, +]; diff --git a/interface/types/ExternalNode.ts b/interface/types/ExternalNode.ts index c951c54..14dcbbe 100644 --- a/interface/types/ExternalNode.ts +++ b/interface/types/ExternalNode.ts @@ -15,10 +15,15 @@ export class ExternalNode extends Node { public linkToNode(other: ExternalNode | Exit) { this.border = new Border(this, other); + if (other instanceof ExternalNode) { + other.border = this.border; + } } public traverseBorder(): ExternalNode | Exit { - if (!this.border) throw Error(`Missing border for node`); + if (!this.border) { + throw Error(`Missing border for node`); + } return (this.border as Border).traverseFrom(this); } } diff --git a/web/src/pages/game/components/GameBoard.scss b/web/src/pages/game/components/GameBoard.scss index 477f4cc..fca4d47 100644 --- a/web/src/pages/game/components/GameBoard.scss +++ b/web/src/pages/game/components/GameBoard.scss @@ -1,79 +1,125 @@ .game-board { - height: 700px; - width: 700px; + height: 850px; + width: 850px; display: grid; - grid-template-columns: repeat(11, auto); + grid-template-columns: repeat(7, auto); gap: auto; - padding: 10px; + padding: 50px; .cell { - width: 60px; - height: 60px; + width: 100px; + height: 100px; border: 2px solid black; border-radius: 10%; - } - .v-exit { - width: 60px; - height: 30px; - - > div { - width: 20px; - height: 100%; - margin: auto; + &.house { + background-color: blue; + } + + &.university { + background-color: orange; + } + + &.factory { + background-color: grey; + } + + .exit:has(.exit-road) { border: solid black; border-width: 0 1px; - .dotted { + .exit-road { margin: auto; + width: 0; + height: 100%; border: dashed black; border-width: 0 1px; - height: 100%; + transform: translate(calc(50% - 1px)); + } + } + + .exit:has(.exit-road) { + border: solid black; + border-width: 0 1px; + + .exit-road { + margin: auto; width: 0; + height: 100%; + border: dashed black; + border-width: 0 1px; + transform: translate(calc(50% - 1px)); } } - &:has(.v-exit-up) { - margin-bottom: -9px; + .exit:has(.exit-rail) { + border-width: 0 1px; + + .exit-rail { + margin: auto; + width: 0; + height: 100%; + border: solid black; + border-width: 0 1px; + transform: translate(calc(50% - 1px)); + } } - &:has(.v-exit-down) { - margin-top: -9px; + .exit:has(.exit-ambivalent) { + border-width: 0 1px; + border-style: dotted; + + .exit-ambivalent { + margin: auto; + width: 0; + height: 100%; + border: dotted black; + border-width: 0 1px 0 0; + transform: translate(calc(50%)); + } + } + } +} + +.exit { + position: relative; + width: 20px; + height: 20px; + + &:has(.exit-north) { + left: 50%; + transform: translate(calc(-50% - 1px), -100%); + } + + &:has(.exit-south) { + left: 50%; + top: 100%; + transform: translate(calc(-50% - 1px), 0%); + } + + &:has(.exit-east) { + left: 100%; + top: 50%; + transform: rotate(90deg) translate(-50%, 0); + } + + &:has(.exit-west) { + left: 0%; + top: 50%; + transform: rotate(90deg) translate(-50%, 100%); + } + + &:has(.exit-north), &:has(.exit-south) { + + .exit:has(.exit-east) { + left: 100%; + transform: rotate(90deg) translate(-150%, 0); } } - .h-exit { - height: 60px; - width: 30px; - - > div { - height: 20px; - width: 100%; - margin: auto; - border: solid black; - border-width: 1px 0; - position: relative; + &:has(.exit-north), &:has(.exit-south) { + + .exit:has(.exit-west) { top: 50%; - transform: translateY(-50%); - - .dotted { - margin: auto; - border: dashed black; - border-width: 1px 0; - width: 100%; - height: 0; - position: relative; - top: 50%; - transform: translateY(-50%); - } - } - - &:has(.h-exit-left) { - margin-right: -9px; - } - - &:has(.h-exit-right) { - margin-left: -9px; + transform: rotate(90deg) translate(-150%, 100%); } } } \ No newline at end of file diff --git a/web/src/pages/game/components/GameBoard.tsx b/web/src/pages/game/components/GameBoard.tsx index 31078b1..bfacbe7 100644 --- a/web/src/pages/game/components/GameBoard.tsx +++ b/web/src/pages/game/components/GameBoard.tsx @@ -1,76 +1,31 @@ +import { buildBoard, Cell, directions, Exit } from "interface"; + const GameBoard = () => { - const range = (n: number) => Array.from(Array(n).keys()); - const cells = range(11).flatMap((rowIndex) => { - return range(11).map((colIndex) => { - return { rowIndex, colIndex }; - }); - }); + const board: Cell[][] = buildBoard(); return (
- {cells.map(({ rowIndex, colIndex }) => { - if ( - (rowIndex === 0 || rowIndex === 10) && - (colIndex === 0 || colIndex === 10) - ) { - return
; - } - if (rowIndex === 0) { - return ( -
-
-
-
-
- ); - } - if (rowIndex === 10) { - return ( -
-
-
-
-
- ); - } - if (colIndex === 0) { - return ( -
-
-
-
-
- ); - } - if (colIndex === 10) { - return ( -
-
-
-
-
- ); - } - return
; - })} + {board.flatMap((row) => + row.map((cell) => ( +
+ {directions.map((direction) => { + const traversedNode = cell.getNodeAt(direction).traverseBorder(); + const isExit = traversedNode instanceof Exit; + if (!isExit) return; + const className = + `exit-${direction.toLowerCase()}` + + ` exit-${(traversedNode as Exit).type.toLowerCase()}`; + return ( +
+
+
+ ); + })} +
+ )), + )}
); - - /* - {range(9).map(() => ( -
-
-
- ))} - {range(81).map(() => ( -
- ))} - {range(9).map(() => ( -
-
-
- ))} - */ }; export default GameBoard;