diff --git a/interface/BoardBuilder.ts b/interface/BoardBuilder.ts new file mode 100644 index 0000000..e2e79c7 --- /dev/null +++ b/interface/BoardBuilder.ts @@ -0,0 +1,115 @@ +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) { + const cell = board[rowIndex][colIndex]; + 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)); + } + } +} + +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..6bc4347 --- /dev/null +++ b/interface/constants/Direction.ts @@ -0,0 +1,13 @@ +export enum Direction { + NORTH = "NORTH", + SOUTH = "SOUTH", + EAST = "EAST", + WEST = "WEST", +} + +export const directions: Direction[] = [ + Direction.NORTH, + Direction.SOUTH, + Direction.EAST, + Direction.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..14dcbbe --- /dev/null +++ b/interface/types/ExternalNode.ts @@ -0,0 +1,29 @@ +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); + if (other instanceof ExternalNode) { + other.border = this.border; + } + } + + 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; + } +} 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..fca4d47 --- /dev/null +++ b/web/src/pages/game/components/GameBoard.scss @@ -0,0 +1,125 @@ +.game-board { + height: 850px; + width: 850px; + display: grid; + grid-template-columns: repeat(7, auto); + gap: auto; + padding: 50px; + + .cell { + width: 100px; + height: 100px; + border: 2px solid black; + border-radius: 10%; + + &.house { + background-color: blue; + } + + &.university { + background-color: orange; + } + + &.factory { + background-color: grey; + } + + .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)); + } + } + + .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)); + } + } + + .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)); + } + } + + .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); + } + } + + &:has(.exit-north), &:has(.exit-south) { + + .exit:has(.exit-west) { + top: 50%; + 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 new file mode 100644 index 0000000..bfacbe7 --- /dev/null +++ b/web/src/pages/game/components/GameBoard.tsx @@ -0,0 +1,31 @@ +import { buildBoard, Cell, directions, Exit } from "interface"; + +const GameBoard = () => { + const board: Cell[][] = buildBoard(); + + 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 ( +
+
+
+ ); + })} +
+ )), + )} +
+ ); +}; + +export default GameBoard;