diff --git a/interface/types/Cell.ts b/interface/types/Cell.ts index 8dc8236..757e471 100644 --- a/interface/types/Cell.ts +++ b/interface/types/Cell.ts @@ -1,10 +1,16 @@ import { CellType } from "../constants/CellType"; import { Direction } from "../constants/Direction"; +import { PieceId, pieceMap } from "../constants/Pieces"; import { ExternalNode } from "./ExternalNode"; +import { PlacedPiece } from "./PlacedPiece"; export class Cell { public readonly externalNodes: Map; public readonly cellType: CellType; + public placedPiece?: { + piece: PlacedPiece; + id: PieceId; + }; constructor(cellType: CellType) { this.externalNodes = new Map([ @@ -21,4 +27,12 @@ export class Cell { if (!node) throw Error(`Could not find node at ${direction}`); return node!; } + + public placePiece(pieceId: PieceId) { + if (this.placedPiece !== undefined) return; + this.placedPiece = { + piece: pieceMap[pieceId].toPlacedPiece(this), + id: pieceId, + }; + } } diff --git a/interface/types/Piece.ts b/interface/types/Piece.ts index dae249a..f684cf4 100644 --- a/interface/types/Piece.ts +++ b/interface/types/Piece.ts @@ -3,7 +3,6 @@ import { TrackType } from "../constants/TrackType"; import { Cell } from "./Cell"; import { InternalNode } from "./InternalNode"; import { PlacedPiece } from "./PlacedPiece"; -import { Node } from "./Node"; import { InternalNodeType } from "../constants/InternalNodeType"; export interface PieceProps { @@ -74,8 +73,8 @@ export class Piece { nodes: { firstNode: cell.getNodeAt(track.joinedPoints.firstPoint), secondNode: - track.joinedPoints.secondPoint instanceof Node - ? (track.joinedPoints.secondPoint as Node) + track.joinedPoints.secondPoint instanceof InternalNode + ? (track.joinedPoints.secondPoint as InternalNode) : cell.getNodeAt(track.joinedPoints.secondPoint as Direction), }, type: track.type, diff --git a/web/src/pages/game/GamePage.tsx b/web/src/pages/game/GamePage.tsx index 3b42ea0..499bd51 100644 --- a/web/src/pages/game/GamePage.tsx +++ b/web/src/pages/game/GamePage.tsx @@ -1,16 +1,32 @@ -import React from "react"; +import React, { useState } from "react"; import GameBoard from "./components/GameBoard"; import DiceSet from "./components/DiceSet"; import "./GamePage.scss"; +import { PieceId } from "interface"; const GamePage = () => { + const getRandomPieceId = () => { + const randomId = Math.floor(Math.random() * Object.keys(PieceId).length); + return Object.values(PieceId)[randomId]; + }; + + const [dice, setDice] = useState( + [1, 2, 3, 4].map(() => { + return { + pieceId: getRandomPieceId(), + isSelected: false, + isDisabled: false, + }; + }), + ); + return (

Game Page Title

- +
- +
diff --git a/web/src/pages/game/components/BoardCell.scss b/web/src/pages/game/components/BoardCell.scss new file mode 100644 index 0000000..a7822ed --- /dev/null +++ b/web/src/pages/game/components/BoardCell.scss @@ -0,0 +1,129 @@ +.cell { + width: 100px; + height: 100px; + border: 2px solid black; + border-radius: 10%; + + .piece { + border-radius: 10%; + position: relative; + + &:nth-child(2) { + top: -20px; + } + + &:nth-child(3) { + top: -40px; + } + } + + &.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/BoardCell.tsx b/web/src/pages/game/components/BoardCell.tsx new file mode 100644 index 0000000..2e1d5af --- /dev/null +++ b/web/src/pages/game/components/BoardCell.tsx @@ -0,0 +1,68 @@ +import { Cell, directions, Exit, PieceId } from "interface"; +import "./BoardCell.scss"; + +export interface BoardCellProps { + cell: Cell; + dice: { + pieceId: PieceId; + isSelected: boolean; + isDisabled: boolean; + }[]; + setDice: React.Dispatch< + React.SetStateAction< + { + pieceId: PieceId; + isSelected: boolean; + isDisabled: boolean; + }[] + > + >; + refreshBoardRender: () => void; +} + +const BoardCell = (props: BoardCellProps) => { + const { cell, dice, setDice, refreshBoardRender } = props; + const handleBoardCellClick = () => { + const selectedDie = dice.find((die) => die.isSelected); + if (!selectedDie) return; + cell.placePiece(selectedDie.pieceId); + setDice( + dice.map((die) => { + return die !== selectedDie + ? die + : { + pieceId: selectedDie.pieceId, + isSelected: false, + isDisabled: true, + }; + }), + ); + refreshBoardRender(); + }; + + return ( +
+ {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 ( +
+
+
+ ); + })} + {cell.placedPiece && ( + + )} +
+ ); +}; + +export default BoardCell; diff --git a/web/src/pages/game/components/DiceSet.scss b/web/src/pages/game/components/DiceSet.scss index 22145b0..1152b08 100644 --- a/web/src/pages/game/components/DiceSet.scss +++ b/web/src/pages/game/components/DiceSet.scss @@ -3,15 +3,4 @@ grid-template-columns: repeat(2, auto); gap: 20px; padding: 50px; - - .dice { - height: 150px; - width: 150px; - border: 2px solid black; - border-radius: 10%; - - img{ - border-radius: 10%; - } - } } \ No newline at end of file diff --git a/web/src/pages/game/components/DiceSet.tsx b/web/src/pages/game/components/DiceSet.tsx index c81e7d1..8e10fe0 100644 --- a/web/src/pages/game/components/DiceSet.tsx +++ b/web/src/pages/game/components/DiceSet.tsx @@ -1,19 +1,45 @@ import { PieceId } from "interface"; import "./DiceSet.scss"; +import Die from "./Die"; -const DiceSet = () => { - const getRandomPieceId = () => { - const randomId = Math.floor(Math.random() * Object.keys(PieceId).length); - return Object.values(PieceId)[randomId]; +export interface DiceSetProps { + dice: { + pieceId: PieceId; + isSelected: boolean; + isDisabled: boolean; + }[]; + setDice: React.Dispatch< + React.SetStateAction< + { + pieceId: PieceId; + isSelected: boolean; + isDisabled: boolean; + }[] + > + >; +} +const DiceSet = (props: DiceSetProps) => { + const { dice, setDice } = props; + const handleDieClick = (die: { + pieceId: PieceId; + isSelected: boolean; + isDisabled: boolean; + }) => { + if (die.isDisabled) return; + const newDiceState = dice.map((oldDie) => { + const isSelected = die === oldDie; + return { + ...oldDie, + isSelected: isSelected, + }; + }); + setDice(newDiceState); }; - const displayedPieceIds = [1, 2, 3, 4].map(getRandomPieceId); return (
- {displayedPieceIds.map((pieceId) => ( -
- -
+ {dice.map((die) => ( + ))}
); diff --git a/web/src/pages/game/components/Die.scss b/web/src/pages/game/components/Die.scss new file mode 100644 index 0000000..d96f7f8 --- /dev/null +++ b/web/src/pages/game/components/Die.scss @@ -0,0 +1,18 @@ +.dice { + height: 150px; + width: 150px; + border: 2px solid black; + border-radius: 10%; + + img{ + border-radius: 10%; + } + + &.selected { + box-shadow: 0 0 20px green; + } + + &.disabled { + box-shadow: 0 0 20px grey; + } +} \ No newline at end of file diff --git a/web/src/pages/game/components/Die.tsx b/web/src/pages/game/components/Die.tsx new file mode 100644 index 0000000..f6644b0 --- /dev/null +++ b/web/src/pages/game/components/Die.tsx @@ -0,0 +1,27 @@ +import { PieceId } from "interface"; +import "./Die.scss"; + +interface DieProps { + die: { pieceId: PieceId; isSelected: boolean; isDisabled: boolean }; + handleDieClick: (die: { + pieceId: PieceId; + isSelected: boolean; + isDisabled: boolean; + }) => void; +} + +const Die = (props: DieProps) => { + const { die, handleDieClick } = props; + const { pieceId, isSelected, isDisabled } = die; + + return ( +
handleDieClick(die)} + > + +
+ ); +}; + +export default Die; diff --git a/web/src/pages/game/components/GameBoard.scss b/web/src/pages/game/components/GameBoard.scss index 4d6cc7b..a11648f 100644 --- a/web/src/pages/game/components/GameBoard.scss +++ b/web/src/pages/game/components/GameBoard.scss @@ -7,121 +7,4 @@ 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 index e3dabf0..a713e7d 100644 --- a/web/src/pages/game/components/GameBoard.tsx +++ b/web/src/pages/game/components/GameBoard.tsx @@ -1,28 +1,42 @@ -import { buildBoard, Cell, directions, Exit } from "interface"; +import { buildBoard, PieceId } from "interface"; import "./GameBoard.scss"; +import { useState } from "react"; +import BoardCell from "./BoardCell"; -const GameBoard = () => { - const board: Cell[][] = buildBoard(); +export interface GameBoardProps { + dice: { + pieceId: PieceId; + isSelected: boolean; + isDisabled: boolean; + }[]; + setDice: React.Dispatch< + React.SetStateAction< + { + pieceId: PieceId; + isSelected: boolean; + isDisabled: boolean; + }[] + > + >; +} + +const GameBoard = (props: GameBoardProps) => { + const [board, setBoard] = useState(buildBoard()); + const [id, setId] = useState(1); + const refreshBoardRender = () => { + setBoard(board); + setId(id + 1); + }; 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 ( -
-
-
- ); - })} -
+ )), )}