summaryrefslogtreecommitdiff
path: root/client/src/Mancala
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/Mancala')
-rw-r--r--client/src/Mancala/Mancala.css1
-rw-r--r--client/src/Mancala/Mancala.tsx35
-rw-r--r--client/src/Mancala/Pit.css46
-rw-r--r--client/src/Mancala/Pit.tsx65
-rw-r--r--client/src/Mancala/Play.css44
-rw-r--r--client/src/Mancala/Play.tsx216
-rw-r--r--client/src/Mancala/StartGame.css10
-rw-r--r--client/src/Mancala/StartGame.tsx76
8 files changed, 493 insertions, 0 deletions
diff --git a/client/src/Mancala/Mancala.css b/client/src/Mancala/Mancala.css
new file mode 100644
index 0000000..8f73945
--- /dev/null
+++ b/client/src/Mancala/Mancala.css
@@ -0,0 +1 @@
+/** Add some styles */ \ No newline at end of file
diff --git a/client/src/Mancala/Mancala.tsx b/client/src/Mancala/Mancala.tsx
new file mode 100644
index 0000000..509410b
--- /dev/null
+++ b/client/src/Mancala/Mancala.tsx
@@ -0,0 +1,35 @@
+import React, { useState } from "react";
+import { StartGame } from "./StartGame";
+import { Play } from "./Play";
+import type { GameState } from "../gameState";
+import "./Mancala.css";
+
+/**
+ * The base component for the Mancala game. If there's no active game, the `StartGame` component allows
+ * users to enter two player names and start a new game.
+ * If there's an active game this component holds the game state. This game state can be passed as a prop
+ * to child components as needed.
+ *
+ * Child components can modify the game state by calling the setGameState (which they recieve as prop.)
+ */
+export function Mancala() {
+
+ // useState is a so called React hook.
+ // It is used to manage variables. When the setGameState function is called, React updates the UI as needed
+ // The call to useState follows the "rules of hooks": https://reactjs.org/docs/hooks-rules.html
+ // To check if code you added also follows the rules of hooks, run "npm run lint" in the command line
+ const [ gameState, setGameState ] = useState<GameState | undefined>(undefined);
+
+ if (localStorage.getItem("state") !== null) {
+ var state = localStorage.getItem("state");
+ const gameState = JSON.parse(state as string);
+ return <Play gameState={gameState} setGameState={setGameState} />
+ }
+
+ if (!gameState) {
+ return <StartGame gameState={gameState} setGameState={setGameState} />
+ }
+
+
+ return <Play gameState={gameState} setGameState={setGameState} />
+}
diff --git a/client/src/Mancala/Pit.css b/client/src/Mancala/Pit.css
new file mode 100644
index 0000000..8cd3f63
--- /dev/null
+++ b/client/src/Mancala/Pit.css
@@ -0,0 +1,46 @@
+.Pit {
+ display: inline-flex;
+
+ justify-content: center;
+ align-items: center;
+
+ width: 16%;
+}
+
+.Recession {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background: var(--recess-color);
+}
+
+.Pit .Recession {
+ border-radius: 50%;
+ height: 50%;
+ width: 100%;
+}
+
+.Kalaha {
+ display: inline-flex;
+
+ justify-content: center;
+ align-items: center;
+
+ width: 10%;
+}
+
+.Kalaha .Recession {
+ border-radius: 30%;
+ height: 50%;
+ width: 100%;
+}
+
+.Stone {
+ position: absolute;
+ background: gray;
+ border-radius: 50%;
+ border: 2px solid black;
+ height: 16px;
+ width: 16px;
+ min-height: 5px;
+}
diff --git a/client/src/Mancala/Pit.tsx b/client/src/Mancala/Pit.tsx
new file mode 100644
index 0000000..300f3f4
--- /dev/null
+++ b/client/src/Mancala/Pit.tsx
@@ -0,0 +1,65 @@
+import React, { useState } from "react";
+import type { GameState } from "../gameState";
+import "./Pit.css";
+
+type Flatten<T> = T extends any[] ? T[number] : T;
+
+type Player = Flatten<GameState["players"]>;
+
+type Pit = Flatten<Player["pits"]>;
+
+type PitProps = {
+ player: Player;
+ index: number;
+ pit: Pit;
+ onClick?: (index: number, player: Player, pit: Pit) => (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => Promise<void>
+ displayStones: (pit: Pit, spread: "small" | "big") => JSX.Element[]
+}
+
+type SmallBowl = Pit & {kind: "small"};
+type Kalaha = Pit & {kind: "big"};
+
+
+
+export function Pit({player, index, pit, onClick, displayStones}: PitProps) {
+ const smallBowl = pit as SmallBowl;
+ smallBowl.kind = "small";
+ if (onClick) {
+ return (
+ <div className="Pit">
+ {pit.nrOfStones > 10 ? "" + pit.nrOfStones : ""}
+ <div className="Recession" onClick={onClick(index, player, pit)}>
+ <div className="stones" id={"pit" + pit.index}>
+ {displayStones(pit, smallBowl.kind)}
+ </div>
+ </div>
+ </div>
+ );
+ } else {
+ return (
+ <div className="Pit">
+ <div className="Recession">
+ <div className="stones">
+ {pit.nrOfStones > 10 ? pit.nrOfStones : ""}
+ {displayStones(pit, smallBowl.kind)}
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+export function Kalaha({player, pit, displayStones}: PitProps) {
+ const kalaha = pit as Kalaha;
+ kalaha.kind = "big";
+ return (
+ <div className="Kalaha">
+ {pit.nrOfStones > 10 ? pit.nrOfStones : ""}
+ <div className="Recession">
+ <div className="stones" id={"pit" + pit.index}>
+ {displayStones(pit, kalaha.kind)}
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/client/src/Mancala/Play.css b/client/src/Mancala/Play.css
new file mode 100644
index 0000000..6aa0d8f
--- /dev/null
+++ b/client/src/Mancala/Play.css
@@ -0,0 +1,44 @@
+/** Add some styles */
+#board {
+ display: flex;
+ background: var(--board-color);
+ padding: 20px;
+
+ border-radius: 10%;
+
+ justify-content: center;
+ flex-flow: row;
+ flex-direction: row;
+
+ width: 800px;
+ height: 400px;
+}
+
+#pits {
+ position: relative;
+ display: inline-flex;
+
+
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: center;
+
+ width: 80%;
+}
+
+.playArea {
+ position: relative;
+}
+
+.playerStatus {
+ position: relative;
+ display: flex;
+
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: center;
+ align-items: center;
+
+ height: 100px;
+
+}
diff --git a/client/src/Mancala/Play.tsx b/client/src/Mancala/Play.tsx
new file mode 100644
index 0000000..d2d7f65
--- /dev/null
+++ b/client/src/Mancala/Play.tsx
@@ -0,0 +1,216 @@
+import React, { useState } from "react";
+import type { GameState } from "../gameState";
+import "./Play.css";
+import { Pit, Kalaha } from "./Pit"
+
+type Flatten<T> = T extends any[] ? T[number] : T;
+
+type Player = Flatten<GameState["players"]>;
+
+type Pit = Flatten<Player["pits"]>;
+
+type PlayProps = {
+ gameState: GameState;
+ setGameState(newGameState: GameState): void;
+}
+
+function xyTranslate(pit: Pit, index: number) {
+ var angle = index/pit.nrOfStones * 2 * Math.PI;
+ var d = (index % 2 + 1) * 13;
+ var ofX = -9;
+ var ofY = -9;
+ if (pit.index === 6 || pit.index === 13) {
+ var xrandom = Math.random();
+ var yrandom = Math.random();
+ return 'translate('+ ((xrandom > 0.5 ? Math.random() : -Math.random()) * 18 - 10) + 'px, ' + (yrandom > 0.5 ? Math.random() : -Math.random()) * 60 + 'px)';
+ } else {
+ return 'translate('+ (Math.cos(angle) * (d) + ofX) + 'px, ' + (Math.sin(angle) * (d) + ofY) + 'px)';
+ }
+}
+
+export function Play({ gameState, setGameState }: PlayProps) {
+
+ function playPit(index: number, player: Player, pit: Pit) {
+ const pitTotal = gameState.players[0].pits.length;
+ const allPits = gameState.players[0].pits.concat(gameState.players[1].pits);
+ const stonesToPass = pit.state.stoneElements as JSX.Element[];
+ return async function event(event: React.MouseEvent<HTMLDivElement>) {
+ if (!player.hasTurn) return;
+ console.log("updating server");
+ console.log(pit.nrOfStones);
+ try {
+ const response = await fetch('mancala/api/play', {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({player: player.type === "player1" ? 0 : 1, index: index})
+ });
+
+ if (response.ok) {
+ const newGameState = await response.json();
+ localStorage.setItem("state", JSON.stringify(newGameState));
+ setGameState({gameState, ...newGameState});
+ } else {
+ console.error(response.statusText);
+ }
+ } catch (error) {
+ console.error(error.toString());
+ }
+ console.log(pit.nrOfStones);
+
+ }
+ }
+
+ // function animate(newGameState: GameState) {
+ // var pits = newGameState.players[0].pits.concat(newGameState.players[1].pits);
+ // var type;
+ // for (var pit of pits) {
+ // type = pit.index === 6 || pit.index === 13 ? "big" : "small" as "big" | "small";
+ // displayStones(pit, type);
+ // setTimeout(() => {}, 1000);
+ // }
+ // };
+
+ function stoneStyler(pit: Pit, index: number, spread: "small" | "big", ) {
+ const colors = ['gray', 'purple', 'blue', 'green'];
+ var stoneStyle;
+ if (spread === "small") {
+ stoneStyle = {
+ transform: xyTranslate(pit, index),
+ background: colors[index % colors.length],
+ };
+ } else {
+ stoneStyle = {
+ transform: xyTranslate(pit, index),
+ background: colors[index % colors.length],
+ };
+ }
+ return stoneStyle;
+ }
+
+ function displayStones(pit: Pit, spread: "small" | "big") {
+ var jsx;
+ var kind = pit.index === 6 || pit.index === 13 ? "big" : "small" as "small"|"big";
+ if (pit.state.stoneElements === undefined) {
+ pit.state.stoneElements = [];
+ for (let i = 0; i < pit.nrOfStones; i++) {
+ jsx = ( <div style={stoneStyler(pit, i, kind)} key={i * (pit.index + 1)} className="Stone"/> );
+ pit.state.stoneElements.reverse().push(jsx);
+ }
+ } else if (pit.nrOfStones < pit.state.stoneElements.length) {
+ // while (pit.nrOfStones < pit.state.stoneElements.length) {
+ // pit.state.stoneElements.pop();
+ // }
+ pit.state.stoneElements = [];
+ for (let i = 0; i < pit.nrOfStones; i++) {
+ jsx = ( <div style={stoneStyler(pit, i, kind)} key={i * (pit.index + 1)} className="Stone"/> );
+ pit.state.stoneElements.reverse().push(jsx);
+ }
+ } else if (pit.nrOfStones > pit.state.stoneElements.length) {
+ pit.state.stoneElements = [];
+ for (let i = 0; i < pit.nrOfStones; i++) {
+ jsx = ( <div style={stoneStyler(pit, i, kind)} key={i * (pit.index + 1)} className="Stone"/> );
+ pit.state.stoneElements.reverse().push(jsx);
+ }
+ }
+ return pit.state.stoneElements;
+ }
+
+ const playerPits = gameState.players.map(
+ player => {
+ return player.pits.slice(0, -1).map(
+ (pit, index) => {
+ const [ pitState, setPitState ] = useState<Pit["state"]>({stoneElements: undefined});
+ pit.state = pitState;
+ pit.setPitState = setPitState;
+ return (
+ <Pit
+ player={player}
+ index={player.type === "player1" ? index : player.pits.length + index}
+ key={player.type === "player1" ? index : player.pits.length + index}
+ pit={pit}
+ onClick={playPit}
+ displayStones={displayStones}
+ />
+ )
+ });
+ });
+
+ const playerKalahas = gameState.players.map(
+ (player, index) => {
+ const [ kalahaState, setKalahaState ] = useState<Pit["state"]>({stoneElements: undefined});
+ const pit = player.pits[player.pits.length - 1];
+ pit.state = kalahaState;
+ pit.setPitState = setKalahaState;
+ return (
+ <Kalaha
+ player={player}
+ index={player.type === "player1" ? player.pits.length - 1 : 2 * (player.pits.length) - 1}
+ key={index}
+ pit={pit}
+ displayStones={displayStones}
+ />
+ )
+ }
+ );
+
+ var player2Pits = [];
+ var player1Pits = [];
+
+ for (var i = 0; i < playerPits[0].length; i++) {
+ player1Pits.push(playerPits[0][i]);
+ player2Pits.push(playerPits[1][i]);
+ }
+
+ const revenge = (
+ <button onClick={async () => {
+ localStorage.clear();
+ try {
+ const response = await fetch('mancala/api/start', {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ nameplayer1: gameState.players[0].name, nameplayer2: gameState.players[1].name })
+ });
+
+ if (response.ok) {
+ const gameState = await response.json();
+ setGameState(gameState);
+ } else {
+ console.error(response.statusText);
+ }
+ } catch (error) {
+ console.error(error.toString());
+ }
+
+ }}>
+ game has ended... play again?
+ </button>
+ );
+
+ return (
+ <div className="playArea">
+ <div className="playerStatus" id="statusPlayer1">
+ {gameState.players[1].name} has {gameState.players[1].pits.reduce((sum, current) => {return (sum + current.nrOfStones)}, 0)} stones.
+ {gameState.players[1].hasTurn ? " Also has the current turn" : ""}
+ </div>
+ <div id="board">
+ {playerKalahas[1]}
+ <div id="pits">
+ {player2Pits.reverse()}
+ {player1Pits}
+ </div>
+ {playerKalahas[0]}
+ </div>
+ <div className="playerStatus" id="statusPlayer1">
+ {gameState.players[0].name} has {gameState.players[0].pits.reduce((sum, current) => {return (sum + current.nrOfStones)}, 0)} stones.
+ {gameState.players[0].hasTurn ? "Also has the current turn" : ""}
+ </div>
+ {gameState.gameStatus.endOfGame ? revenge : ""}
+ </div>
+ )
+}
diff --git a/client/src/Mancala/StartGame.css b/client/src/Mancala/StartGame.css
new file mode 100644
index 0000000..3ab5872
--- /dev/null
+++ b/client/src/Mancala/StartGame.css
@@ -0,0 +1,10 @@
+.startGameButton {
+ font-size: 2em;
+ background-color: lightgreen;
+ border: 2px solid black;
+}
+
+.errorMessage {
+ height: 1em;
+ color: red;
+} \ No newline at end of file
diff --git a/client/src/Mancala/StartGame.tsx b/client/src/Mancala/StartGame.tsx
new file mode 100644
index 0000000..461148e
--- /dev/null
+++ b/client/src/Mancala/StartGame.tsx
@@ -0,0 +1,76 @@
+import React, { useState } from "react";
+import type { GameState } from "../gameState";
+import "./StartGame.css";
+import { Play } from "./Play"
+
+type StartGameProps = {
+ gameState: GameState | undefined;
+ setGameState(newGameState: GameState): void;
+}
+
+/**
+ * Allows the players to enter their name. A name is required for both players. They can't have the same names.
+ */
+export function StartGame({gameState, setGameState }: StartGameProps) {
+
+ const [errorMessage, setErrorMessage] = useState("");
+ const [playerOne, setPlayerOne] = useState("");
+ const [playerTwo, setPlayerTwo] = useState("");
+
+ async function tryStartGame(e: React.FormEvent) {
+ e.preventDefault(); // Prevent default browser behavior of submitting forms
+ if (!playerOne) {
+ setErrorMessage("A name is required for player 1");
+ return;
+ }
+ if (!playerTwo) {
+ setErrorMessage("A name is required for player 2");
+ return;
+ }
+ if (playerOne === playerTwo) {
+ setErrorMessage("Each player should have a unique name");
+ return;
+ }
+ setErrorMessage("");
+
+ try {
+ const response = await fetch('mancala/api/start', {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ nameplayer1: playerOne, nameplayer2: playerTwo })
+ });
+
+ if (response.ok) {
+ const gameState = await response.json();
+ setGameState(gameState);
+ } else {
+ console.error(response.statusText);
+ }
+ } catch (error) {
+ console.error(error.toString());
+ }
+ }
+
+ return (
+ <form onSubmit={(e) => tryStartGame(e)}>
+ <input value={playerOne}
+ placeholder="Player 1 name"
+ onChange={(e) => setPlayerOne(e.target.value)}
+ />
+
+ <input value={playerTwo}
+ placeholder="Player 2 name"
+ onChange={(e) => setPlayerTwo(e.target.value)}
+ />
+
+ <p className="errorMessage">{errorMessage}</p>
+
+ <button className="startGameButton" type="submit">
+ Play Mancala!
+ </button>
+ </form>
+ )
+}