diff options
| author | Mike Vink <mike1994vink@gmail.com> | 2021-06-25 12:56:04 +0200 |
|---|---|---|
| committer | Mike Vink <mike1994vink@gmail.com> | 2021-06-25 12:56:04 +0200 |
| commit | 6bd8d0345e3ac653c3fad4f1c7a6352e8a4a166e (patch) | |
| tree | f142d3f43add40c2dd56bc496d6b4ab497f31a3e /client/src | |
| parent | 38d59e2876b9f4d7c589d58295ef8acdf336a45b (diff) | |
| parent | 102b25f18d9b269c58d15677f10cd71c15003c4b (diff) | |
Merge branch 'mvcFeature' into mainline
Diffstat (limited to 'client/src')
| -rw-r--r-- | client/src/About/About.tsx | 12 | ||||
| -rw-r--r-- | client/src/App.css | 28 | ||||
| -rw-r--r-- | client/src/App.tsx | 29 | ||||
| -rw-r--r-- | client/src/Header/Header.css | 30 | ||||
| -rw-r--r-- | client/src/Header/Header.tsx | 20 | ||||
| -rw-r--r-- | client/src/Header/logo.jpg | bin | 0 -> 5184 bytes | |||
| -rw-r--r-- | client/src/Mancala/Mancala.css | 1 | ||||
| -rw-r--r-- | client/src/Mancala/Mancala.tsx | 35 | ||||
| -rw-r--r-- | client/src/Mancala/Pit.css | 46 | ||||
| -rw-r--r-- | client/src/Mancala/Pit.tsx | 65 | ||||
| -rw-r--r-- | client/src/Mancala/Play.css | 44 | ||||
| -rw-r--r-- | client/src/Mancala/Play.tsx | 216 | ||||
| -rw-r--r-- | client/src/Mancala/StartGame.css | 10 | ||||
| -rw-r--r-- | client/src/Mancala/StartGame.tsx | 76 | ||||
| -rw-r--r-- | client/src/gameState.ts | 27 | ||||
| -rw-r--r-- | client/src/index.tsx | 15 | ||||
| -rw-r--r-- | client/src/pitState.ts | 0 | ||||
| -rw-r--r-- | client/src/pitState.tsx | 0 |
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 Binary files differnew file mode 100644 index 0000000..9104922 --- /dev/null +++ b/client/src/Header/logo.jpg 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 |
