diff --git a/package-lock.json b/package-lock.json index 07d4adc..eb76420 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3064,9 +3064,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001412", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001412.tgz", - "integrity": "sha512-+TeEIee1gS5bYOiuf+PS/kp2mrXic37Hl66VY6EAfxasIk5fELTktK2oOezYed12H8w7jt3s512PpulQidPjwA==", + "version": "1.0.30001559", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001559.tgz", + "integrity": "sha512-cPiMKZgqgkg5LY3/ntGeLFUpi6tzddBNS58A4tnTgQw1zON7u2sZMU7SzOeVH4tj20++9ggL+V6FDOFMTaFFYA==", "dev": true, "funding": [ { @@ -3076,6 +3076,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ] }, @@ -16325,9 +16329,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001412", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001412.tgz", - "integrity": "sha512-+TeEIee1gS5bYOiuf+PS/kp2mrXic37Hl66VY6EAfxasIk5fELTktK2oOezYed12H8w7jt3s512PpulQidPjwA==", + "version": "1.0.30001559", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001559.tgz", + "integrity": "sha512-cPiMKZgqgkg5LY3/ntGeLFUpi6tzddBNS58A4tnTgQw1zON7u2sZMU7SzOeVH4tj20++9ggL+V6FDOFMTaFFYA==", "dev": true }, "cardinal": { diff --git a/rule-book/build-icn.png b/rule-book/build-icn.png new file mode 100644 index 0000000..0da2f52 Binary files /dev/null and b/rule-book/build-icn.png differ diff --git a/rule-book/move-icn.png b/rule-book/move-icn.png new file mode 100644 index 0000000..2b1440e Binary files /dev/null and b/rule-book/move-icn.png differ diff --git a/rule-book/recruit-icn.png b/rule-book/recruit-icn.png new file mode 100644 index 0000000..1691e01 Binary files /dev/null and b/rule-book/recruit-icn.png differ diff --git a/src/@types/storming.d.ts b/src/@types/storming.d.ts index 919be94..1d62347 100644 --- a/src/@types/storming.d.ts +++ b/src/@types/storming.d.ts @@ -8,7 +8,7 @@ type PlayerType = | "enemy2" /* blue */ | "enemy3" /* green */; -type PlayerHandCardStatus = "available" | "selected" | "played"; +type PlayerHandCardStatus = "selected" | "available" | "played"; /** * ```ts @@ -23,11 +23,11 @@ type PlayerHand = { card: Card; status: PlayerHandCardStatus }[]; declare type TileID = import("models/tiles")._TileID; -interface Coordinates { +type Coordinates = { x: number; y: number; str: TileID; -} +}; type Board = Record; @@ -63,34 +63,37 @@ interface TileWithStatus extends Tile { // CARDS // -------------- +type CardId = `${PlayerType}_${ActionCardType | EventCardType}_${number}`; + type ActionCardType = "build" | "diplo" | "move" | "recruit"; type Card = ActionCard | EventCard; -interface ActionCard { +type ActionCard = { cardType: "actionCard"; action: ActionCardType; owner: PlayerType; - cardId: string; -} + cardId: CardId; +}; -type EventCardType = "even1" | "event2" | "event3"; +type EventCardType = "event1" | "event2" | "event3"; -interface EventCard { +type EventCard = { cardType: "eventCard"; event: EventCardType; playedBy: PlayerType; - cardId: string; -} + cardId: CardId; +}; // -------------- // TIMELINE // -------------- -type Timeline = { - current: Card | undefined; - next: Card[]; - future: Card[]; +type PhaseType = "setup" | "planification" | "action"; + +type TimelineCard = { + card: Card; + commited: boolean; }; // -------------- @@ -111,54 +114,40 @@ interface GameLogContext { // GameContext // -------------- -type PhaseType = "setup" | "planification" | "action"; - -interface BuildAction { - tile: TileID; - building: Building; -} - -interface MoveAction { - from: TileID; - to: TileID; - piece: Piece; -} - -interface RecruitAction { - tile: TileID; - piece: Piece; -} - -interface PlanAction { - player: PlayerType; - nextCard: ActionCard; - futureCard: ActionCard; - eventCard?: EventCard; // TODO !MVP -} - -interface PlayerStatus { +type PlayerStatus = { player: PlayerType; points: number; - greatestEmpirePoint: boolean; -} + greatestEmpirePoint: boolean; // deprecated +}; -interface GameContext { +type GameContext = { phase: PhaseType; - board: Board; - timeline: Timeline; activeCard: Card | undefined; activePlayer: PlayerType | undefined; + next: TimelineCard[]; + future: TimelineCard[]; + board: Board; players: PlayerStatus[]; - build(action: BuildAction): void; - move(action: MoveAction): void; - recruit(action: RecruitAction): void; - plan(action: PlanAction): void; - firstPlayer(player: PlayerType): void; + // planning phase + plan(action: { + nextActionCard?: ActionCard; + futureActionCard?: ActionCard; + eventCard?: EventCard; // TODO !MVP + }): void; + submitPlanification(): void; + + // action phase + build(action: { tile: TileID; building: Building }): void; + move(action: { from: TileID; to: TileID; piece: Piece }): void; + recruit(action: { tile: TileID; piece: Piece }): void; skip(): void; - loadSavegame(gameContext: GameContext): void -} + firstPlayer(player: PlayerType): void; // deprecated? + + // other + loadSavegame(gameContext: GameContext): void; +}; // ---- @@ -166,4 +155,4 @@ type Savegame = { createdAt: string; // ms from Epoch playerEmpireSize: number; gameContext: GameContext; -} +}; diff --git a/src/app.scss b/src/app.scss index 9f464d2..e890134 100644 --- a/src/app.scss +++ b/src/app.scss @@ -43,3 +43,19 @@ .ignore-clicks { pointer-events: none; } + +.player { + background-color: #fffaaf; +} + +.enemy1 { + background-color: #ffafaf; +} + +.enemy2 { + background-color: #afafff; +} + +.enemy3 { + background-color: #afffaf; +} diff --git a/src/app.tsx b/src/app.tsx index ca0b7ba..4b92ed0 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -6,12 +6,12 @@ import { PlayerHand, TimeLine, } from "components"; -import { GameContextProvider } from "contexts"; +import { GameContextProvider } from "game-context"; import { StrictMode } from "react"; import "./app.scss"; -function App() { +export function App() { return (
@@ -27,5 +27,3 @@ function App() { ); } - -export default App; diff --git a/src/components/game/board/__snapshots__/board.test.tsx.snap b/src/components/board/__snapshots__/board.test.tsx.snap similarity index 68% rename from src/components/game/board/__snapshots__/board.test.tsx.snap rename to src/components/board/__snapshots__/board.test.tsx.snap index 817ce2c..20e5dad 100644 --- a/src/components/game/board/__snapshots__/board.test.tsx.snap +++ b/src/components/board/__snapshots__/board.test.tsx.snap @@ -9,53 +9,53 @@ exports[` render: match snapshot 1`] = ` class="board__row board__row--3-to-equator" >
render: match snapshot 1`] = ` class="board__row board__row--2-to-equator" >
render: match snapshot 1`] = `
render: match snapshot 1`] = ` class="board__row board__row--1-to-equator" >
render: match snapshot 1`] = ` class="board__row" >
render: match snapshot 1`] = `
render: match snapshot 1`] = `
render: match snapshot 1`] = ` class="board__row board__row--1-to-equator" >
render: match snapshot 1`] = ` class="board__row board__row--2-to-equator" >
render: match snapshot 1`] = `
render: match snapshot 1`] = ` class="board__row board__row--3-to-equator" >
state validation before changing GameContext <= - * */ -function BoardController(): JSX.Element { +export function BoardController() { const gameContext = useGameContext(); - const [selectedTile, setSelectedTile] = useState(); - const [buildingTile, setBuildingTile] = useState(); - const [recruitingTile, setRecruitingTile] = useState(); + const [selectedTile, setSelectedTile] = useState(); + const [buildingTile, setBuildingTile] = useState(); + const [recruitingTile, setRecruitingTile] = useState(); /* derived state */ const board = inferVisualBoardFromGameContext(gameContext, selectedTile); @@ -35,13 +32,11 @@ function BoardController(): JSX.Element { ); } setSelectedTile(undefined); + setBuildingTile(undefined); + setRecruitingTile(undefined); gameContext.build({ tile, - building: { - type: "village" as const, - owner: piece.owner, - hasWalls: false, - }, + building: NewBuilding({ owner: piece.owner }), }); }; @@ -100,6 +95,8 @@ function BoardController(): JSX.Element { } if (piece.owner === gameContext.activePlayer) { setSelectedTile(undefined); + setBuildingTile(undefined); + setRecruitingTile(undefined); return gameContext.move({ piece, from: selectedTile, @@ -118,7 +115,6 @@ function BoardController(): JSX.Element { { tile: board[tile] } ); } - setSelectedTile(undefined); setBuildingTile(undefined); setRecruitingTile(undefined); @@ -172,8 +168,6 @@ function BoardController(): JSX.Element { return moveFromTile(tile); case "recruit": return resolveRecruitOnTile(tile); - default: - break; } }; @@ -216,5 +210,3 @@ function BoardController(): JSX.Element { ); } - -export default BoardController; diff --git a/src/components/board/board.scss b/src/components/board/board.scss new file mode 100644 index 0000000..b5e0cd0 --- /dev/null +++ b/src/components/board/board.scss @@ -0,0 +1,37 @@ +@use 'tile-size.scss' as *; + +$collapse-rows-y: calc($TILE_SIZE / 5); // 22px aprox +$row-left-space: calc($TILE_SIZE / 2); + +.board { + grid-area: board; + + position: relative; + width: -moz-fit-content; + width: fit-content; + + &__row { + display: flex; + position: relative; + + width: -moz-fit-content; + width: fit-content; + + // Loop for collapsing rows + @for $i from 2 through 7 { + &:nth-child(#{$i}) { + margin-top: $collapse-rows-y * (-1) ; + } + } + + // Loop for moving rows to the right + @for $i from 1 through 3 { + &--#{$i}-to-equator { + left: $row-left-space * $i; + } + } + } + + // debug + border: 1px solid red; +} diff --git a/src/components/game/board/board.test.tsx b/src/components/board/board.test.tsx similarity index 100% rename from src/components/game/board/board.test.tsx rename to src/components/board/board.test.tsx diff --git a/src/components/game/board/board.tsx b/src/components/board/board.tsx similarity index 85% rename from src/components/game/board/board.tsx rename to src/components/board/board.tsx index 79e25a3..a6c1d9a 100644 --- a/src/components/game/board/board.tsx +++ b/src/components/board/board.tsx @@ -1,16 +1,16 @@ -import { row } from "models/tiles"; +import { TILES, coordinates } from "models/tiles"; import { logRender } from "utils/console"; import { Piece } from "./piece"; import { Tile } from "./tile"; import "./board.scss"; -interface Props { +type Props = { state: VisualBoard; onTileClick: (titleID: Coordinates) => void; -} +}; -function Board({ state, onTileClick }: Props): JSX.Element { +function Board({ state, onTileClick }: Props) { logRender("Board"); const renderRow = (n: -3 | -2 | -1 | 0 | 1 | 2 | 3) => @@ -45,3 +45,7 @@ function Board({ state, onTileClick }: Props): JSX.Element { } export default Board; + +function row(n: -3 | -2 | -1 | 0 | 1 | 2 | 3): TileID[] { + return TILES.filter((id) => coordinates(id).y === n); +} diff --git a/src/components/game/board/build-dialog.tsx b/src/components/board/build-dialog.tsx similarity index 78% rename from src/components/game/board/build-dialog.tsx rename to src/components/board/build-dialog.tsx index 80cf70f..96f11ac 100644 --- a/src/components/game/board/build-dialog.tsx +++ b/src/components/board/build-dialog.tsx @@ -1,20 +1,16 @@ -import { Dialog } from "components/common"; import c from "classnames"; +import { Dialog } from "elements"; +import { CardSilhouette } from "components/cards"; import "./option-dialog.scss"; -import { CardSilhouette } from "../cards"; -interface Props { +type Props = { onWallOption: () => void; onUpgradeOption: () => void; onClose: () => void; -} +}; -function BuildDialog({ - onWallOption, - onUpgradeOption, - onClose, -}: Props): JSX.Element { +function BuildDialog({ onWallOption, onUpgradeOption, onClose }: Props) { return (
diff --git a/src/components/game/board/deprecated/ballista.png b/src/components/board/deprecated/ballista.png similarity index 100% rename from src/components/game/board/deprecated/ballista.png rename to src/components/board/deprecated/ballista.png diff --git a/src/components/game/board/deprecated/hexagon.svg b/src/components/board/deprecated/hexagon.svg similarity index 100% rename from src/components/game/board/deprecated/hexagon.svg rename to src/components/board/deprecated/hexagon.svg diff --git a/src/components/game/board/deprecated/knight.png b/src/components/board/deprecated/knight.png similarity index 100% rename from src/components/game/board/deprecated/knight.png rename to src/components/board/deprecated/knight.png diff --git a/src/components/game/board/deprecated/soldier.png b/src/components/board/deprecated/soldier.png similarity index 100% rename from src/components/game/board/deprecated/soldier.png rename to src/components/board/deprecated/soldier.png diff --git a/src/components/game/board/deprecated/upgrade.png b/src/components/board/deprecated/upgrade.png similarity index 100% rename from src/components/game/board/deprecated/upgrade.png rename to src/components/board/deprecated/upgrade.png diff --git a/src/components/game/board/deprecated/wall.png b/src/components/board/deprecated/wall.png similarity index 100% rename from src/components/game/board/deprecated/wall.png rename to src/components/board/deprecated/wall.png diff --git a/src/components/game/board/infer-visual-board.test.ts b/src/components/board/infer-visual-board.test.ts similarity index 56% rename from src/components/game/board/infer-visual-board.test.ts rename to src/components/board/infer-visual-board.test.ts index 909fe1a..c209e90 100644 --- a/src/components/game/board/infer-visual-board.test.ts +++ b/src/components/board/infer-visual-board.test.ts @@ -1,17 +1,11 @@ -import { initialBoard } from "contexts/game-context/initial-board"; +import { initialBoard } from "game-context/initial-board"; +import { NewCard } from "models/new-card"; import { inferVisualBoardFromGameContext } from "./infer-visual-board"; -test("board/inferVisualBoardFromGameContext()", () => { - const activeCard: Card = { - action: "build", - owner: "player", - cardType: "actionCard", - cardId: "player_build_A", - }; - +test("inferVisualBoardFromGameContext()", () => { const board = inferVisualBoardFromGameContext({ board: initialBoard, - activeCard, + activeCard: NewCard("build", "player"), }); /* only settlement of player at "-4,0" */ diff --git a/src/components/game/board/infer-visual-board.ts b/src/components/board/infer-visual-board.ts similarity index 72% rename from src/components/game/board/infer-visual-board.ts rename to src/components/board/infer-visual-board.ts index c12e256..e74319c 100644 --- a/src/components/game/board/infer-visual-board.ts +++ b/src/components/board/infer-visual-board.ts @@ -1,6 +1,5 @@ import { getAvailableTilesForActionCard } from "game-logic/available-tiles"; - /** * { * ... @@ -13,11 +12,24 @@ import { getAvailableTilesForActionCard } from "game-logic/available-tiles"; * } */ export function inferVisualBoardFromGameContext( - { board, activeCard }: Pick, + { board, activeCard }: { board: Board; activeCard: Card | undefined }, selectedTile?: TileID ): VisualBoard { + if (!activeCard) { + return Object.entries(board).reduce( + (acc, [tileId, tile]) => ({ + ...acc, + [tileId]: { + ...tile, + status: "idle", + }, + }), + {} as VisualBoard + ); + } + const availableTiles = - activeCard?.cardType === "actionCard" + activeCard.cardType === "actionCard" ? getAvailableTilesForActionCard({ board, activeCard, diff --git a/src/components/game/board/option-dialog.scss b/src/components/board/option-dialog.scss similarity index 100% rename from src/components/game/board/option-dialog.scss rename to src/components/board/option-dialog.scss diff --git a/src/components/game/board/piece.scss b/src/components/board/piece.scss similarity index 93% rename from src/components/game/board/piece.scss rename to src/components/board/piece.scss index 73c5ab3..f013b02 100644 --- a/src/components/game/board/piece.scss +++ b/src/components/board/piece.scss @@ -1,6 +1,7 @@ @use '/src/styles/colors.scss'; +@use './tile-size.scss' as *; -$piece-size: 60px; +$piece-size: calc($TILE_SIZE * 0.6); @mixin piece-icon($piece, $owner) { background-image: url('./assets/' + $piece + '-' + $owner + '.svg'); diff --git a/src/components/game/board/piece.test.tsx b/src/components/board/piece.test.tsx similarity index 100% rename from src/components/game/board/piece.test.tsx rename to src/components/board/piece.test.tsx diff --git a/src/components/game/board/piece.tsx b/src/components/board/piece.tsx similarity index 70% rename from src/components/game/board/piece.tsx rename to src/components/board/piece.tsx index 8d0f4ad..01ee142 100644 --- a/src/components/game/board/piece.tsx +++ b/src/components/board/piece.tsx @@ -2,15 +2,12 @@ import "./piece.scss"; import c from "classnames"; import { logRender } from "utils/console"; -interface Props { +type Props = { type?: PieceType; owner?: PlayerType; -} +}; -export function Piece({ - type = "soldier", - owner = "player", -}: Props): JSX.Element { +export function Piece({ type = "soldier", owner = "player" }: Props) { logRender("Piece"); return ( diff --git a/src/components/game/board/recruit-dialog.tsx b/src/components/board/recruit-dialog.tsx similarity index 95% rename from src/components/game/board/recruit-dialog.tsx rename to src/components/board/recruit-dialog.tsx index 9966e39..c46b516 100644 --- a/src/components/game/board/recruit-dialog.tsx +++ b/src/components/board/recruit-dialog.tsx @@ -1,4 +1,4 @@ -import { Dialog } from "components/common"; +import { Dialog } from "elements"; import c from "classnames"; import "./option-dialog.scss"; diff --git a/src/components/board/tile-size.scss b/src/components/board/tile-size.scss new file mode 100644 index 0000000..ba26da9 --- /dev/null +++ b/src/components/board/tile-size.scss @@ -0,0 +1 @@ +$TILE_SIZE: 12vh; diff --git a/src/components/board/tile.scss b/src/components/board/tile.scss new file mode 100644 index 0000000..691d84e --- /dev/null +++ b/src/components/board/tile.scss @@ -0,0 +1,108 @@ +@use '/src/styles/colors.scss'; +@use 'tile-size.scss' as *; + +$forbidden-tile: rgba(190, 88, 88, 0.5); + +$horizontal-padding: calc($TILE_SIZE/ 30); + +$tile-stroke: 3px; +$default-opacity: 0.7; + +@mixin setStroke($player, $color) { + + // tile__available--player + &__#{$player} { + path { + stroke: $color; + } + } +} + +@mixin setBackgroundImage($element, $image) { + &--#{$element} { + background-size: 100% 100%; + background-repeat: no-repeat; + background-position: center; + background-image: url('./assets/#{$image}'); + } +} + +.tile { + position: relative; + width: $TILE_SIZE; + height: $TILE_SIZE; + margin: 0; + padding: 0 $horizontal-padding; + + display: flex; + justify-content: center; + align-items: center; + + opacity: $default-opacity; + + &:hover { + opacity: 1; + } + + &--available { + opacity: 1; + + @include setStroke(player, colors.$player-primary, ); + @include setStroke(enemy1, colors.$enemy1-primary, ); + @include setStroke(enemy2, colors.$enemy2-primary, ); + @include setStroke(enemy3, colors.$enemy3-primary, ); + } + + /* debug: turn on at tile.tsx */ + &__id { + position: absolute; + font-size: larger; + background-color: white; + z-index: 1; + } + + &__content {} + + &__building { + // make it relative to $TILE_SIZE + width: calc(0.75 * #{$TILE_SIZE}); + height: calc(0.75 * #{$TILE_SIZE}); + + position: absolute; + bottom: 1vh; // building in the bottom + + @include setBackgroundImage('player', 'castle--player.png'); + @include setBackgroundImage('enemy1', 'castle--enemy1.png'); + @include setBackgroundImage('enemy2', 'castle--enemy2.png'); + @include setBackgroundImage('enemy3', 'castle--enemy3.png'); + } + + &__hexagon { + position: absolute; + + path { + stroke-width: $tile-stroke; + } + + &--forbidden { + path { + fill: $forbidden-tile; + } + } + + &__clickable-area { + clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%); + + // debug + // background-color: rgba(255, 0, 0, 0.292); + + // FIXME: this is absolutly manual, there should be a better way of solving this. + position: absolute; + top: 5%; + left: 5%; + width: 90%; + height: 90%; + // --- + } + } +} diff --git a/src/components/game/board/tile.test.tsx b/src/components/board/tile.test.tsx similarity index 100% rename from src/components/game/board/tile.test.tsx rename to src/components/board/tile.test.tsx diff --git a/src/components/game/board/tile.tsx b/src/components/board/tile.tsx similarity index 88% rename from src/components/game/board/tile.tsx rename to src/components/board/tile.tsx index 56bfcf8..67c6a43 100644 --- a/src/components/game/board/tile.tsx +++ b/src/components/board/tile.tsx @@ -32,7 +32,7 @@ export function Tile({ const debugTileID = false; return ( -
+
{debugTileID && {id}} {terrain === "mountain" && ( @@ -56,11 +56,10 @@ export function Tile({ diff --git a/src/components/game/cards/assets/build-bg.svg b/src/components/cards/assets/build-bg.svg similarity index 100% rename from src/components/game/cards/assets/build-bg.svg rename to src/components/cards/assets/build-bg.svg diff --git a/src/components/game/cards/assets/build-icn--enemy1.svg b/src/components/cards/assets/build-icn--enemy1.svg similarity index 100% rename from src/components/game/cards/assets/build-icn--enemy1.svg rename to src/components/cards/assets/build-icn--enemy1.svg diff --git a/src/components/game/cards/assets/build-icn--enemy2.svg b/src/components/cards/assets/build-icn--enemy2.svg similarity index 100% rename from src/components/game/cards/assets/build-icn--enemy2.svg rename to src/components/cards/assets/build-icn--enemy2.svg diff --git a/src/components/game/cards/assets/build-icn--enemy3.svg b/src/components/cards/assets/build-icn--enemy3.svg similarity index 100% rename from src/components/game/cards/assets/build-icn--enemy3.svg rename to src/components/cards/assets/build-icn--enemy3.svg diff --git a/src/components/game/cards/assets/build-icn--player.svg b/src/components/cards/assets/build-icn--player.svg similarity index 100% rename from src/components/game/cards/assets/build-icn--player.svg rename to src/components/cards/assets/build-icn--player.svg diff --git a/src/components/game/cards/assets/diplo-bg.svg b/src/components/cards/assets/diplo-bg.svg similarity index 100% rename from src/components/game/cards/assets/diplo-bg.svg rename to src/components/cards/assets/diplo-bg.svg diff --git a/src/components/game/cards/assets/diplo-icn--enemy1.svg b/src/components/cards/assets/diplo-icn--enemy1.svg similarity index 100% rename from src/components/game/cards/assets/diplo-icn--enemy1.svg rename to src/components/cards/assets/diplo-icn--enemy1.svg diff --git a/src/components/game/cards/assets/diplo-icn--enemy2.svg b/src/components/cards/assets/diplo-icn--enemy2.svg similarity index 100% rename from src/components/game/cards/assets/diplo-icn--enemy2.svg rename to src/components/cards/assets/diplo-icn--enemy2.svg diff --git a/src/components/game/cards/assets/diplo-icn--enemy3.svg b/src/components/cards/assets/diplo-icn--enemy3.svg similarity index 100% rename from src/components/game/cards/assets/diplo-icn--enemy3.svg rename to src/components/cards/assets/diplo-icn--enemy3.svg diff --git a/src/components/game/cards/assets/diplo-icn--player.svg b/src/components/cards/assets/diplo-icn--player.svg similarity index 100% rename from src/components/game/cards/assets/diplo-icn--player.svg rename to src/components/cards/assets/diplo-icn--player.svg diff --git a/src/components/game/cards/assets/move-bg.svg b/src/components/cards/assets/move-bg.svg similarity index 100% rename from src/components/game/cards/assets/move-bg.svg rename to src/components/cards/assets/move-bg.svg diff --git a/src/components/game/cards/assets/move-icn--enemy1.svg b/src/components/cards/assets/move-icn--enemy1.svg similarity index 100% rename from src/components/game/cards/assets/move-icn--enemy1.svg rename to src/components/cards/assets/move-icn--enemy1.svg diff --git a/src/components/game/cards/assets/move-icn--enemy2.svg b/src/components/cards/assets/move-icn--enemy2.svg similarity index 100% rename from src/components/game/cards/assets/move-icn--enemy2.svg rename to src/components/cards/assets/move-icn--enemy2.svg diff --git a/src/components/game/cards/assets/move-icn--enemy3.svg b/src/components/cards/assets/move-icn--enemy3.svg similarity index 100% rename from src/components/game/cards/assets/move-icn--enemy3.svg rename to src/components/cards/assets/move-icn--enemy3.svg diff --git a/src/components/game/cards/assets/move-icn--player.svg b/src/components/cards/assets/move-icn--player.svg similarity index 100% rename from src/components/game/cards/assets/move-icn--player.svg rename to src/components/cards/assets/move-icn--player.svg diff --git a/src/components/game/cards/assets/recruit-bg.svg b/src/components/cards/assets/recruit-bg.svg similarity index 100% rename from src/components/game/cards/assets/recruit-bg.svg rename to src/components/cards/assets/recruit-bg.svg diff --git a/src/components/game/cards/assets/recruit-icn--enemy1.svg b/src/components/cards/assets/recruit-icn--enemy1.svg similarity index 100% rename from src/components/game/cards/assets/recruit-icn--enemy1.svg rename to src/components/cards/assets/recruit-icn--enemy1.svg diff --git a/src/components/game/cards/assets/recruit-icn--enemy2.svg b/src/components/cards/assets/recruit-icn--enemy2.svg similarity index 100% rename from src/components/game/cards/assets/recruit-icn--enemy2.svg rename to src/components/cards/assets/recruit-icn--enemy2.svg diff --git a/src/components/game/cards/assets/recruit-icn--enemy3.svg b/src/components/cards/assets/recruit-icn--enemy3.svg similarity index 100% rename from src/components/game/cards/assets/recruit-icn--enemy3.svg rename to src/components/cards/assets/recruit-icn--enemy3.svg diff --git a/src/components/game/cards/assets/recruit-icn--player.svg b/src/components/cards/assets/recruit-icn--player.svg similarity index 100% rename from src/components/game/cards/assets/recruit-icn--player.svg rename to src/components/cards/assets/recruit-icn--player.svg diff --git a/src/components/cards/card-silhouette.scss b/src/components/cards/card-silhouette.scss new file mode 100644 index 0000000..f780b03 --- /dev/null +++ b/src/components/cards/card-silhouette.scss @@ -0,0 +1,10 @@ +@use '/src/styles/colors.scss'; + +.card-silhouette { + border: 3px dashed colors.$base-grey; + border-radius: 16px; + + &__title {} + + &__text {} +} diff --git a/src/components/game/cards/card-silhouette.tsx b/src/components/cards/card-silhouette.tsx similarity index 73% rename from src/components/game/cards/card-silhouette.tsx rename to src/components/cards/card-silhouette.tsx index 6ea6362..4f84194 100644 --- a/src/components/game/cards/card-silhouette.tsx +++ b/src/components/cards/card-silhouette.tsx @@ -3,7 +3,7 @@ import CARD_TEXT from "./card-text.json"; import "./card-silhouette.scss"; -interface Props { +type Props = { card: | "next" | "future" @@ -12,14 +12,15 @@ interface Props { | "recruit-soldier" | "recruit-knight"; onClick?: () => void; -} +}; -export function CardSilhouette({ card, onClick }: Props): JSX.Element { +export function CardSilhouette({ card, onClick }: Props) { return (
{card}
{CARD_TEXT[card]}
diff --git a/src/components/game/cards/card-text.json b/src/components/cards/card-text.json similarity index 100% rename from src/components/game/cards/card-text.json rename to src/components/cards/card-text.json diff --git a/src/components/game/cards/card.scss b/src/components/cards/card.scss similarity index 97% rename from src/components/game/cards/card.scss rename to src/components/cards/card.scss index 7caffe1..91e0e7e 100644 --- a/src/components/game/cards/card.scss +++ b/src/components/cards/card.scss @@ -23,7 +23,10 @@ $card-text-content-width: 120px; height: 280px; padding: 36px 24px; background: #FFFFFF; + margin-top: 2vh; + margin-bottom: 2vh; @include shadows.simple-box-shadow(); + } .card { @@ -35,6 +38,8 @@ $card-text-content-width: 120px; justify-content: space-between; align-items: left; + // border: 3px none transparent; + &--played { // to every children diff --git a/src/components/game/cards/card.tsx b/src/components/cards/card.tsx similarity index 96% rename from src/components/game/cards/card.tsx rename to src/components/cards/card.tsx index 58aaf83..3e54666 100644 --- a/src/components/game/cards/card.tsx +++ b/src/components/cards/card.tsx @@ -30,11 +30,11 @@ function EventCard({ card }: { card: EventCard }): JSX.Element { return
; } -interface Props { +type Props = { card: Card; status?: PlayerHandCardStatus; onClick?: () => void; -} +}; export function Card({ card, @@ -43,14 +43,15 @@ export function Card({ }: Props): JSX.Element { return (
{card.cardType === "actionCard" ? ( diff --git a/src/components/game/cards/icon-card.scss b/src/components/cards/icon-card.scss similarity index 100% rename from src/components/game/cards/icon-card.scss rename to src/components/cards/icon-card.scss diff --git a/src/components/game/cards/icon-card.tsx b/src/components/cards/icon-card.tsx similarity index 100% rename from src/components/game/cards/icon-card.tsx rename to src/components/cards/icon-card.tsx diff --git a/src/components/game/cards/index.ts b/src/components/cards/index.ts similarity index 62% rename from src/components/game/cards/index.ts rename to src/components/cards/index.ts index 13af78a..598e8fb 100644 --- a/src/components/game/cards/index.ts +++ b/src/components/cards/index.ts @@ -1 +1,2 @@ +export { Card } from "./card"; export { CardSilhouette } from "./card-silhouette"; diff --git a/src/components/common/button.scss b/src/components/common/button.scss deleted file mode 100644 index 46c0145..0000000 --- a/src/components/common/button.scss +++ /dev/null @@ -1,19 +0,0 @@ -.button { - width: 133px; - height: 34px; - - /* Auto layout */ - display: flex; - flex-direction: column; - align-items: center; - padding: 8px 0px; - flex: none; - order: 0; - flex-grow: 0; - - /* visual */ - border: 2px solid #000000; - border-radius: 16px; - background: #FFFFFF; - box-shadow: inset 0px -4px 0px #FFCA3A; -} diff --git a/src/components/common/button.tsx b/src/components/common/button.tsx deleted file mode 100644 index c9489c1..0000000 --- a/src/components/common/button.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import type { ButtonHTMLAttributes } from "react"; - -import "./button.scss"; - -interface ButtonProps extends ButtonHTMLAttributes { - children: React.ReactNode; -} - -export function Button(props: ButtonProps): JSX.Element { - return ( - - ); -} diff --git a/src/components/current-phase/__snapshots__/current-phase.test.tsx.snap b/src/components/current-phase/__snapshots__/current-phase.test.tsx.snap new file mode 100644 index 0000000..1c9da09 --- /dev/null +++ b/src/components/current-phase/__snapshots__/current-phase.test.tsx.snap @@ -0,0 +1,174 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render: match snapshot - action 1`] = ` + +
+
+

+ action +

+
+
+
+
+
+ + move + +
+ +
+
+
+ +`; + +exports[` render: match snapshot - planification 1`] = ` + +
+
+

+ planification +

+
+
+
+
+
+ next +
+
+ Drag here the card you wish to use next round +
+
+
+
+ future +
+
+ Drag here the card you wish to use in two rounds +
+
+ +
+
+
+
+`; + +exports[` render: match snapshot - planification with cards 1`] = ` + +
+
+

+ planification +

+
+
+
+
+
+
+ move +
+
+ Move up to two different Units +
+
+
+
+
+
+ recruit +
+
+ Recruit units at up to two different Settlements +
+
+
+ +
+
+
+ +`; diff --git a/src/components/game/current-phase/current-phase-controller.test.tsx b/src/components/current-phase/current-phase-controller.test.tsx similarity index 89% rename from src/components/game/current-phase/current-phase-controller.test.tsx rename to src/components/current-phase/current-phase-controller.test.tsx index 6f833e6..c0e29f2 100644 --- a/src/components/game/current-phase/current-phase-controller.test.tsx +++ b/src/components/current-phase/current-phase-controller.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from "@testing-library/react"; +import { GameContextProvider } from "game-context"; import { CurrentPhaseController } from "./current-phase-controller"; -import { GameContextProvider } from "contexts"; describe("", () => { test("reads GameContext.phase", async () => { diff --git a/src/components/current-phase/current-phase-controller.tsx b/src/components/current-phase/current-phase-controller.tsx new file mode 100644 index 0000000..f65a63e --- /dev/null +++ b/src/components/current-phase/current-phase-controller.tsx @@ -0,0 +1,45 @@ +import { useGameContext } from "game-context"; +import { CurrentPhase } from "./current-phase"; +import { mustSkip } from "./must-skip"; +import { warnInconsistentState } from "utils/console"; +import { isActionCard } from "models/new-card"; + +export function CurrentPhaseController() { + const gameContext = useGameContext(); + + if (!gameContext.activePlayer) { + warnInconsistentState(": no active player"); + return <>ERROR; + } + + if (gameContext.phase === "setup") { + return
; + } + + const nextAction = gameContext.next.find( + (timelineCard) => + timelineCard.commited === false && isActionCard(timelineCard.card) + )?.card as ActionCard; + + const futureAction = gameContext.future.find( + (timelineCard) => + timelineCard.commited === false && isActionCard(timelineCard.card) + )?.card as ActionCard; + + return ( + { + gameContext.skip(); + }} + onSubmitPlan={() => { + gameContext.submitPlanification(); + }} + /> + ); +} diff --git a/src/components/current-phase/current-phase.scss b/src/components/current-phase/current-phase.scss new file mode 100644 index 0000000..ee6143c --- /dev/null +++ b/src/components/current-phase/current-phase.scss @@ -0,0 +1,33 @@ +.current-phase { + height: 100vh; + display: flex; + flex-direction: column; + overflow: hidden; + + &__heading { + display: flex; + flex: 1; + align-items: center; // center Y + justify-content: center; // center X + + max-height: 20%; + + border-bottom: 1px solid #ccc; + } + + &__content { + display: flex; + flex: 1; + flex-direction: column; + align-items: center; // center Y + justify-content: flex-start; + } + + &__planification { + display: flex; + flex-direction: column; + align-items: center; // center Y + } + + &__action {} +} diff --git a/src/components/current-phase/current-phase.test.tsx b/src/components/current-phase/current-phase.test.tsx new file mode 100644 index 0000000..d6da384 --- /dev/null +++ b/src/components/current-phase/current-phase.test.tsx @@ -0,0 +1,42 @@ +import { render } from "@testing-library/react"; +import { CurrentPhase } from "./current-phase"; +import { NewCard } from "models/new-card"; + +describe("", () => { + test("render: match snapshot - action", () => { + const { asFragment } = render( + + ); + expect(asFragment()).toMatchSnapshot(); + }); + + test("render: match snapshot - planification", () => { + const { asFragment } = render( + + ); + expect(asFragment()).toMatchSnapshot(); + }); + + test("render: match snapshot - planification with cards", () => { + const { asFragment } = render( + + ); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/src/components/current-phase/current-phase.tsx b/src/components/current-phase/current-phase.tsx new file mode 100644 index 0000000..ad1a31b --- /dev/null +++ b/src/components/current-phase/current-phase.tsx @@ -0,0 +1,80 @@ +import c from "classnames"; +import { Button } from "elements"; +import { Card, CardSilhouette } from "components/cards"; +import IconCard from "components/cards/icon-card"; // TODO: deprecated +import { logRender } from "utils/console"; + +import "./current-phase.scss"; + +type ActionPhaseProps = { + phase: "action"; + activePlayer: PlayerType; + activeCard?: Card; + mustSkip: boolean; + onSkip: () => void; +}; + +type PlanningPhaseProps = { + phase: "planification"; + activePlayer: PlayerType; + nextAction?: ActionCard; + futureAction?: ActionCard; + event?: EventCard; // TODO Event Cards + onSubmitPlan: () => void; +}; + +export function CurrentPhase(props: ActionPhaseProps | PlanningPhaseProps) { + logRender("CurrentPhase"); + + return ( +
+
+

{props.phase}

+
+
+ {props.phase === "action" ? ( + + ) : ( + + )} +
+
+ ); +} + +function ActionPhase({ activeCard, mustSkip, onSkip }: ActionPhaseProps) { + return ( +
+ + +
+ ); +} + +function PlanningPhase({ + activePlayer, + nextAction, + futureAction, + onSubmitPlan, +}: PlanningPhaseProps) { + const buttonDisabled = !nextAction || !futureAction; + return ( +
+ {nextAction ? : } + {futureAction ? ( + + ) : ( + + )} + +
+ ); +} diff --git a/src/components/current-phase/must-skip.test.ts b/src/components/current-phase/must-skip.test.ts new file mode 100644 index 0000000..70c7945 --- /dev/null +++ b/src/components/current-phase/must-skip.test.ts @@ -0,0 +1,22 @@ +import { emptyBoard } from "game-context/empty-board"; +import { initialBoard } from "game-context/initial-board"; +import { NewCard } from "models/new-card"; +import { mustSkip } from "./must-skip"; + +describe("mustSkip()", () => { + test("returns true if no available tiles for action card", () => { + const gameContext = { + activeCard: NewCard("build", "player"), + board: emptyBoard, + } as GameContext; + expect(mustSkip(gameContext)).toBe(true); + }); + + test("returns false if there are available tiles for action card", () => { + const gameContext = { + activeCard: NewCard("build", "player"), + board: initialBoard, + } as GameContext; + expect(mustSkip(gameContext)).toBe(false); + }); +}); diff --git a/src/components/current-phase/must-skip.ts b/src/components/current-phase/must-skip.ts new file mode 100644 index 0000000..aeaa68a --- /dev/null +++ b/src/components/current-phase/must-skip.ts @@ -0,0 +1,17 @@ +import { getAvailableTilesForActionCard } from "game-logic/available-tiles"; + +export function mustSkip({ + activeCard, + board, +}: { + activeCard: Card | undefined; + board: Board; +}) { + if (activeCard?.cardType === "actionCard") { + return ( + activeCard.action !== "diplo" && + getAvailableTilesForActionCard({ board, activeCard }).length === 0 + ); + } + return false; +} diff --git a/src/components/game/board/board.scss b/src/components/game/board/board.scss deleted file mode 100644 index 40e3f7f..0000000 --- a/src/components/game/board/board.scss +++ /dev/null @@ -1,58 +0,0 @@ -// FIXME: this is absolutly manual, there should be a better way of solving this. -$collapse-rows: -26px; -$row-left-space: 60px; - -.board { - position: relative; - width: -moz-fit-content; - width: fit-content; - height: 688px; // FIXME why doesn't fit-content work? - - &__row { - display: flex; - position: relative; - - width: -moz-fit-content; - width: fit-content; - - // collapse rows - &:nth-child(2) { - top: $collapse-rows; - } - - &:nth-child(3) { - top: $collapse-rows * 2; - } - - &:nth-child(4) { - top: $collapse-rows * 3; - } - - &:nth-child(5) { - top: $collapse-rows * 4; - } - - &:nth-child(6) { - top: $collapse-rows * 5; - } - - &:nth-child(7) { - top: $collapse-rows * 6; - } - - // move rows to the right - &--1-to-equator { - left: $row-left-space; - } - - &--2-to-equator { - left: $row-left-space * 2; - } - - &--3-to-equator { - left: $row-left-space * 3; - } - } - - border: 1px solid red; -} diff --git a/src/components/game/board/tile.scss b/src/components/game/board/tile.scss deleted file mode 100644 index 9c624f0..0000000 --- a/src/components/game/board/tile.scss +++ /dev/null @@ -1,164 +0,0 @@ -@use '/src/styles/colors.scss'; - -$forbidden-tile: rgba(190, 88, 88, 0.5); - -$horizontal-padding: 5px; - -.tile { - position: relative; - width: 120px; - height: 120px; - margin: 0; - padding: 0 $horizontal-padding; - - display: flex; - justify-content: center; - align-items: center; - - /* debug: turn on at tile.tsx */ - &__id { - position: absolute; - font-size: larger; - background-color: white; - z-index: 1; - } - - &__content {} - - &__terrain { - position: absolute; - stroke: #5f5858; - stroke-width: 3px; - - &--forest { - top: 15%; - left: 15%; - - width: 80px; - height: 80px - } - - &--lake { - top: 5%; - left: 5%; - width: 110px; - height: 110px - } - } - - &__building { - width: 90px; - height: 90px; - - position: absolute; - bottom: 5px; // building in the bottom - - background-size: 100% 100%; - background-repeat: no-repeat; - background-position: center; - - &--player { - background-image: url('./assets/castle--player.png'); - } - - &--enemy1 { - background-image: url('./assets/castle--enemy1.png'); - } - - &--enemy2 { - background-image: url('./assets/castle--enemy2.png'); - } - - &--enemy3 { - background-image: url('./assets/castle--enemy3.png'); - } - } - - &__hexagon { - position: absolute; - - path { - fill: none; - } - - &--selected { - path { - fill: colors.$selected-tile; - stroke: colors.$dark-grey; - stroke-width: 3px; - } - - &-player { - path { - stroke: colors.$player-primary; - } - } - - &-enemy1 { - path { - stroke: colors.$enemy1-primary; - } - } - - &-enemy2 { - path { - stroke: colors.$enemy2-primary; - } - } - - &-enemy3 { - path { - stroke: colors.$enemy3-primary; - } - } - } - - &--available { - &-player { - path { - stroke: colors.$player-primary; - } - } - - &-enemy1 { - path { - stroke: colors.$enemy1-primary; - } - } - - &-enemy2 { - path { - stroke: colors.$enemy2-primary; - } - } - - &-enemy3 { - path { - stroke: colors.$enemy3-primary; - } - } - } - - &--forbidden { - path { - fill: $forbidden-tile; - } - } - - &__clickable-area { - clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%); - - // debug - // background-color: rgba(255, 0, 0, 0.292); - - // FIXME: this is absolutly manual, there should be a better way of solving this. - position: absolute; - top: 5%; - left: 5%; - width: 90%; - height: 90%; - // --- - } - } - -} diff --git a/src/components/game/cards/card-silhouette.scss b/src/components/game/cards/card-silhouette.scss deleted file mode 100644 index 6b4f765..0000000 --- a/src/components/game/cards/card-silhouette.scss +++ /dev/null @@ -1,18 +0,0 @@ -@use '/src/styles/colors.scss'; -@use './card.scss'; - -.card-silhouette { - @include card.card-container(); - position: relative; - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: left; - - border: 3px dashed #ADB5BD; - border-radius: 16px; - - &__title {} - - &__text {} -} diff --git a/src/components/game/current-phase/__snapshots__/current-phase.test.tsx.snap b/src/components/game/current-phase/__snapshots__/current-phase.test.tsx.snap deleted file mode 100644 index 662f283..0000000 --- a/src/components/game/current-phase/__snapshots__/current-phase.test.tsx.snap +++ /dev/null @@ -1,53 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` render: match snapshot 1`] = ` - -
-

- setup -

-
-
-
- next -
-
- Drag here the card you wish to use next round -
-
-
-
- future -
-
- Drag here the card you wish to use in two rounds -
-
- -
-
-
-`; diff --git a/src/components/game/current-phase/current-phase-controller.tsx b/src/components/game/current-phase/current-phase-controller.tsx deleted file mode 100644 index c3295f5..0000000 --- a/src/components/game/current-phase/current-phase-controller.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useGameContext } from "contexts"; -import { CurrentPhase } from "./current-phase"; -import { mustSkip } from "./must-skip"; - -/** - * Renders depends on: - * - `useGameContext()` - * - * Defines visual current phase from GameContext - * - */ -export function CurrentPhaseController(): JSX.Element { - const gameContext = useGameContext(); - - return ( - { - gameContext.skip(); - }} - /> - ); -} diff --git a/src/components/game/current-phase/current-phase.scss b/src/components/game/current-phase/current-phase.scss deleted file mode 100644 index 2e66f1e..0000000 --- a/src/components/game/current-phase/current-phase.scss +++ /dev/null @@ -1,27 +0,0 @@ -.current-phase { - width: 100%; - - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: center; - - background-color: rgb(243, 220, 220); - - &__heading { - height: 100px; - } - - &__planning { - height: 100%; - - .card-silhouette { - margin: 10px; // FIXME - } - } - - &__action { - height: 100%; - } - -} diff --git a/src/components/game/current-phase/current-phase.test.tsx b/src/components/game/current-phase/current-phase.test.tsx deleted file mode 100644 index f914a3b..0000000 --- a/src/components/game/current-phase/current-phase.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { render } from "@testing-library/react"; -import { CurrentPhase } from "./current-phase"; - -describe("", () => { - test("render: match snapshot", () => { - const { asFragment } = render(); - expect(asFragment()).toMatchSnapshot(); - }); -}); diff --git a/src/components/game/current-phase/current-phase.tsx b/src/components/game/current-phase/current-phase.tsx deleted file mode 100644 index 5174f1d..0000000 --- a/src/components/game/current-phase/current-phase.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { Button } from "components/common"; -import IconCard from "components/game/cards/icon-card"; // TODO: deprecated -import { logRender } from "utils/console"; -import { CardSilhouette } from "components/game/cards"; - -import "./current-phase.scss"; - -interface Props { - phase?: PhaseType; - activeCard?: Card; - mustSkip?: boolean; - onSkip: () => void; -} - -export function CurrentPhase({ - phase = 'setup', - activeCard = undefined, - mustSkip = false, - onSkip, -}: Props): JSX.Element { - logRender("CurrentPhase"); - - return ( -
-

{phase}

- {phase === "action" ? ( - - ) : ( - - )} -
- ); -} - -function ActionPhase({ activeCard, mustSkip, onSkip }: Omit) { - return ( -
- - -
- ); -} - -function PlanningPhase() { - return ( -
- - - -
- ); -} diff --git a/src/components/game/current-phase/must-skip.test.ts b/src/components/game/current-phase/must-skip.test.ts deleted file mode 100644 index 714b5c2..0000000 --- a/src/components/game/current-phase/must-skip.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { mustSkip } from "./must-skip"; -import { emptyBoard } from "contexts/game-context/empty-board"; -import { initialBoard } from "contexts/game-context/initial-board"; - -test("mustSkip() returns true if no available tiles for action card", () => { - const gameContext = { - activeCard: { - cardType: "actionCard", - action: "build", - owner: "player", - cardId: "player_build_A", - }, - board: emptyBoard, - } as GameContext; - expect(mustSkip(gameContext)).toBe(true); -}); - -test("mustSkip() returns false if there are available tiles for action card", () => { - const gameContext = { - activeCard: { - cardType: "actionCard", - action: "build", - owner: "player", - cardId: "player_build_A", - }, - board: initialBoard, - } as GameContext; - expect(mustSkip(gameContext)).toBe(false); -}); diff --git a/src/components/game/current-phase/must-skip.ts b/src/components/game/current-phase/must-skip.ts deleted file mode 100644 index 4b4d2fe..0000000 --- a/src/components/game/current-phase/must-skip.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { getAvailableTilesForActionCard } from "game-logic/available-tiles"; - -export function mustSkip(gameContext: GameContext) { - if (gameContext.activeCard?.cardType === "actionCard") { - return ( - gameContext.activeCard.action !== "diplo" && - getAvailableTilesForActionCard({ - board: gameContext.board, - activeCard: gameContext.activeCard, - }).length === 0 - ); - } - return false; -} diff --git a/src/components/game/log-panel/log-panel.scss b/src/components/game/log-panel/log-panel.scss deleted file mode 100644 index 1f61a0b..0000000 --- a/src/components/game/log-panel/log-panel.scss +++ /dev/null @@ -1,27 +0,0 @@ -@use '/src/styles/colors.scss'; - - -.log-panel { - display: flex; - flex-direction: column-reverse; - - &__entry { - - &--player { - outline: 2px solid colors.$player-primary; - } - - &--enemy1 { - outline: 2px solid colors.$enemy1-primary; - } - - &--enemy2 { - outline: 2px solid colors.$enemy2-primary; - } - - &--enemy3 { - outline: 2px solid colors.$enemy3-primary; - } - } - -} diff --git a/src/components/game/log-panel/log-panel.tsx b/src/components/game/log-panel/log-panel.tsx deleted file mode 100644 index c2cd9eb..0000000 --- a/src/components/game/log-panel/log-panel.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { useGameLog } from "contexts"; -import c from "classnames"; - -import "./log-panel.scss"; - -function LogPanel(): JSX.Element { - const { actions } = useGameLog(); - return ( -
- {actions.map((action, i) => ( - - ))} -
- ); -} - -interface LogEntryProps { - action: ActionLog; -} - -function LogEntry({ action }: LogEntryProps): JSX.Element { - return ( -
- {action.msg} -
- ); -} - -export default LogPanel; diff --git a/src/components/game/player-hand/infer-player-hands.ts b/src/components/game/player-hand/infer-player-hands.ts deleted file mode 100644 index 8826650..0000000 --- a/src/components/game/player-hand/infer-player-hands.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { PLAYER_CARDS } from "models"; - -function getPlayedActionCards(timeline: Timeline): ActionCard[] { - const playedCards = [...timeline.next, ...timeline.future]; - if (timeline.current) { - playedCards.unshift(timeline.current); // insert timeline.current >> [X...] - } - return playedCards.filter( - (playedCard) => playedCard.cardType === "actionCard" - ) as ActionCard[]; -} - -function defineCardStatus({ - cardId, - selectedCard, - playedActionCards, -}: { - playedActionCards: ActionCard[]; - cardId: string; - selectedCard?: ActionCard; -}): PlayerHandCardStatus { - if (selectedCard && selectedCard.cardId === cardId) { - return "selected"; - } - const hasBeenPlayed = playedActionCards.some( - (playedCard) => playedCard.cardId === cardId - ); - return hasBeenPlayed ? "played" : "available"; -} - -/** - * { - * "player": [ { card, status }, { card, status }, { card, status }... ] - * "enemy1": [ ... ] - * "enemy2": [ ... ] - * "enemy3": [ ... ] - * } - */ -export function inferPlayerHandsFromGameContext( - timeline: Timeline, - selectedCard?: ActionCard -) { - const playedActionCards = getPlayedActionCards(timeline); - - const players = Object.keys(PLAYER_CARDS) as PlayerType[]; - const playerHands = players.reduce((acc, player) => { - const playerHand = PLAYER_CARDS[player].map((card) => { - const status = defineCardStatus({ - cardId: card.cardId, - selectedCard, - playedActionCards, - }); - return { card, status }; - }); - return { - ...acc, - [player]: playerHand, - }; - }, {} as Record); - return playerHands; -} diff --git a/src/components/game/player-hand/player-hand-controller.tsx b/src/components/game/player-hand/player-hand-controller.tsx deleted file mode 100644 index ad2d03f..0000000 --- a/src/components/game/player-hand/player-hand-controller.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { useGameContext } from "contexts"; -import { useEffect, useState } from "react"; -import { warnInconsistentState } from "utils/console"; -import { inferPlayerHandsFromGameContext } from "./infer-player-hands"; -import PlayerHand from "./player-hand"; - -/** - * Renders depends on: - * - `useGameContext()` - * - * Defines visual player hand from GameContext - * - */ -function PlayerHandController(): JSX.Element { - const gameContext = useGameContext(); - const [nextCard, setNextCard] = useState(undefined); - - // FIXME can I change the approach: new player -> no selected card - useEffect(() => { - setNextCard(undefined); - }, [gameContext.activePlayer]); - - /* infered state */ - const playerCards = inferPlayerHandsFromGameContext( - gameContext.timeline, - nextCard - ); - const activePlayerHand = gameContext.activePlayer - ? playerCards[gameContext.activePlayer] - : []; - - const handlePlayerCardClick = (cardId: string) => { - if (!gameContext.activePlayer) { - return warnInconsistentState( - `trying to handle click on ${cardId} while no activePlayer` - ); - } - const playerCard = activePlayerHand.find((e) => e.card.cardId === cardId); - if (!playerCard) { - return warnInconsistentState( - `trying to handle click on ${cardId}, can't find this card on player's hand`, - { activePlayerHand } - ); - } - - if (playerCard.status !== 'available') { - return - } - - if (nextCard) { - gameContext.plan({ - nextCard, - futureCard: playerCard.card as ActionCard, // casting? XXX - player: gameContext.activePlayer, - }); - } else { - setNextCard(playerCard.card as ActionCard); // casting? XXX - } - }; - - return ( - - ); -} - -export default PlayerHandController; diff --git a/src/components/game/timeline/timeline-controller.tsx b/src/components/game/timeline/timeline-controller.tsx deleted file mode 100644 index 4d00600..0000000 --- a/src/components/game/timeline/timeline-controller.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { useGameContext } from "contexts"; -import { Timeline } from "./timeline"; - -/** - * Renders depends on: - * - useGameContext() - */ -export function TimelineController(): JSX.Element { - const gameContext = useGameContext(); - - return ; -} diff --git a/src/components/game/timeline/timeline.test.tsx b/src/components/game/timeline/timeline.test.tsx deleted file mode 100644 index ceffeff..0000000 --- a/src/components/game/timeline/timeline.test.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { render } from "@testing-library/react"; -import { Timeline } from "./timeline"; - -describe("", () => { - test("render: match snapshot", () => { - const { asFragment } = render(); - expect(asFragment()).toMatchSnapshot(); - }); -}); - -const timeline: Timeline = { - current: { - cardType: "actionCard", - action: "move", - owner: "player", - cardId: "player_move_A", - }, - next: [ - { - cardType: "actionCard", - action: "move", - owner: "enemy1", - cardId: "enemy1_move_A", - }, - { - cardType: "actionCard", - action: "move", - owner: "enemy2", - cardId: "enemy2_move_A", - }, - { - cardType: "actionCard", - action: "move", - owner: "enemy3", - cardId: "enemy3_move_A", - }, - ], - future: [], -}; diff --git a/src/components/index.ts b/src/components/index.ts index 9f93599..6e7b6f0 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,8 +1,7 @@ -export { Menu } from "./common/menu/menu"; -export { default as Board } from "./game/board/board-controller"; -export { default as LogPanel } from "./game/log-panel/log-panel"; -export { default as Marketplace } from "./game/marketplace/marketplace"; -export { default as PlayerHand } from "./game/player-hand/player-hand-controller"; -export { default as RoundSummary } from "./game/round-summary/round-summary"; -export { TimelineController as TimeLine } from "./game/timeline/timeline-controller"; -export { CurrentPhaseController as CurrentPhase } from "./game/current-phase/current-phase-controller"; +export { BoardController as Board } from "./board/board-controller"; +export { CurrentPhaseController as CurrentPhase } from "./current-phase/current-phase-controller"; +export { default as Marketplace } from "./marketplace/marketplace"; +export { Menu } from "./menu/menu"; +export { PlayerHandController as PlayerHand } from "./player-hand/player-hand-controller"; +export { default as RoundSummary } from "./round-summary/round-summary"; +export { TimelineController as TimeLine } from "./timeline/timeline-controller"; diff --git a/src/components/game/marketplace/marketplace.scss b/src/components/marketplace/marketplace.scss similarity index 100% rename from src/components/game/marketplace/marketplace.scss rename to src/components/marketplace/marketplace.scss diff --git a/src/components/game/marketplace/marketplace.tsx b/src/components/marketplace/marketplace.tsx similarity index 100% rename from src/components/game/marketplace/marketplace.tsx rename to src/components/marketplace/marketplace.tsx diff --git a/src/components/common/menu/menu-icon.svg b/src/components/menu/menu-icon.svg similarity index 100% rename from src/components/common/menu/menu-icon.svg rename to src/components/menu/menu-icon.svg diff --git a/src/components/common/menu/menu.scss b/src/components/menu/menu.scss similarity index 100% rename from src/components/common/menu/menu.scss rename to src/components/menu/menu.scss diff --git a/src/components/common/menu/menu.tsx b/src/components/menu/menu.tsx similarity index 95% rename from src/components/common/menu/menu.tsx rename to src/components/menu/menu.tsx index a007705..b68f1bf 100644 --- a/src/components/common/menu/menu.tsx +++ b/src/components/menu/menu.tsx @@ -1,5 +1,5 @@ -import { Button, Dialog } from "components/common"; -import { useGameContext } from "contexts"; +import { Button, Dialog } from "elements"; +import { useGameContext } from "game-context"; import { useState } from "react"; import { listSavegames, loadSavegame, savegame } from "services/db"; import { logRender } from "utils/console"; diff --git a/src/components/game/player-hand/__snapshots__/player-hand.test.tsx.snap b/src/components/player-hand/__snapshots__/player-hand.test.tsx.snap similarity index 100% rename from src/components/game/player-hand/__snapshots__/player-hand.test.tsx.snap rename to src/components/player-hand/__snapshots__/player-hand.test.tsx.snap diff --git a/src/components/game/player-hand/infer-player-hands.test.ts b/src/components/player-hand/infer-player-hands.test.ts similarity index 80% rename from src/components/game/player-hand/infer-player-hands.test.ts rename to src/components/player-hand/infer-player-hands.test.ts index cae964d..74749fc 100644 --- a/src/components/game/player-hand/infer-player-hands.test.ts +++ b/src/components/player-hand/infer-player-hands.test.ts @@ -1,63 +1,53 @@ +import { NewCard, _resetCardId } from "models/new-card"; import { inferPlayerHandsFromGameContext } from "./infer-player-hands"; -const timeline: Timeline = { - current: undefined, - next: [ - { - cardType: "actionCard", - action: "build", - owner: "player", - cardId: "player_build_1", - }, - { - cardType: "actionCard", - action: "move", - owner: "enemy1", - cardId: "enemy1_move_1", - }, - { - cardType: "actionCard", - action: "recruit", - owner: "enemy2", - cardId: "enemy2_recruit_1", - }, - { - cardType: "actionCard", - action: "move", - owner: "enemy3", - cardId: "enemy3_move_2", - }, - ], - future: [ - { - cardType: "actionCard", - action: "move", - owner: "player", - cardId: "player_move_1", - }, - { - cardType: "actionCard", - action: "move", - owner: "enemy1", - cardId: "enemy1_move_2", - }, - { - cardType: "actionCard", - action: "diplo", - owner: "enemy2", - cardId: "enemy2_diplo_1", - }, - { - cardType: "actionCard", - action: "build", - owner: "enemy3", - cardId: "enemy3_build_1", - }, - ], -}; +function mockTimeline() { + return { + activeCard: undefined, + next: [ + { + card: NewCard("build", "player"), + commited: true, + }, + { + card: NewCard("move", "enemy1"), + commited: true, + }, + { + card: NewCard("recruit", "enemy2"), + commited: true, + }, + { + card: NewCard("move", "enemy3"), + commited: true, + }, + ], + future: [ + { + card: NewCard("move", "player"), + commited: true, + }, + { + card: NewCard("move", "enemy1"), + commited: true, + }, + { + card: NewCard("diplo", "enemy2"), + commited: true, + }, + { + card: NewCard("build", "enemy3"), + commited: true, + }, + ], + }; +} describe("inferPlayerHandsFromGameContext()", () => { + beforeEach(_resetCardId); + it("returns card status grouped by player", () => { + const timeline = mockTimeline(); const got = inferPlayerHandsFromGameContext(timeline); const player = [ @@ -221,7 +211,7 @@ describe("inferPlayerHandsFromGameContext()", () => { owner: "enemy3", cardId: "enemy3_move_1", }, - status: "available", + status: "played", }, { card: { @@ -230,7 +220,7 @@ describe("inferPlayerHandsFromGameContext()", () => { owner: "enemy3", cardId: "enemy3_move_2", }, - status: "played", + status: "available", }, { card: { diff --git a/src/components/player-hand/infer-player-hands.ts b/src/components/player-hand/infer-player-hands.ts new file mode 100644 index 0000000..4e6edf9 --- /dev/null +++ b/src/components/player-hand/infer-player-hands.ts @@ -0,0 +1,53 @@ +import { isActionCard } from "models/new-card"; +import { PLAYER_CARDS } from "models/player-cards"; + +function getPlayedActionCards({ + activeCard, + next, + future, +}: { + activeCard: Card | undefined; + next: TimelineCard[]; + future: TimelineCard[]; +}): ActionCard[] { + const playedCards = [...next, ...future]; + if (activeCard && isActionCard(activeCard)) { + // insert timeline.current >> [X...] + playedCards.unshift({ card: activeCard, commited: true }); + } + return playedCards + .filter((playedCard) => isActionCard(playedCard.card)) + .map((playedCard) => playedCard.card as ActionCard); +} + +/** + * { + * "player": [ { card, status }, { card, status }, { card, status }... ] + * "enemy1": [ ... ] + * "enemy2": [ ... ] + * "enemy3": [ ... ] + * } + */ +export function inferPlayerHandsFromGameContext(playedCards: { + activeCard: Card | undefined; + next: TimelineCard[]; + future: TimelineCard[]; +}) { + const playedActionCards = getPlayedActionCards(playedCards); + const players = Object.keys(PLAYER_CARDS) as PlayerType[]; + const playerHands = players.reduce((acc, player) => { + const playerHand = PLAYER_CARDS[player].map((card) => { + const status = playedActionCards.some( + (playedCard) => playedCard.cardId === card.cardId + ) + ? "played" + : "available"; + return { card, status }; + }); + return { + ...acc, + [player]: playerHand, + }; + }, {} as Record); + return playerHands; +} diff --git a/src/components/player-hand/player-hand-controller.tsx b/src/components/player-hand/player-hand-controller.tsx new file mode 100644 index 0000000..35f5e68 --- /dev/null +++ b/src/components/player-hand/player-hand-controller.tsx @@ -0,0 +1,69 @@ +import { useGameContext } from "game-context"; +import { isActionCard } from "models/new-card"; +import { logRender, warnInconsistentState } from "utils/console"; +import { inferPlayerHandsFromGameContext } from "./infer-player-hands"; +import { PlayerHand } from "./player-hand"; + +export function PlayerHandController() { + logRender("PlayerHandController"); + const gameContext = useGameContext(); + + if (!gameContext.activePlayer) { + warnInconsistentState( + "trying to render while no activePlayer", + { gameContext } + ); + return <>ERROR; + } + + /* infered state */ + const nextCard = gameContext.next.find( + (timelineCard) => !timelineCard.commited && isActionCard(timelineCard.card) + )?.card as ActionCard; + const playerCards = inferPlayerHandsFromGameContext(gameContext); + const activePlayerHand = playerCards[gameContext.activePlayer]; + + const handlePlayerCardClick = (cardId: CardId) => { + if (!gameContext.activePlayer) { + return warnInconsistentState( + `trying to handle click on ${cardId} while no activePlayer` + ); + } + const playerCard = activePlayerHand.find((e) => e.card.cardId === cardId); + if (!playerCard) { + return warnInconsistentState( + `trying to handle click on ${cardId}, can't find this card on player's hand`, + { activePlayerHand } + ); + } + + if (playerCard.status !== "available") { + return; // ignore click + } + + if (!isActionCard(playerCard.card)) { + // TODO; event cards + return; + } + + if (nextCard) { + gameContext.plan({ + futureActionCard: playerCard.card, + }); + } else { + // FIXME: we may only modify future card once next card is set + gameContext.plan({ + nextActionCard: playerCard.card, + }); + } + }; + + return ( + + ); +} diff --git a/src/components/game/player-hand/player-hand.scss b/src/components/player-hand/player-hand.scss similarity index 100% rename from src/components/game/player-hand/player-hand.scss rename to src/components/player-hand/player-hand.scss diff --git a/src/components/game/player-hand/player-hand.test.tsx b/src/components/player-hand/player-hand.test.tsx similarity index 50% rename from src/components/game/player-hand/player-hand.test.tsx rename to src/components/player-hand/player-hand.test.tsx index 3335d01..39aa686 100644 --- a/src/components/game/player-hand/player-hand.test.tsx +++ b/src/components/player-hand/player-hand.test.tsx @@ -1,41 +1,27 @@ import { render } from "@testing-library/react"; -import PlayerHand from "./player-hand"; - -describe("", () => { - test("render: match snapshot", () => { - const { asFragment } = render( - - ); - expect(asFragment()).toMatchSnapshot(); - }); -}); +import { NewCard } from "models/new-card"; +import { PlayerHand } from "./player-hand"; const playerHand: PlayerHand = [ { - card: { - cardType: "actionCard", - action: "move", - owner: "player", - cardId: "player_move_A", - }, + card: NewCard("move", "player"), status: "available", }, { - card: { - cardType: "actionCard", - action: "move", - owner: "player", - cardId: "player_move_B", - }, + card: NewCard("move", "player"), status: "available", }, { - card: { - cardType: "actionCard", - action: "build", - owner: "player", - cardId: "player_build_A", - }, + card: NewCard("build", "player"), status: "available", }, ]; + +describe("", () => { + test("render: match snapshot", () => { + const { asFragment } = render( + + ); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/src/components/game/player-hand/player-hand.tsx b/src/components/player-hand/player-hand.tsx similarity index 75% rename from src/components/game/player-hand/player-hand.tsx rename to src/components/player-hand/player-hand.tsx index 29e09d3..30e361d 100644 --- a/src/components/game/player-hand/player-hand.tsx +++ b/src/components/player-hand/player-hand.tsx @@ -1,16 +1,16 @@ -import { Card } from "components/game/cards/card"; +import { Card } from "components/cards/card"; import { logRender } from "utils/console"; import "./player-hand.scss"; -interface Props { +type Props = { cards: PlayerHand; isActive?: boolean; player?: PlayerType; - onClick: (cardId: string) => void; -} + onClick: (cardId: CardId) => void; +}; -function PlayerHand({ +export function PlayerHand({ cards, isActive = false, player = undefined, @@ -18,7 +18,7 @@ function PlayerHand({ }: Props): JSX.Element { logRender("PlayerHand"); - const considerOnClickIfActive = (cardId: string) => { + const considerOnClickIfActive = (cardId: CardId) => { isActive && onClick(cardId); }; @@ -38,5 +38,3 @@ function PlayerHand({
); } - -export default PlayerHand; diff --git a/src/components/game/round-summary/assets/enemy-1.png b/src/components/round-summary/assets/enemy-1.png similarity index 100% rename from src/components/game/round-summary/assets/enemy-1.png rename to src/components/round-summary/assets/enemy-1.png diff --git a/src/components/game/round-summary/assets/enemy-2.png b/src/components/round-summary/assets/enemy-2.png similarity index 100% rename from src/components/game/round-summary/assets/enemy-2.png rename to src/components/round-summary/assets/enemy-2.png diff --git a/src/components/game/round-summary/assets/enemy-3.png b/src/components/round-summary/assets/enemy-3.png similarity index 100% rename from src/components/game/round-summary/assets/enemy-3.png rename to src/components/round-summary/assets/enemy-3.png diff --git a/src/components/game/round-summary/assets/player.png b/src/components/round-summary/assets/player.png similarity index 100% rename from src/components/game/round-summary/assets/player.png rename to src/components/round-summary/assets/player.png diff --git a/src/components/game/round-summary/assets/star.png b/src/components/round-summary/assets/star.png similarity index 100% rename from src/components/game/round-summary/assets/star.png rename to src/components/round-summary/assets/star.png diff --git a/src/components/game/round-summary/player-summary.scss b/src/components/round-summary/player-summary.scss similarity index 100% rename from src/components/game/round-summary/player-summary.scss rename to src/components/round-summary/player-summary.scss diff --git a/src/components/game/round-summary/player-summary.tsx b/src/components/round-summary/player-summary.tsx similarity index 100% rename from src/components/game/round-summary/player-summary.tsx rename to src/components/round-summary/player-summary.tsx diff --git a/src/components/game/round-summary/round-summary.scss b/src/components/round-summary/round-summary.scss similarity index 100% rename from src/components/game/round-summary/round-summary.scss rename to src/components/round-summary/round-summary.scss diff --git a/src/components/game/round-summary/round-summary.tsx b/src/components/round-summary/round-summary.tsx similarity index 91% rename from src/components/game/round-summary/round-summary.tsx rename to src/components/round-summary/round-summary.tsx index 0026ea1..72cf0cf 100644 --- a/src/components/game/round-summary/round-summary.tsx +++ b/src/components/round-summary/round-summary.tsx @@ -1,14 +1,10 @@ -import { useGameContext } from "contexts"; +import { useGameContext } from "game-context"; import { logRender } from "utils/console"; import PlayerSummary from "./player-summary"; import "./round-summary.scss"; -/** - * Renders depends on: - * - useGameContext() - */ function RoundSummary(): JSX.Element { logRender("RoundSummary"); diff --git a/src/components/game/timeline/__snapshots__/timeline.test.tsx.snap b/src/components/timeline/__snapshots__/timeline.test.tsx.snap similarity index 100% rename from src/components/game/timeline/__snapshots__/timeline.test.tsx.snap rename to src/components/timeline/__snapshots__/timeline.test.tsx.snap diff --git a/src/components/game/timeline/line-item.scss b/src/components/timeline/line-item.scss similarity index 93% rename from src/components/game/timeline/line-item.scss rename to src/components/timeline/line-item.scss index 326f266..8d90118 100644 --- a/src/components/game/timeline/line-item.scss +++ b/src/components/timeline/line-item.scss @@ -17,7 +17,9 @@ $content-size: 20px; border: 2px solid black; background-color: colors.$base-grey; - + &--pending { + border: 3px dotted colors.$darkest-grey; + } &__content { width: $content-size; diff --git a/src/components/game/timeline/line-item.tsx b/src/components/timeline/line-item.tsx similarity index 62% rename from src/components/game/timeline/line-item.tsx rename to src/components/timeline/line-item.tsx index e1178bc..333c9d3 100644 --- a/src/components/game/timeline/line-item.tsx +++ b/src/components/timeline/line-item.tsx @@ -2,13 +2,14 @@ import c from "classnames"; import "./line-item.scss"; -interface ActionLineItem { +type Props = { card: ActionCard; -} + commited: boolean; +}; -export function ActionLineItem({ card }: ActionLineItem): JSX.Element { +export function ActionLineItem({ card, commited }: Props): JSX.Element { return ( -
+
diff --git a/src/components/timeline/timeline-controller.tsx b/src/components/timeline/timeline-controller.tsx new file mode 100644 index 0000000..7a3a947 --- /dev/null +++ b/src/components/timeline/timeline-controller.tsx @@ -0,0 +1,8 @@ +import { useGameContext } from "game-context"; +import { Timeline } from "./timeline"; + +export function TimelineController(): JSX.Element { + const gameContext = useGameContext(); + + return ; +} diff --git a/src/components/game/timeline/timeline.scss b/src/components/timeline/timeline.scss similarity index 100% rename from src/components/game/timeline/timeline.scss rename to src/components/timeline/timeline.scss diff --git a/src/components/timeline/timeline.test.tsx b/src/components/timeline/timeline.test.tsx new file mode 100644 index 0000000..25c09c3 --- /dev/null +++ b/src/components/timeline/timeline.test.tsx @@ -0,0 +1,26 @@ +import { render } from "@testing-library/react"; +import { NewCard } from "models/new-card"; +import { Timeline } from "./timeline"; + +const next: TimelineCard[] = [ + { + card: NewCard("move", "enemy1"), + commited: true, + }, + { + card: NewCard("move", "enemy2"), + commited: true, + }, + { + card: NewCard("move", "enemy3"), + commited: true, + }, +]; +const future: TimelineCard[] = []; + +describe("", () => { + test("render: match snapshot", () => { + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/src/components/game/timeline/timeline.tsx b/src/components/timeline/timeline.tsx similarity index 61% rename from src/components/game/timeline/timeline.tsx rename to src/components/timeline/timeline.tsx index 7d0bce3..9fe488f 100644 --- a/src/components/game/timeline/timeline.tsx +++ b/src/components/timeline/timeline.tsx @@ -3,17 +3,20 @@ import { ActionLineItem } from "./line-item"; import "./timeline.scss"; -interface Props { - timeline: Timeline; -} +type Props = { + next: TimelineCard[]; + future: TimelineCard[]; +}; -export function Timeline({ timeline }: Props): JSX.Element { +export function Timeline({ next, future }: Props): JSX.Element { logRender("Timeline"); - const renderLineItems = (section: Card[]) => { - return section.map((card) => { + const renderLineItems = (section: TimelineCard[]) => { + return section.map(({ card, commited }) => { if (card.cardType === "actionCard") - return ; + return ( + + ); }); }; @@ -22,13 +25,13 @@ export function Timeline({ timeline }: Props): JSX.Element {
NEXT
- {renderLineItems(timeline.next)} + {renderLineItems(next)}
FUTURE
- {renderLineItems(timeline.future)} + {renderLineItems(future)}
diff --git a/src/contexts/game-context/use-game-context.tsx b/src/contexts/game-context/use-game-context.tsx deleted file mode 100644 index 925bd36..0000000 --- a/src/contexts/game-context/use-game-context.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import { empireSize } from "game-logic/empire-size"; -import { isConquering, isCreatingGreatesEmpire } from "game-logic/score-check"; -import { createContext, useContext, useState } from "react"; -import { logRender } from "utils/console"; -import { emptyBoard } from "./empty-board"; -import { useBoard } from "./use-board"; -import usePlayers from "./use-players"; -import { emptyTimeline, useTimeline } from "./use-timeline"; - -const GameContext = createContext({ - phase: "setup", - board: emptyBoard, - timeline: emptyTimeline, - activeCard: undefined, - activePlayer: undefined, - players: [], - build: () => {}, - move: () => {}, - recruit: () => {}, - plan: () => {}, - firstPlayer: () => {}, - skip: () => {}, - loadSavegame: () => {}, -}); - -interface Props { - children: React.ReactNode; -} - -function defineActivePlayer( - phase: PhaseType, - timeline: Timeline, - players: PlayerStatus[] -) { - if (phase === "planification") { - // [ . . . . ] playerOrder[0] 0, 4 - // [ . ] playerOrder[1] 1, 5 - // [ . . ] playerOrder[2] 2, 6 - return players[timeline.next.length % 4].player; - } - if (phase === "action") { - return timeline.current?.cardType === "actionCard" - ? timeline.current.owner - : undefined; - } - return undefined; -} - -export function GameContextProvider({ children }: Props): JSX.Element { - logRender("GameContextProvider"); - - const [phase, setPhase] = useState("planification"); // will be setup - const { board, buildOnTile, movePiece, recruitOnTile, _overrideBoard } = - useBoard(); - const { timeline, nextCard, planification, newTurn, _overrideTimeline } = - useTimeline(); - const { - players, - firstPlayer, - scorePoint, - declareGreatestEmpire, - _overridePlayers, - } = usePlayers(); - - /* derived state */ - const activeCard = timeline.current; - const activePlayer = defineActivePlayer(phase, timeline, players); - - /* API */ - const overrideGameContext = ({ - phase, - board, - timeline, - players, - }: GameContext) => { - setPhase(phase); - _overrideBoard(board); - _overrideTimeline(timeline); - _overridePlayers(players); - }; - - const changePhase = () => { - let nextPhase: PhaseType; - switch (phase) { - case "setup": - nextPhase = "planification"; - break; - case "planification": - nextPhase = "action"; - nextCard(); // XXX - break; - case "action": - nextPhase = "planification"; - break; - } - console.info(`GameContext.changePhase() (${phase} -> ${nextPhase})`); - if (nextPhase === "planification") { - newTurn(); - } - setPhase(nextPhase); - }; - - const resolveActionCard = () => { - nextCard(); - if (timeline.next.length === 0) { - console.info( - "GameContext.resolveActionCard(): no more cards to resolve this turn" - ); - changePhase(); - } - }; - - return ( - - {children} - - ); -} - -export function useGameContext() { - return useContext(GameContext); -} diff --git a/src/contexts/game-context/use-timeline.test.tsx b/src/contexts/game-context/use-timeline.test.tsx deleted file mode 100644 index f3a1e10..0000000 --- a/src/contexts/game-context/use-timeline.test.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { fireEvent, render, screen } from "@testing-library/react"; -import { useTimeline } from "./use-timeline"; - -function TestingComponent() { - const { timeline, planification, nextCard, newTurn } = useTimeline(); - - return ( - <> -
- {timeline.current?.cardType === "actionCard" && timeline.current.action} -
- -
- {timeline.next[0]?.cardType === "actionCard" && timeline.next[0].action} -
- -
- {timeline.future[0]?.cardType === "actionCard" && - timeline.future[0].action} -
- - + ); +} diff --git a/src/components/common/dialog.scss b/src/elements/dialog.scss similarity index 100% rename from src/components/common/dialog.scss rename to src/elements/dialog.scss diff --git a/src/components/common/dialog.tsx b/src/elements/dialog.tsx similarity index 100% rename from src/components/common/dialog.tsx rename to src/elements/dialog.tsx diff --git a/src/components/common/index.ts b/src/elements/index.ts similarity index 100% rename from src/components/common/index.ts rename to src/elements/index.ts diff --git a/src/contexts/game-context/empty-board.ts b/src/game-context/empty-board.ts similarity index 100% rename from src/contexts/game-context/empty-board.ts rename to src/game-context/empty-board.ts diff --git a/src/game-context/index.ts b/src/game-context/index.ts new file mode 100644 index 0000000..d5b17a3 --- /dev/null +++ b/src/game-context/index.ts @@ -0,0 +1 @@ +export { useGameContext, GameContextProvider } from "./use-game-context"; diff --git a/src/contexts/game-context/initial-board.ts b/src/game-context/initial-board.ts similarity index 100% rename from src/contexts/game-context/initial-board.ts rename to src/game-context/initial-board.ts diff --git a/src/contexts/game-context/use-board.test.tsx b/src/game-context/use-board.test.tsx similarity index 89% rename from src/contexts/game-context/use-board.test.tsx rename to src/game-context/use-board.test.tsx index 392d55d..74cba57 100644 --- a/src/contexts/game-context/use-board.test.tsx +++ b/src/game-context/use-board.test.tsx @@ -56,32 +56,32 @@ function TestingComponent() { const getTileInfo = (testId: string) => screen.getByTestId(testId).textContent; -describe("game-context", () => { - test("useBoard() returns board{}", () => { +describe("useBoard()", () => { + it("returns board{}", () => { render(); expect(getTileInfo("tile-0,2")).toEqual("soldier (enemy3)"); expect(getTileInfo("tile-1,2")).toEqual(""); expect(getTileInfo("tile-0,3")).toEqual("village (enemy3)"); }); - test("useBoard() returns buildOnTile()", () => { + it("returns buildOnTile()", () => { render(); fireEvent.click(screen.getByTestId("build-on-tile-12")); expect(getTileInfo("tile-1,2")).toEqual("village (enemy3)"); }); - test("useBoard() returns movePiece()", () => { + it("returns movePiece()", () => { render(); fireEvent.click(screen.getByTestId("move-piece-from-02-to12")); expect(getTileInfo("tile-0,2")).toEqual(""); expect(getTileInfo("tile-1,2")).toEqual("soldier (enemy3)"); }); - test("useBoard() returns recruitOnTile()", () => { + it("returns recruitOnTile()", () => { render(); fireEvent.click(screen.getByTestId("recruit-on-tile-03")); expect(getTileInfo("tile-0,3")).toEqual("village (enemy3)soldier (enemy3)"); }); - test.todo("useBoard() returns isConquering()"); + it.todo("returns isConquering()"); }); diff --git a/src/contexts/game-context/use-board.ts b/src/game-context/use-board.ts similarity index 53% rename from src/contexts/game-context/use-board.ts rename to src/game-context/use-board.ts index a61be88..350b278 100644 --- a/src/contexts/game-context/use-board.ts +++ b/src/game-context/use-board.ts @@ -4,37 +4,42 @@ import { initialBoard } from "./initial-board"; /** * plain react state + named update methods * - * **no game logic here** + * - **direct update on the board; no validity check** + * - **no game logic** + * - **no console.log** + * - **no inconsistent state** */ export function useBoard() { const [board, setBoard] = useState(initialBoard); - const buildOnTile = ({ tile, building }: BuildAction) => { - console.info( - `GameContext.buildOnTile({ tile: <${tile}>, building: ${building.type}(${building.owner}) })` - ); + const buildOnTile = ({ + tile, + building, + }: { + tile: TileID; + building: Building; + }) => { setBoard((currentBoard) => { - const newTile: Tile = { - ...currentBoard[tile], - building, - }; return { ...currentBoard, - [tile]: newTile, + [tile]: { + ...currentBoard[tile], + building, + }, }; }); }; - /** direct update on the board; no validity check */ - const movePiece = ({ piece, from, to }: MoveAction) => { - console.info( - `GameContext.movePiece({ from: <${from}>, to: <${to}>, piece: ${piece.type}(${piece.owner}) })` - ); + const movePiece = ({ + piece, + from, + to, + }: { + piece: Piece; + from: TileID; + to: TileID; + }) => { setBoard((currentBoard) => { - const originTile: Tile = { - ...currentBoard[from], - piece: undefined, - }; const targetTile: Tile = { ...currentBoard[to], piece, @@ -47,16 +52,16 @@ export function useBoard() { } return { ...currentBoard, - [from]: originTile, + [from]: { + ...currentBoard[from], + piece: undefined, + }, [to]: targetTile, }; }); }; - const recruitOnTile = ({ tile, piece }: RecruitAction) => { - console.info( - `GameContext.recruitOnTile({ tile: <${tile}>, piece: ${piece.type}(${piece.owner}) })` - ); + const recruitOnTile = ({ tile, piece }: { tile: TileID; piece: Piece }) => { setBoard((currentBoard) => { const newTile: Tile = { ...currentBoard[tile], diff --git a/src/game-context/use-game-context.tsx b/src/game-context/use-game-context.tsx new file mode 100644 index 0000000..c54574e --- /dev/null +++ b/src/game-context/use-game-context.tsx @@ -0,0 +1,259 @@ +import { empireSize } from "game-logic/empire-size"; +import { isConquering, isCreatingGreatesEmpire } from "game-logic/score-check"; +import { createContext, useContext } from "react"; +import { logRender, warnInconsistentState } from "utils/console"; +import { emptyBoard } from "./empty-board"; +import { useBoard } from "./use-board"; +import { usePlayers } from "./use-players"; +import { useTimeline } from "./use-timeline"; + +const GameContext = createContext({ + phase: "setup", + activeCard: undefined, + activePlayer: undefined, + board: emptyBoard, + next: [], + future: [], + players: [], + build: () => {}, + move: () => {}, + recruit: () => {}, + skip: () => {}, + plan: () => {}, + submitPlanification: () => {}, + firstPlayer: () => {}, + loadSavegame: () => {}, +}); + +type Props = { children: React.ReactNode }; + +/** + * GameController + * + * game state + * + * ``` + * { useTimeline(), useBoard(), usePlayers() } + * ``` + */ +export function GameContextProvider({ children }: Props) { + logRender("GameContextProvider"); + + const { board, buildOnTile, movePiece, recruitOnTile, _overrideBoard } = + useBoard(); + + const timeline = useTimeline(); + + const { + players, + nextFirstPlayer, + scorePoint, + declareGreatestEmpire, + _overridePlayers, + } = usePlayers(); + + /* derived state */ + const activePlayer = defineActivePlayer(timeline, { players }); + + /* API */ + + const loadSavegame = ({ + phase, + activeCard, + next, + future, + board, + players, + }: { + phase: PhaseType; + activeCard: Card | undefined; + next: TimelineCard[]; + future: TimelineCard[]; + board: Board; + players: PlayerStatus[]; + }) => { + _overrideBoard(board); + timeline._overrideTimeline({ phase, activeCard, next, future }); + _overridePlayers(players); + }; + + const _resolveActionCard = () => { + if (timeline.next.length === 0) { + console.info("resolveActionCard(): no more cards, changing phase"); + timeline.startPlanningPhase(); + } else { + console.info("resolveActionCard(): next card"); + timeline.nextActiveCard(); + } + }; + + const build = (action: { tile: TileID; building: Building }) => { + if (timeline.phase !== "action") { + return warnInconsistentState( + `trying to build but not in "action" phase`, + { phase: timeline.phase, action } + ); + } + console.info( + `buildOnTile({ tile: <${action.tile}>, building: ${action.building} })` + ); + if (isCreatingGreatesEmpire({ ...action, empires: empireSize(board) })) { + declareGreatestEmpire(action.building.owner); + } + buildOnTile(action); + _resolveActionCard(); + }; + + const move = (action: { piece: Piece; from: TileID; to: TileID }) => { + if (timeline.phase !== "action") { + return warnInconsistentState(`trying to move but not in "action" phase`, { + phase: timeline.phase, + action, + }); + } + console.info( + `movePiece({ from: <${action.from}>, to: <${action.to}>, piece: ${action.piece} })` + ); + const player = action.piece.owner; + if (isConquering({ player, targetTile: board[action.to] })) { + scorePoint(player); + } + movePiece(action); + _resolveActionCard(); + }; + + const recruit = (action: { tile: TileID; piece: Piece }) => { + if (timeline.phase !== "action") { + return warnInconsistentState( + `trying to recruit but not in "action" phase`, + { phase: timeline.phase, action } + ); + } + console.info( + `recruitOnTile({ tile: <${action.tile}>, piece: ${action.piece} })` + ); + recruitOnTile(action); + _resolveActionCard(); + }; + + const firstPlayer = () => { + nextFirstPlayer(); + _resolveActionCard(); + }; + + const plan = (action: { + nextActionCard: ActionCard | undefined; + futureActionCard: ActionCard | undefined; + }) => { + console.info( + `plan({ next: ${action.nextActionCard}, future: ${action.futureActionCard} })` + ); + if (timeline.phase !== "planification") { + return warnInconsistentState( + `trying to plan action but phase is not "planification"`, + { phase: timeline.phase, action } + ); + } + timeline.planAction(action); + }; + + const submitPlanification = () => { + const { phase, activeCard, next, future } = timeline; + if (phase !== "planification") { + return warnInconsistentState( + `trying to submit planification but phase is not "planification"`, + { phase, activeCard, next, future } + ); + } + console.info("submitPlanification()"); + + const pendingNextActions = next.filter((card) => !card.commited); + if (pendingNextActions.length !== 1) { + return warnInconsistentState( + `trying to submit planification but there are ${pendingNextActions.length} pending actions`, + { phase, activeCard, next, future } + ); + } + + const pendingFutureActions = future.filter((card) => !card.commited); + if (pendingFutureActions.length !== 1) { + return warnInconsistentState( + `trying to submit planification but there are ${pendingFutureActions.length} pending actions`, + { phase, activeCard, next, future } + ); + } + + timeline.submitPlanification(); + if (next[0] && next.length % 4 === 0) { + timeline.startActionPhase(); + } + }; + + const skip = () => { + const { phase, activeCard } = timeline; + if ( + activeCard?.cardType !== "actionCard" || + activeCard?.owner !== activePlayer + ) { + return warnInconsistentState( + `trying to skip action but active card is not an action card`, + { phase, activeCard } + ); + } + _resolveActionCard(); + }; + + return ( + + {children} + + ); +} + +export function useGameContext() { + return useContext(GameContext); +} + +// TODO move to state +function defineActivePlayer( + { + activeCard, + phase, + next, + }: { activeCard: Card | undefined; phase: PhaseType; next: TimelineCard[] }, + { players }: { players: PlayerStatus[] } +) { + if (phase === "planification") { + // [ . . . . ] playerOrder[0] 0, 4 + // [ . ] playerOrder[1] 1, 5 + // [ . . ] playerOrder[2] 2, 6 + const committedActions = next.filter( + (timelineCard) => timelineCard.commited + ); + return players[committedActions.length % 4].player; + } + if (phase === "action") { + if (activeCard && activeCard.cardType === "actionCard") { + return activeCard.owner; + } + } + return undefined; +} diff --git a/src/contexts/game-context/use-players.test.tsx b/src/game-context/use-players.test.tsx similarity index 78% rename from src/contexts/game-context/use-players.test.tsx rename to src/game-context/use-players.test.tsx index a2a616f..fda3d09 100644 --- a/src/contexts/game-context/use-players.test.tsx +++ b/src/game-context/use-players.test.tsx @@ -1,8 +1,10 @@ import { render, screen, fireEvent } from "@testing-library/react"; -import usePlayers from "./use-players"; +import { usePlayers } from "./use-players"; + +// TODO change to renderHook(() => usePlayers()); function TestingComponent() { - const { players, firstPlayer, scorePoint, declareGreatestEmpire } = + const { players, nextFirstPlayer, scorePoint, declareGreatestEmpire } = usePlayers(); return ( <> @@ -18,12 +20,8 @@ function TestingComponent() { onClick={() => scorePoint("enemy1")} />