summaryrefslogtreecommitdiff
path: root/client/src
diff options
context:
space:
mode:
authorMike Vink <mike1994vink@gmail.com>2021-06-25 12:56:04 +0200
committerMike Vink <mike1994vink@gmail.com>2021-06-25 12:56:04 +0200
commit6bd8d0345e3ac653c3fad4f1c7a6352e8a4a166e (patch)
treef142d3f43add40c2dd56bc496d6b4ab497f31a3e /client/src
parent38d59e2876b9f4d7c589d58295ef8acdf336a45b (diff)
parent102b25f18d9b269c58d15677f10cd71c15003c4b (diff)
Merge branch 'mvcFeature' into mainline
Diffstat (limited to 'client/src')
-rw-r--r--client/src/About/About.tsx12
-rw-r--r--client/src/App.css28
-rw-r--r--client/src/App.tsx29
-rw-r--r--client/src/Header/Header.css30
-rw-r--r--client/src/Header/Header.tsx20
-rw-r--r--client/src/Header/logo.jpgbin0 -> 5184 bytes
-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
-rw-r--r--client/src/gameState.ts27
-rw-r--r--client/src/index.tsx15
-rw-r--r--client/src/pitState.ts0
-rw-r--r--client/src/pitState.tsx0
18 files changed, 654 insertions, 0 deletions
diff --git a/client/src/About/About.tsx b/client/src/About/About.tsx
new file mode 100644
index 0000000..e7a9c69
--- /dev/null
+++ b/client/src/About/About.tsx
@@ -0,0 +1,12 @@
+import React from "react";
+
+export function About() {
+ return <div>
+ <h2>Mancala</h2>
+ <p>
+ Mancala is one of the oldest known board games. It has many different variations and is known under several different names.
+ The objective of the game is obtaining as many stones, beads or seeds as possible.
+ It is a game of skill that does not rely on any sort of randomness, making it similar to chess.
+ </p>
+ </div>
+} \ No newline at end of file
diff --git a/client/src/App.css b/client/src/App.css
new file mode 100644
index 0000000..1d59297
--- /dev/null
+++ b/client/src/App.css
@@ -0,0 +1,28 @@
+html {
+ height: 100%;
+}
+
+body {
+ margin: 0px;
+ padding: 0px;
+ background-color: #eeeeee;
+ height: 100%;
+}
+
+#root {
+ height: 100%;
+ --board-color: rgb(222,184,135);
+ --recess-color: rgb(188,143,143);
+}
+
+.main-content {
+ display: flex;
+
+ justify-content: center;
+ align-items: center;
+ align-self: center;
+
+ flex: 0 1 auto;
+
+ height: calc(100% - (107px))
+}
diff --git a/client/src/App.tsx b/client/src/App.tsx
new file mode 100644
index 0000000..edd9504
--- /dev/null
+++ b/client/src/App.tsx
@@ -0,0 +1,29 @@
+import React from "react";
+import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
+import { Header } from "./Header/Header";
+import { About } from "./About/About";
+import { Mancala } from "./Mancala/Mancala";
+import "./App.css";
+
+export function App() {
+ return (
+ <Router>
+ {/* The header with navigation options is always on top of every page */}
+ <Header />
+
+ <div className="main-content">
+ <Switch>
+ {/* If the user goes to the url /about, show the about page */}
+ <Route path="/about">
+ <About />
+ </Route>
+
+ {/* If the user goes to any other url, show the play page */}
+ <Route path="/">
+ <Mancala />
+ </Route>
+ </Switch>
+ </div>
+ </Router>
+ )
+} \ No newline at end of file
diff --git a/client/src/Header/Header.css b/client/src/Header/Header.css
new file mode 100644
index 0000000..6744f32
--- /dev/null
+++ b/client/src/Header/Header.css
@@ -0,0 +1,30 @@
+.main-header {
+ background-color: white;
+ color: green;
+ font-size: 2rem;
+ margin-bottom: 8px;
+}
+
+.main-navigation {
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+ background-color: green;
+ font-size: 1.3rem;
+ height: 45px;
+}
+
+.main-navigation a {
+ color: white;
+ text-decoration: none;
+}
+
+.main-header div {
+ display: flex;
+ align-items: center;
+}
+
+.main-header img {
+ height: 50px;
+ margin: 6px;
+} \ No newline at end of file
diff --git a/client/src/Header/Header.tsx b/client/src/Header/Header.tsx
new file mode 100644
index 0000000..9f96d0c
--- /dev/null
+++ b/client/src/Header/Header.tsx
@@ -0,0 +1,20 @@
+import React from "react";
+import { Link } from "react-router-dom";
+import "./Header.css";
+import urlLogo from "./logo.jpg";
+
+/**
+ * A Header component with a Sogyo logo, the name of the application and several links to different pages
+ */
+export function Header() {
+ return <header className="main-header">
+ <div>
+ <img src={urlLogo} />
+ Mancala
+ </div>
+ <div className="main-navigation">
+ <Link to="/">Play</Link>
+ <Link to="/about">About</Link>
+ </div>
+ </header>
+} \ No newline at end of file
diff --git a/client/src/Header/logo.jpg b/client/src/Header/logo.jpg
new file mode 100644
index 0000000..9104922
--- /dev/null
+++ b/client/src/Header/logo.jpg
Binary files differ
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>
+ )
+}
diff --git a/client/src/gameState.ts b/client/src/gameState.ts
new file mode 100644
index 0000000..b276136
--- /dev/null
+++ b/client/src/gameState.ts
@@ -0,0 +1,27 @@
+
+export interface GameState {
+ players: [ Player, Player ]; // a player array contains exactly two Players
+ gameStatus: {
+ endOfGame: boolean;
+ };
+}
+
+interface Player {
+ name: string;
+ pits: Pit[];
+ type: "player1" | "player2"; // only "player1" and "player2" are valid options for this string
+ hasTurn: boolean;
+}
+
+interface Pit {
+ index: number;
+ nrOfStones: number;
+ state: PitState;
+ setPitState: (newPitState: PitState) => void;
+}
+
+
+interface PitState {
+ stoneElements: JSX.Element[] | undefined;
+ }
+
diff --git a/client/src/index.tsx b/client/src/index.tsx
new file mode 100644
index 0000000..c92ef7e
--- /dev/null
+++ b/client/src/index.tsx
@@ -0,0 +1,15 @@
+import * as React from "react";
+import ReactDOM from "react-dom";
+import { App } from "./App";
+
+ReactDOM.render(
+ <App/>,
+ document.getElementById("root")
+)
+
+// Hot Module Replacement (HMR) - Remove this snippet to remove HMR.
+// Learn more: https://www.snowpack.dev/#hot-module-replacement
+const hotMudleReplacement = import.meta.hot;
+if (hotMudleReplacement) {
+ hotMudleReplacement.accept();
+}
diff --git a/client/src/pitState.ts b/client/src/pitState.ts
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/client/src/pitState.ts
diff --git a/client/src/pitState.tsx b/client/src/pitState.tsx
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/client/src/pitState.tsx