mancala/apps/frontend/src/components/board/BoardView.tsx
Halit Aksoy e3e7916265 feat(frontend): draw the new board
Pits and stores are wells with scattered stones and count badges, the
viewer's store on the right. Below 600 px the board stands upright.
Playable pits preview their move on hover, animated pits are ringed and
the pit of the last move has a dashed ring.
2026-10-16 09:37:50 +03:00

91 lines
3.9 KiB
TypeScript

import * as React from "react";
import { FunctionComponent, useState } from "react";
import { useTranslation } from "react-i18next";
import { BoardViewModel, Game, getSowingPath } from "@mancala/core";
import { Pit } from "mancala.js";
import { Context } from "../../context/context";
import { getWebTheme } from "../../theme/themes";
import PitView, { PitMarks } from "./PitView";
import StoreView from "./StoreView";
import "./BoardView.css";
// The board as the bottom player sees it: their pits along the bottom from left to right, their store on the right.
// It fills its container's width.
const BoardView: FunctionComponent<{
game: Game;
context: Context;
boardViewModel: BoardViewModel;
// Show player 2 at the bottom.
revert: boolean;
topName: string;
bottomName: string;
// Makes the bottom player's pits buttons.
onPitSelect?: (index: number, pit: Pit) => void;
// The bottom player can move now: their pits with stones are highlighted and preview their move.
canPlay?: boolean;
// Board index of the pit the last move was played from.
lastMoveIndex?: number;
// Board index where the shown move's last stone landed.
landIndex?: number;
}> = ({ game, context, boardViewModel, revert, topName, bottomName, onPitSelect, canPlay, lastMoveIndex, landIndex }) => {
const { t } = useTranslation();
const [previewIndex, setPreviewIndex] = useState<number | undefined>(undefined);
const board = game.mancalaGame.board;
const themeId = getWebTheme(context.themeManager.theme).id;
const stones = boardViewModel.pits.map((pit) => pit.stoneCount);
// Pits the hovered move drops stones into, with how many each, and where its last stone lands.
const preview: Record<number, number> = {};
const path = canPlay && previewIndex !== undefined ? getSowingPath(stones, previewIndex) : [];
path.forEach((index) => (preview[index] = (preview[index] ?? 0) + 1));
const previewLand = path.length > 0 ? path[path.length - 1] : undefined;
const marksOf = (index: number): PitMarks => ({
highlight: boardViewModel.pits[index]?.highlight,
preview: preview[index],
land: previewLand !== undefined ? index === previewLand : index === landIndex,
last: index === lastMoveIndex,
});
const topPits = revert ? board.player1Pits : board.player2Pits;
const bottomPits = revert ? board.player2Pits : board.player1Pits;
const [topStore, bottomStore] = revert
? [board.player1BankIndex(), board.player2BankIndex()]
: [board.player2BankIndex(), board.player1BankIndex()];
const pitView = (pit: Pit, position: number, top: boolean) => {
const stoneCount = stones[pit.index];
const playable = !top && !!canPlay && stoneCount > 0;
const pitNumber = (top ? 5 - position : position) + 1;
return (
<PitView
key={pit.index}
index={pit.index}
stoneCount={stoneCount}
top={top}
position={position}
themeId={themeId}
label={t("PitLabel", { name: top ? topName : bottomName, pit: pitNumber, count: stoneCount })}
marks={marksOf(pit.index)}
playable={playable}
onClick={!top && onPitSelect ? () => onPitSelect(pit.index, board.pits[pit.index]) : undefined}
onPreview={(active) => setPreviewIndex(active ? pit.index : undefined)} />
);
};
return (
<div className="bwrap">
<div className="board">
<StoreView index={topStore} stoneCount={stones[topStore]} side="l" themeId={themeId}
label={t("StoreLabel", { name: topName, count: stones[topStore] })} marks={marksOf(topStore)} />
{[...topPits].reverse().map((pit, position) => pitView(pit, position, true))}
{bottomPits.map((pit, position) => pitView(pit, position, false))}
<StoreView index={bottomStore} stoneCount={stones[bottomStore]} side="r" themeId={themeId}
label={t("StoreLabel", { name: bottomName, count: stones[bottomStore] })} marks={marksOf(bottomStore)} />
</div>
</div>
);
};
export default BoardView;