diff --git a/.env b/.env index d3911fe1..3703645a 100644 --- a/.env +++ b/.env @@ -1,2 +1,3 @@ NODE_ENV=development -API_URL=http://localhost:8080 \ No newline at end of file +API_URL=http://localhost:8080 +PORT=8090 diff --git a/.gitignore b/.gitignore index 8c25b36e..0341ff70 100644 --- a/.gitignore +++ b/.gitignore @@ -150,4 +150,8 @@ sketch # Build build -# End of https://www.toptal.com/developers/gitignore/api/react,node \ No newline at end of file +# End of https://www.toptal.com/developers/gitignore/api/react,node + +# runtime env + +runtime-env.js \ No newline at end of file diff --git a/README.md b/README.md index 18223e6d..f6c29aa1 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,45 @@ # Snakebot Reactclient + This is the webclient for the Cygni snakebot tournament written in React with TypeScript. This application communicates with a [snakebot game server](https://github.com/cygni/snakebot) using a websocket. +# + ## Users -If you are a user who **only** wants to code your own bot, then simply head to the [snakebot client respository](https://github.com/cygni/snakebot-client-js) and follow the instructions there. There will be a *docker-compose* file there to easily get your own server and webclient running as containers without the need to clone them from here. +If you are a user who **only** wants to code your own bot, then simply head to the [snakebot client respository](https://github.com/cygni/snakebot-client-js) and follow the instructions there. There will be a _docker-compose_ file there to easily get your own server and webclient running as containers without the need to clone them from here. + +# ## Maintainers ### Requirements -* Node.js >= 16.15.1 -* npm >= 8.11.0 + +- Node.js >= 16.15.1 +- npm >= 8.11.0 + +# ### To get the development server running locally + After cloning the repository, open a terminal inside the root folder and run the following commands: + ``` > npm install > npm start ``` + The server should now be running on http://localhost:8090. +# + ### **Updates and Docker** **IMPORTANT**: Commits on the **main** branch will launch an action that builds and **overrides** the docker image tagged as the latest on [DockerHub](https://hub.docker.com/repository/docker/cygni/snakebot-reactclient). Therefore it is important to **ONLY** push changes to **main** that works and have been tested, to ensure that latest image works for anyone who wants to use it. If a commit is deemed **stable** you can also add a **tag** to that commit to ensure it remains on [DockerHub](https://hub.docker.com/repository/docker/cygni/snakebot-reactclient) without getting overwritten. E.g creating a release with a tag will both push the newly build docker image with the tag latest **AND** the tag name given as long as it follows the standard **X.X.X** name. Because of what mentioned above, when adding a new feature or changing some behavior, make sure to work on a **different branch** first before pushing to **main**. + +### **Production** + +**IMPORTANT**: Commits to the main branch will also act as commits towards production. Rebuilding the images on DockerHub through commits from main will cause the production server to reboot with the updated version of the image. + +# diff --git a/package.json b/package.json index ec566e1e..80c2de27 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { - "name": "snakebot-webclient-2.0", - "version": "0.1.0", - "homepage": "https://dolvur.github.io/", + "name": "snakebot-reactclient", + "description": "A new webclient for Snakebot", + "version": "1.0.0", + "contributors": ["Daniel Karlsson, Sebastian Helin", "Olivia Harlin"], "private": true, "dependencies": { "@reduxjs/toolkit": "^1.8.2", diff --git a/public/favicon-old.ico b/public/favicon-old.ico new file mode 100644 index 00000000..a11777cc Binary files /dev/null and b/public/favicon-old.ico differ diff --git a/public/favicon.ico b/public/favicon.ico index a11777cc..02fc382a 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/index.html b/public/index.html index 271907fc..2d0f80e1 100644 --- a/public/index.html +++ b/public/index.html @@ -11,7 +11,7 @@ Snakebot diff --git a/public/runtime-env.js b/public/runtime-env.js deleted file mode 100644 index 9b4024c9..00000000 --- a/public/runtime-env.js +++ /dev/null @@ -1 +0,0 @@ -window.__RUNTIME_CONFIG__ = {"NODE_ENV":"development","API_URL":"http://localhost:8080"}; \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 67e5977e..d7b0482e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,15 +1,13 @@ import './stylesheet.scss'; import { store } from './context/store'; import { Routes, Route, BrowserRouter } from 'react-router-dom'; +import { Provider } from 'react-redux'; +import axios from 'axios'; import PageHeader from './components/PageHeader'; import PageFooter from './components/PageFooter'; -import HomeView from './views/HomeView'; -import AboutView from './views/AboutView'; -import GettingStartedView from './views/GettingStartedView'; +import StartView from './views/StartView'; import GamesearchView from './views/GamesearchView'; -import axios from 'axios'; import GameboardView from './views/GameboardView'; -import { Provider } from 'react-redux'; import TournamentView from './views/TournamentView'; import LoginView from './views/LoginView' import Constants from './constants/Arbitraryconstants'; @@ -23,17 +21,15 @@ function App() { - }> - }> - }> + }> }> }> }> }> }> + - ); } diff --git a/src/api.ts b/src/api.ts index be893744..fba9191f 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,120 +1,154 @@ -import axios from "axios"; -import { Console } from "console"; -import SockJS from "sockjs-client"; -import Arbitraryconstants from "./constants/Arbitraryconstants"; -import MessageTypes, { GameSettings } from "./constants/messageTypes" -import { onSocketMessage } from "./context/messageDispatch"; -import { GameData } from "./context/slices/gameDataSlice"; - -const socket = new SockJS(Arbitraryconstants.SERVER_URL + "/events"); +import axios from 'axios'; +import SockJS from 'sockjs-client'; +import Arbitraryconstants from './constants/Arbitraryconstants'; +import { GameSettings } from './constants/messageTypes'; +import { onSocketMessage } from './context/messageDispatch'; +import { GameData } from './context/slices/gameDataSlice'; +import { clearTournament } from './context/slices/tournamentSlice'; +import { store } from './context/store'; + +let socket = new SockJS(Arbitraryconstants.SERVER_URL + '/events'); let onConnectQueue: string[] = []; +newConnection(); + +function newConnection() { + socket = new SockJS(Arbitraryconstants.SERVER_URL + '/events'); + + socket.onopen = () => { + console.log('Connected to server'); + // Send all queued messages + onConnectQueue.forEach((msg) => { + socket.send(msg); + }); + onConnectQueue = []; + }; + + socket.onmessage = (event: any) => onSocketMessage(event.data); + + socket.onclose = () => { + console.log('Disconnected from server'); + localStorage.clear(); + store.dispatch(clearTournament()); + + setTimeout(() => { + console.log('Trying to reconnect...'); + newConnection(); + }, 3000); + }; +} function sendWhenConnected(msg: string) { - console.log("Queuing/sending socket message:", JSON.parse(msg)); - if (socket.readyState === 1 && localStorage.getItem("token") !== null) { - socket.send(msg); - } else { - onConnectQueue.push(msg); // if no connection is established, save message in queue - } + console.log('Queuing/sending socket message:', JSON.parse(msg)); + if (socket.readyState === 1 && localStorage.getItem('token') !== null) { + socket.send(msg); + } else { + onConnectQueue.push(msg); // if no connection is established, save message in queue + } +} + +// ############################################################################################## +// ########################### REST API ######################################################## +// ############################################################################################## +export async function getToken(username: string, password: string): Promise<{ success: boolean; data: string }> { + try { + let resp = await axios.get(`/login?login=${username}&password=${password}`); + return { success: true, data: resp.data }; + } catch (error: any) { + console.error('Error getting token:', error); + return { success: false, data: typeof error.response.data === 'string' ? error.response.data : error.message }; + } } -export async function getToken(username: string, password: string): Promise<{success: boolean, data: string}> { - try { - let resp = await axios.get(`/login?login=${username}&password=${password}`) - return {success: true, data: resp.data}; - } catch (error: any) { - console.error("Error getting token:", error); - return {success: false, data: typeof(error.response.data)==="string" ? error.response.data : error.message}; - } +export type Game = { + gameDate: string; + gameId: string; + players: string[]; +}; + +async function searchForGames(snakeName: string): Promise { + const resp = await axios.get(`/history/search/${snakeName}`).catch((err) => { + console.error(err); + }); + return resp ? resp.data.items : []; } +async function getGame(gameId: string): Promise { + const resp = await axios.get(`/history/${gameId}`).catch((err) => { + console.error(err); + }); + return resp ? resp.data : {}; +} -socket.onopen = () => { - console.log("Connected to server"); - // Send all queued messages - onConnectQueue.forEach(msg => { - socket.send(msg); - }); - onConnectQueue = []; +// ############################################################################################## +// ###################### SOCKET FUNCTIONS ##################################################### +// ############################################################################################## +async function createTournament(tournamentName: string): Promise { + sendWhenConnected( + JSON.stringify({ + type: 'se.cygni.snake.eventapi.request.CreateTournament', + token: localStorage.getItem('token'), + tournamentName: tournamentName, + }) + ); } -socket.onmessage = (event: any) => onSocketMessage(event.data); +async function killTournament(): Promise { + sendWhenConnected( + JSON.stringify({ + type: 'se.cygni.snake.eventapi.request.KillTournament', + token: localStorage.getItem('token'), + tournamentId: 'NOT_IMPLEMENTED', + }) + ); +} -socket.onclose = () => { - console.log("Disconnected from server"); +async function getActiveTournament(): Promise { + sendWhenConnected( + JSON.stringify({ + type: 'se.cygni.snake.eventapi.request.GetActiveTournament', + token: localStorage.getItem('token'), + }) + ); } -export type Game = { - gameDate: string, - gameId: string, - players: string[], +async function startTournament(tournamentId: string): Promise { + sendWhenConnected( + JSON.stringify({ + type: 'se.cygni.snake.eventapi.request.StartTournament', + token: localStorage.getItem('token'), + tournamentId: tournamentId, + }) + ); +} + +async function startTournamentGame(gameId: string): Promise { + sendWhenConnected( + JSON.stringify({ + type: 'se.cygni.snake.eventapi.request.StartGame', + gameId: gameId, + }) + ); +} + +async function updateTournamentSettings(gameSettings: GameSettings): Promise { + sendWhenConnected( + JSON.stringify({ + type: 'se.cygni.snake.eventapi.request.UpdateTournamentSettings', + token: localStorage.getItem('token'), + gameSettings: gameSettings, + }) + ); } -export default { - - // ############################################################################################## - // ########################### REST API ######################################################## - // ############################################################################################## - async searchForGames(snakeName: string): Promise { - const resp = await axios.get(`/history/search/${snakeName}`).catch(err => { - console.error(err); - }); - return resp ? resp.data.items: []; - }, - - async getGame(gameId: string): Promise { - const resp = await axios.get(`/history/${gameId}`).catch(err => { - console.error(err); - }); - return resp ? resp.data: {}; - }, - - // ############################################################################################## - // ###################### SOCKET FUNCTIONS ##################################################### - // ############################################################################################## - async createTournament(tournamentName: string): Promise { - sendWhenConnected(JSON.stringify({ - type: 'se.cygni.snake.eventapi.request.CreateTournament', - token: localStorage.getItem("token"), - tournamentName: tournamentName, - })); - }, - - async killTournament(): Promise { - sendWhenConnected(JSON.stringify({ - type: 'se.cygni.snake.eventapi.request.KillTournament', - token: localStorage.getItem("token"), - tournamentId: 'NOT_IMPLEMENTED', - })); - }, - - async getActiveTournament(): Promise { - sendWhenConnected(JSON.stringify({ - type: 'se.cygni.snake.eventapi.request.GetActiveTournament', - token: localStorage.getItem("token"), - })); - }, - - async startTournament(tournamentId: string): Promise { - sendWhenConnected(JSON.stringify({ - type: 'se.cygni.snake.eventapi.request.StartTournament', - token: localStorage.getItem("token"), - tournamentId: tournamentId, - })); - }, - - async startTournamentGame(gameId: string): Promise { - sendWhenConnected(JSON.stringify({ - type: 'se.cygni.snake.eventapi.request.StartGame', - gameId: gameId, - })); - }, - - async updateTournamentSettings(gameSettings: GameSettings): Promise { - sendWhenConnected(JSON.stringify({ - type: 'se.cygni.snake.eventapi.request.UpdateTournamentSettings', - token: localStorage.getItem("token"), - gameSettings: gameSettings, - })); - }, -}; \ No newline at end of file +const api = { + searchForGames, + getGame, + createTournament, + killTournament, + getActiveTournament, + startTournament, + startTournamentGame, + updateTournamentSettings, +}; + +export default api; diff --git a/src/assets/icons/search-icon.svg b/src/assets/icons/search-icon.svg new file mode 100644 index 00000000..d3a73efd --- /dev/null +++ b/src/assets/icons/search-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/Podium.svg b/src/assets/images/Podium.svg new file mode 100644 index 00000000..6d78779a --- /dev/null +++ b/src/assets/images/Podium.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/images/cygni-instagram.png b/src/assets/images/cygni-instagram.png new file mode 100644 index 00000000..21a9277f Binary files /dev/null and b/src/assets/images/cygni-instagram.png differ diff --git a/src/assets/images/podium.png b/src/assets/images/podium.png deleted file mode 100644 index 1d2fa2b7..00000000 Binary files a/src/assets/images/podium.png and /dev/null differ diff --git a/src/assets/logos/cygni-logo-black.svg b/src/assets/logos/cygni-logo-black.svg new file mode 100644 index 00000000..58615d2a --- /dev/null +++ b/src/assets/logos/cygni-logo-black.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/canvasComponents/Obstacles.tsx b/src/canvasComponents/Obstacles.tsx deleted file mode 100644 index c21dd9e6..00000000 --- a/src/canvasComponents/Obstacles.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { useEffect, useRef } from "react"; -import { Image, Group } from "react-konva" -import { TILE_OFFSET_X, TILE_OFFSET_Y, TILE_SIZE } from "../constants/BoardUtils"; -import { getBlackhole } from "../constants/Images"; -import { TilePosition } from "../context/slices/currentFrameSlice"; -import konva from "konva"; - -type Props = { - obstacles: TilePosition[]; -} - -function Obstacles({obstacles}: Props) { - const image = getBlackhole(); - const groupRef = useRef(null); - - let anim = new konva.Animation((frame)=>{ - groupRef?.current?.children.forEach((child: any) => child.rotate(360 * (frame?.timeDiff!/1000) )); - }, groupRef.current); - - useEffect(()=>{ - anim.start() - }, []); - - function getNeighbouringObstacles(obstacle: TilePosition) { - return (obstacles.filter(o => o.x === obstacle.x && o.y === obstacle.y + 1 || - o.x === obstacle.x && o.y === obstacle.y - 1 || - o.x === obstacle.x + 1 && o.y === obstacle.y || - o.x === obstacle.x - 1 && o.y === obstacle.y)); - } - - function findCluster(obstacle: TilePosition) { - const cluster: TilePosition[] = []; - if (obstacle === undefined) return cluster; - const visited: TilePosition[] = []; - const queue = [obstacle]; - while (queue.length > 0) { - const current = queue.shift()!; - if (visited.includes(current)) continue; - visited.push(current); - cluster.push(current); - const neighbors = getNeighbouringObstacles(current); - - neighbors.forEach((neighbor) => { - if (!visited.includes(neighbor)) { - queue.push(neighbor); - } - }); - } - return cluster; - } - - function renderObstacles() { - const alreadyRendered = new Set(); - const imagesToRender = []; - - for (let i = 0; i < obstacles.length; i++) { - const obstacle = obstacles[i]; - if (alreadyRendered.has(obstacle)) continue; // Skip clusters already rendered - alreadyRendered.add(obstacle); - - const cluster = findCluster(obstacle); - cluster.forEach((tile) => { - alreadyRendered.add(tile); - }); - - const sizeFactor = Math.sqrt(cluster.length); - const offset = (TILE_SIZE * sizeFactor)/2; - imagesToRender.push( - ); - } - - return imagesToRender; - } - - return ( - - {renderObstacles()} - - ) -} - -export default Obstacles \ No newline at end of file diff --git a/src/canvasComponents/Stars.tsx b/src/canvasComponents/Stars.tsx deleted file mode 100644 index e9b5b68a..00000000 --- a/src/canvasComponents/Stars.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useEffect, useRef } from "react"; -import { TilePosition } from "../context/slices/currentFrameSlice"; -import konva from "konva"; -import { Image, Group } from "react-konva" -import { TILE_OFFSET_X, TILE_OFFSET_Y, TILE_SIZE } from "../constants/BoardUtils"; -import { getStar } from "../constants/Images"; - -type Props = { - stars: TilePosition[]; -} - -function Stars({stars}: Props) { - const starImage = getStar(); - const groupRef = useRef(null); - const lastTime = useRef(0); - - let anim = new konva.Animation((frame)=>{ - groupRef?.current?.children.forEach((child: any, index: number) => { - if (frame!.time > lastTime.current + 1000) { - if (child.opacity() < 0.5) { - child.to({ - opacity: 1, - duration: 0.5, - scaleX: 1, - scaleY: 1, - }) - } else if (Math.random() > 0.9) { - child.to({ - duration: 1, - opacity: 0.4, - scaleX: 0.2, - scaleY: 0.2, - }); - } - - if (index === groupRef.current.children.length - 1) { - lastTime.current = frame!.time; - } - } - }); - }, groupRef.current); - - useEffect(()=>{ - anim.start() - }, []); - - const offset = TILE_SIZE/2; - return ( - - {stars.map((star, index) => ())} - - ) -} - -export default Stars \ No newline at end of file diff --git a/src/components/ControllBar.tsx b/src/components/ControllBar.tsx index d7fd9659..d89e4ceb 100644 --- a/src/components/ControllBar.tsx +++ b/src/components/ControllBar.tsx @@ -1,45 +1,43 @@ -import Backwards from '../assets/icons/icon-backwards.svg'; -import Play from '../assets/icons/icon-play.svg'; -import Pause from '../assets/icons/icon-pause.svg'; -import Replay from '../assets/icons/icon-replay.svg'; -import Forwards from '../assets/icons/icon-forward.svg'; +import Backwards from "../assets/icons/icon-backwards.svg"; +import Play from "../assets/icons/icon-play.svg"; +import Pause from "../assets/icons/icon-pause.svg"; +import Replay from "../assets/icons/icon-replay.svg"; +import Forwards from "../assets/icons/icon-forward.svg"; -import messageDispatch from '../context/messageDispatch'; -import { useEffect, useState } from 'react'; -import Constants from '../constants/Arbitraryconstants'; -import { setCounter } from '../context/slices/gameDataSlice'; -import { useAppDispatch, useAppSelector } from '../context/hooks'; +import messageDispatch from "../context/messageDispatch"; +import { useEffect, useRef, useState } from "react"; +import Constants from "../constants/Arbitraryconstants"; +import { setMessageIndex } from "../context/slices/gameDataSlice"; +import { useAppDispatch, useAppSelector } from "../context/hooks"; -function ControllBar(){ - - const frameCount = useAppSelector(state => state.gameData.messages.length); - const currentFrame = useAppSelector(state => state.gameData.counter); +function ControllBar() { const dispatch = useAppDispatch(); - + const messagesLength = useAppSelector((state) => state.gameData.messages?.length); + const messageIndex = useAppSelector((state) => state.gameData.counter); const [running, setRunning] = useState(false); const [frequency, setFrequency] = useState(Constants.STARTING_FREQUENCY); - let intervalID: NodeJS.Timer; - const gameEnded = () => currentFrame >= frameCount-1; + const gameEnded = useAppSelector((state) => state.currentFrame.gameEnded); + const intervalID = useRef(); useEffect(() => { if (running) { - intervalID = setInterval(() => { + intervalID.current = setInterval(() => { if (running) { messageDispatch(); } }, frequency); } - return () => clearInterval(intervalID); + return () => clearInterval(intervalID.current); }, [running, frequency]); useEffect(() => { - if (gameEnded()) { + if (gameEnded) { setRunning(false); } }, [gameEnded]); function getPlayIcon() { - if (gameEnded()) { + if (gameEnded) { return Replay; } if (!running) { @@ -49,48 +47,50 @@ function ControllBar(){ } return ( -
+
{ - console.log("event value:", e.target.value); - dispatch(setCounter(parseInt(e.target.value))); + dispatch(setMessageIndex(parseInt(e.target.value))); messageDispatch(false); }} /> -
+
setFrequency(frequency + Constants.FREQUENCY_STEP)} /> { + type='image' + src={getPlayIcon()} + alt='playbutton' + name='PlayButton' + className='playButton' + onClick={() => { setRunning(!running); - if (gameEnded()) dispatch(setCounter(0)); + if (gameEnded) dispatch(setMessageIndex(0)); }} /> setFrequency(Math.max(frequency - Constants.FREQUENCY_STEP, Constants.MIN_FREQUENCY))} + alt='forwardbutton' + name='ButtonForward' + className='forwardButton' + onClick={() => setFrequency(Math.max(frequency - Constants.FREQUENCY_STEP, Constants.MIN_FREQUENCY))} />
); } -export default ControllBar; \ No newline at end of file +export default ControllBar; diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx deleted file mode 100644 index 11e8594d..00000000 --- a/src/components/Modal.tsx +++ /dev/null @@ -1,57 +0,0 @@ - -import { RiCloseLine } from "react-icons/ri"; -import { useSelector } from "react-redux" -import type { RootState } from '../context/store'; - - -function Modal({setIsOpen} :any){ - const snakes = useSelector((state: RootState) => state.currentFrame); - return ( - <> -
setIsOpen(false)}/> -
-
-
- -
- snakehead -
-
- -

{snakes.playerRanks[0]}

-
-
- -

{snakes.playerPoints[0]}

-
-
- snakehead -
-
-

{snakes.playerRanks[1]}

-
-
- -

{snakes.playerPoints[1]}

-
-
- snakehead -
-
-

{snakes.playerRanks[2]}

-
-
-

{snakes.playerPoints[2]}

-
-
- - -
-
- - ); -} - -export default Modal; \ No newline at end of file diff --git a/src/components/PageFooter.tsx b/src/components/PageFooter.tsx index 3c3a7ff2..1d7a864a 100644 --- a/src/components/PageFooter.tsx +++ b/src/components/PageFooter.tsx @@ -1,14 +1,15 @@ -import cygnilogo from '../assets/logos/cygni-logo.svg'; +import cygnilogo from '../assets/logos/cygni-logo-black.svg'; function PageFooter() { return ( - + <> + + ); } -export default PageFooter; \ No newline at end of file +export default PageFooter; diff --git a/src/components/PageHeader.tsx b/src/components/PageHeader.tsx index b534d5a7..ec9a7819 100644 --- a/src/components/PageHeader.tsx +++ b/src/components/PageHeader.tsx @@ -1,11 +1,12 @@ import snakelogo from '../assets/logos/snakelogo.png'; -import { Link, NavLink } from 'react-router-dom'; +import { Link, NavLink, useLocation } from 'react-router-dom'; import { useAppDispatch, useAppSelector } from '../context/hooks'; import { setLoggedIn } from '../context/slices/tournamentSlice'; function PageHeader() { - const isLoggedIn = useAppSelector(state => state.tournament.isLoggedIn); + const { pathname } = useLocation(); const dispatch = useAppDispatch(); + const isLoggedIn = useAppSelector((state) => state.tournament.isLoggedIn); function handleLogout() { localStorage.removeItem('token'); @@ -13,26 +14,44 @@ function PageHeader() { } return ( -
- - Snakebot-logo - - -
+
+ + Snakebot-logo + + +
); } -export default PageHeader; \ No newline at end of file +export default PageHeader; diff --git a/src/components/PlayerList.tsx b/src/components/PlayerList.tsx index dcd999e7..9ccd7f65 100644 --- a/src/components/PlayerList.tsx +++ b/src/components/PlayerList.tsx @@ -1,17 +1,52 @@ -import { useAppSelector } from '../context/hooks' +import { useAppDispatch, useAppSelector } from "../context/hooks"; +import { + editSettings, + startTournament, +} from "../context/slices/tournamentSlice"; +import api from "../api"; function PlayerList() { -const playerList = useAppSelector(state => state.tournament.players); + const tournamentPlayers = useAppSelector((state) => state.tournament.players); + const tournamentId = useAppSelector((state) => state.tournament.tournamentId); + const dispatch = useAppDispatch(); + + function initTournament() { + if (tournamentPlayers.length >= 2) { + console.log("Starting tournament..."); + dispatch(startTournament()); + api.startTournament(tournamentId); + } else { + alert("A minimum of 2 players is required to start Tournament"); + } + } + + function backToSettings() { + dispatch(editSettings()); + } return ( <> -
-

Players

- {playerList.map((player, index) => ( -

Player {index+1}: {player.name}

- )) - } +

Connect to game

+

+ Connect to the tournament by following the instructions found in the + README-file +

+
+ {tournamentPlayers.map((player, index) => ( +

+ {player.name} +

+ ))} +
+
+ + +
- ) + ); } -export default PlayerList \ No newline at end of file +export default PlayerList; diff --git a/src/components/ScoreBoard.tsx b/src/components/ScoreBoard.tsx index 78021603..6ee3d60a 100644 --- a/src/components/ScoreBoard.tsx +++ b/src/components/ScoreBoard.tsx @@ -1,11 +1,11 @@ -import { getCurrentSnakeHead } from '../constants/Images' +import { getCurrentSnakeHead } from '../constants/Images'; import { useAppSelector } from '../context/hooks'; function ScoreBoard() { - const snakes = useAppSelector(state => state.currentFrame.snakesData); + const snakes = useAppSelector((state) => state.currentFrame.snakesData); - //SORT BY 1. ALIVE, 2. POINTS, 3.NAME IN THAT ORDER - function sortSnakes(snakeID_one: string, snakeID_two: string){ + // SORT BY 1. ALIVE, 2. POINTS, 3.NAME IN THAT ORDER + function sortSnakes(snakeID_one: string, snakeID_two: string) { const snakeOne = snakes[snakeID_one]; const snakeTwo = snakes[snakeID_two]; @@ -14,37 +14,40 @@ function ScoreBoard() { } else if (!snakeTwo.alive && snakeOne.alive) { return -1; } - + const pointDiff = snakeTwo.points - snakeOne.points; if (pointDiff !== 0) { return pointDiff; } - + if (snakeOne.name > snakeTwo.name) { return 1; - } else if (snakeTwo.name < snakeTwo.name) { + } else if (snakeTwo.name < snakeOne.name) { return -1; } return 0; } return ( -
+
+

Leaderboard

+
    - { - Object.keys(snakes).sort(sortSnakes).map((snakeID, index) => ( -
  • -
    - snakehead -
    - {snakes[snakeID].points} - {snakes[snakeID].name} -
  • - )) - } + {Object.keys(snakes) + .sort(sortSnakes) + .map((snakeID, index) => ( +
  • + snakehead +
    +

    {snakes[snakeID].name}

    +

    {snakes[snakeID].points} points

    +
    +
  • + ))}
+
- ) + ); } export default ScoreBoard; diff --git a/src/components/Tournament/TournamentBracket.tsx b/src/components/Tournament/TournamentBracket.tsx deleted file mode 100644 index 0533e266..00000000 --- a/src/components/Tournament/TournamentBracket.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { TournamentGame } from '../../constants/messageTypes'; -import Star from '../../assets/images/star.svg'; -import { useNavigate } from 'react-router-dom'; -import { useAppDispatch, useAppSelector } from '../../context/hooks'; -import { viewedGame } from '../../context/slices/tournamentSlice'; -import { useCallback } from 'react'; - -type Props = { - tournamentGame: TournamentGame; - levelIndex: number; -} - -function TournamentBracket({tournamentGame, levelIndex}: Props) { - const star = Lived the longest - const navigate = useNavigate(); - const dispatch = useAppDispatch(); - const priorLevel = useAppSelector(state => state.tournament.tournamentLevels[levelIndex-1]); - - function goToGame(tournamentGame: TournamentGame) { - if (tournamentGame.gamePlayed && priorLevelViewed()) { - console.log("Go to tournamentGame:", tournamentGame.gameId); - dispatch(viewedGame(tournamentGame.gameId)); - navigate(`/tournament/${tournamentGame.gameId}`); - } else { - alert("You must view the previous round before you can view this game"); - } - } - - const priorLevelViewed = useCallback( - () => { - if (levelIndex===0) return true; - return priorLevel.tournamentGames.every(game => game.isViewed); - }, [priorLevel]); - - function RenderPlayer(index: number) { - let player = tournamentGame.players[index]; - if (!player || !priorLevelViewed()) { - - return
  • - TBD -
  • ; - } - - if (tournamentGame.gamePlayed && tournamentGame.isViewed) { - return ( -
  • - {player.points} - {player.name} {player.isWinner ? star : null} -
  • - ); - } - - return ( -
  • {player.name}
  • - ); - } - - return ( - <> - {[...Array(tournamentGame.expectedNoofPlayers)].map((_, index) => RenderPlayer(index))} - -
    - -
    - - ) -} - -export default TournamentBracket \ No newline at end of file diff --git a/src/components/Tournament/TournamentSchedule.tsx b/src/components/Tournament/TournamentSchedule.tsx deleted file mode 100644 index 18468647..00000000 --- a/src/components/Tournament/TournamentSchedule.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { TournamentLevel } from '../../constants/messageTypes'; -import { useAppSelector } from '../../context/hooks' -import TournamentBracket from './TournamentBracket'; - -function roundClassName(round: TournamentLevel) { - switch (round.level) { - case 0: - return 'first-round'; - case 1: - return 'second-round'; - case 2: - return 'third-round'; - default: - return 'final-round'; - // default to keeping it at the largest size - } -} - - -function TournamentSchedule() { - const levels = useAppSelector(state => state.tournament.tournamentLevels); - const tournamentName = useAppSelector(state => state.tournament.tournamentName); - return ( -
    -
    -

    {tournamentName}

    -
    -
    - {levels.slice(0).reverse().map((level, index) => ( -
    -
    -

    Round {level.level}

    -
    - { - level.tournamentGames.map((game, gameIndex) => ( -
      - -
    - )) - } -
    -
    - { level.level > 0 ?
    :
    } -
    - ))} -
    -
    - ) -} - -export default TournamentSchedule \ No newline at end of file diff --git a/src/components/Tournament/TournamentSettings.tsx b/src/components/Tournament/TournamentSettings.tsx deleted file mode 100644 index 0c232929..00000000 --- a/src/components/Tournament/TournamentSettings.tsx +++ /dev/null @@ -1,301 +0,0 @@ -import {useState, useEffect } from 'react' -import api from '../../api'; -import { useAppDispatch, useAppSelector } from '../../context/hooks' -import { startTournament, updateGameSettings } from '../../context/slices/tournamentSlice' -import PlayerList from '../PlayerList'; - -function TournamentSettings() { - const gameSettings = useAppSelector(state => state.tournament.gameSettings); - const tournamentName = useAppSelector(state => state.tournament.tournamentName); - const tournamentId = useAppSelector(state => state.tournament.tournamentId); - const noofPlayerrs = useAppSelector(state => state.tournament.players); - const [localGameSettings, setLocalGameSettings] = useState(gameSettings); - const dispatch = useAppDispatch(); - - function initTournament(){ - dispatch(updateGameSettings(localGameSettings)); - api.startTournament(tournamentId); - } - - useEffect(() => { - console.log("Got default tournament settings from socket:",gameSettings); - setLocalGameSettings(gameSettings); - }, [gameSettings]); - - return ( -
    -
    -
    -

    {tournamentName}

    -
    - -
    { - e.preventDefault(); - if(noofPlayerrs.length >= 2 ){ - console.log("Starting tournament..."); - dispatch(startTournament()); - initTournament(); - }else{ - alert('A minimum of 2 players is required to start Tournament'); - } - }}> - -
    - - {setLocalGameSettings({...localGameSettings, maxNoofPlayers: parseInt(e.target.value)})}} - /> - -
    - -
    - - {setLocalGameSettings({...localGameSettings, startSnakeLength: parseInt(e.target.value)})}} - /> -
    - -
    - - {setLocalGameSettings({...localGameSettings, timeInMsPerTick: parseInt(e.target.value)})}} - /> -
    - -
    - - {setLocalGameSettings({...localGameSettings, pointsPerLength: parseInt(e.target.value)})}} - /> -
    - -
    - - {setLocalGameSettings({...localGameSettings, pointsPerFood: parseInt(e.target.value)})}} - /> -
    - -
    - - {setLocalGameSettings({...localGameSettings, pointsPerCausedDeath: parseInt(e.target.value)})}} - /> -
    - -
    - - {setLocalGameSettings({...localGameSettings, pointsPerNibble: parseInt(e.target.value)})}} - /> -
    - -
    - - {setLocalGameSettings({...localGameSettings, noofRoundsTailProtectedAfterNibble: parseInt(e.target.value)})}} - /> -
    - - -
    -
    - -
    - {setLocalGameSettings({...localGameSettings, addFoodLikelihood: parseInt(e.target.value)})}} - /> -
    - -
    -
    - -
    -
    - {setLocalGameSettings({...localGameSettings, removeFoodLikelihood: parseInt(e.target.value)})}} - - /> -
    -
    - -
    -
    - -
    - -
    - {setLocalGameSettings({...localGameSettings, obstaclesEnabled: true})}} - /> True - - -
    -
    - -
    -
    - -
    -
    - {setLocalGameSettings({...localGameSettings, foodEnabled: true})}} - /> True - - -
    -
    - -
    -
    - -
    -
    - {setLocalGameSettings({...localGameSettings, headToTailConsumes: true})}} - /> True - -
    -
    - -
    -
    - -
    -
    - {setLocalGameSettings({...localGameSettings, tailConsumeGrows: true})}} - /> True - - -
    -
    - -
    - -
    - - - -
    - -
    -
    - ) -} - -export default TournamentSettings \ No newline at end of file diff --git a/src/components/canvas/Food.tsx b/src/components/canvas/Food.tsx new file mode 100644 index 00000000..2f0515f4 --- /dev/null +++ b/src/components/canvas/Food.tsx @@ -0,0 +1,32 @@ +import { useRef } from "react"; +import { TilePosition } from "../../context/slices/currentFrameSlice"; +import { Image, Group } from "react-konva"; +import { TILE_OFFSET_X, TILE_OFFSET_Y, TILE_SIZE } from "../../constants/BoardUtils"; +import { getStar } from "../../constants/Images"; + +type Props = { + stars: TilePosition[]; +}; + +function Stars({ stars }: Props) { + const starImage = getStar(); + const groupRef = useRef(null); // Can be used later to animate the stars + const offset = TILE_SIZE / 2; + return ( + + {stars.map((star, index) => ( + + ))} + + ); +} + +export default Stars; diff --git a/src/components/canvas/Obstacles.tsx b/src/components/canvas/Obstacles.tsx new file mode 100644 index 00000000..621a9694 --- /dev/null +++ b/src/components/canvas/Obstacles.tsx @@ -0,0 +1,97 @@ +import { useEffect, useMemo, useRef } from "react"; +import { Image, Group } from "react-konva"; +import { TILE_OFFSET_X, TILE_OFFSET_Y, TILE_SIZE } from "../../constants/BoardUtils"; +import { getBlackhole } from "../../constants/Images"; +import { TilePosition } from "../../context/slices/currentFrameSlice"; +import konva from "konva"; + +type Props = { + obstacles: TilePosition[]; +}; + +function Obstacles({ obstacles }: Props) { + const image = getBlackhole(); + const groupRef = useRef(null); + + const anim = useMemo( + () => + new konva.Animation((frame) => { + groupRef.current?.children.forEach((child: any) => child.rotate(360 * (frame!.timeDiff / 1000))); + }), + [groupRef] + ); + + useEffect(() => { + anim.start(); + return () => { + anim.stop(); + }; + }, [anim]); + + function getNeighbouringObstacles(obstacle: TilePosition) { + return obstacles.filter( + (o) => + (o.x === obstacle.x && o.y === obstacle.y + 1) || + (o.x === obstacle.x && o.y === obstacle.y - 1) || + (o.x === obstacle.x + 1 && o.y === obstacle.y) || + (o.x === obstacle.x - 1 && o.y === obstacle.y) + ); + } + + function findCluster(obstacle: TilePosition) { + const cluster: TilePosition[] = []; + if (obstacle === undefined) return cluster; + const visited: TilePosition[] = []; + const queue = [obstacle]; + while (queue.length > 0) { + const current = queue.shift()!; + if (visited.includes(current)) continue; + visited.push(current); + cluster.push(current); + const neighbors = getNeighbouringObstacles(current); + + neighbors.forEach((neighbor) => { + if (!visited.includes(neighbor)) { + queue.push(neighbor); + } + }); + } + return cluster; + } + + function renderObstacles() { + const alreadyRendered = new Set(); + const imagesToRender = []; + + for (let i = 0; i < obstacles.length; i++) { + const obstacle = obstacles[i]; + if (alreadyRendered.has(obstacle)) continue; // Skip clusters already rendered + alreadyRendered.add(obstacle); + + const cluster = findCluster(obstacle); + cluster.forEach((tile) => { + alreadyRendered.add(tile); + }); + + const sizeFactor = Math.sqrt(cluster.length); + const offset = (TILE_SIZE * sizeFactor) / 2; + imagesToRender.push( + + ); + } + + return imagesToRender; + } + + return {renderObstacles()}; +} + +export default Obstacles; diff --git a/src/canvasComponents/Snake.tsx b/src/components/canvas/Snake.tsx similarity index 54% rename from src/canvasComponents/Snake.tsx rename to src/components/canvas/Snake.tsx index c2114406..d10eb57e 100644 --- a/src/canvasComponents/Snake.tsx +++ b/src/components/canvas/Snake.tsx @@ -1,14 +1,14 @@ -import { Group, Rect, Image, Arc } from 'react-konva'; -import { TILE_MARGIN, TILE_OFFSET_X, TILE_OFFSET_Y, TILE_SIZE } from '../constants/BoardUtils'; -import { SnakeData, TilePosition } from '../context/slices/currentFrameSlice'; -import Colors from '../constants/Colors'; -import { getCurrentSnakeHead, getCurrentSnakeTail } from '../constants/Images'; +import { Group, Rect, Image, Arc } from "react-konva"; +import { TILE_MARGIN, TILE_OFFSET_X, TILE_OFFSET_Y, TILE_SIZE } from "../../constants/BoardUtils"; +import { SnakeData, TilePosition } from "../../context/slices/currentFrameSlice"; +import Colors from "../../constants/Colors"; +import { getCurrentSnakeHead, getCurrentSnakeTail } from "../../constants/Images"; type Props = { snake: SnakeData; -} +}; -function Snake({snake}: Props) { +function Snake({ snake }: Props) { const headImage = getCurrentSnakeHead(snake); const tailImage = getCurrentSnakeTail(snake); const color = snake.alive ? snake.color : Colors.DEAD_SNAKE; @@ -17,7 +17,7 @@ function Snake({snake}: Props) { if (line.length <= 1) return null; // Check if line is horizontal or vertical - const horizontal = line[0].y === line[1].y; + const isHorizontal: boolean = line[0].y === line[1].y; // Temporary for not drawing the last tile and not getting it as left/topMost tile line = line.slice(0, line.length - 1); @@ -26,75 +26,71 @@ function Snake({snake}: Props) { if (JSON.stringify(line[0]) === JSON.stringify(snake.positions[0])) { line = line.slice(1); if (line.length === 0) return null; - }; + } - if (horizontal) { - const leftMostTile = line.reduce((prev, curr) => prev.x < curr.x ? prev : curr); + if (isHorizontal) { + const leftMostTile = line.reduce((prev, curr) => (prev.x < curr.x ? prev : curr)); return ( ) + /> + ); } else { - const topMostTile = line.reduce((prev, curr) => prev.y < curr.y ? prev : curr); + const topMostTile = line.reduce((prev, curr) => (prev.y < curr.y ? prev : curr)); return ( ) + /> + ); } } - function renderRect() { + function renderSnakeBody() { if (snake.positions.length <= 1) return null; let lines: TilePosition[][] = []; let currentLine = [snake.positions[0]]; let lastMovementHorizontal = true; - for(let i = 1; i < snake.positions.length; i++) { - const dy = snake.positions[i].y - snake.positions[i-1].y; - const currentMovementHorizontal = (dy === 0); + for (let i = 1; i < snake.positions.length; i++) { + const dy = snake.positions[i].y - snake.positions[i - 1].y; + const currentMovementHorizontal = dy === 0; if (i === 1) lastMovementHorizontal = currentMovementHorizontal; // first movement is same direction - if (currentMovementHorizontal !== lastMovementHorizontal) { // new line + if (currentMovementHorizontal !== lastMovementHorizontal) { + // new line if (currentLine.length > 0) lines.push(currentLine); currentLine = [snake.positions[i]]; } else { currentLine.push(snake.positions[i]); } lastMovementHorizontal = currentMovementHorizontal; - } lines.push(currentLine); // add last line return lines.map((line, lineIndex) => { - const lastTile = line[line.length-1]; + const lastTile = line[line.length - 1]; // Draw line return ( {drawLine(line)} - {drawRoundedRect(lastTile, lines, lineIndex)} + {drawRoundedPart(lastTile, lines, lineIndex)} ); }); } - function drawRoundedRect(tile: TilePosition, lines: TilePosition[][], lineIndex: number) { + function drawRoundedPart(tile: TilePosition, lines: TilePosition[][], lineIndex: number) { const currLine = lines[lineIndex]; - const nextLine = lines[lineIndex+1]; - const prevLine = lines[lineIndex-1]; + const nextLine = lines[lineIndex + 1]; + const prevLine = lines[lineIndex - 1]; if (nextLine === undefined) return null; // tail of snake const toTile = nextLine[0]; let fromTile = currLine[currLine.length - 2]; // second last tile of current line @@ -103,67 +99,65 @@ function Snake({snake}: Props) { fromTile = prevLine[prevLine.length - 1]; } - const tileToRight = (toTile.x - tile.x === 1) || (fromTile.x - tile.x === 1); - const tileToUp = (toTile.y - tile.y === 1) || (fromTile.y - tile.y === 1); - const tileToLeft = (toTile.x - tile.x === -1) || (fromTile.x - tile.x === -1); - const tileToDown = (toTile.y - tile.y === -1) || (fromTile.y - tile.y === -1); + const tileToRight = toTile.x - tile.x === 1 || fromTile.x - tile.x === 1; + const tileToUp = toTile.y - tile.y === 1 || fromTile.y - tile.y === 1; + const tileToLeft = toTile.x - tile.x === -1 || fromTile.x - tile.x === -1; + const tileToDown = toTile.y - tile.y === -1 || fromTile.y - tile.y === -1; let rotation = 0; if (tileToRight) { rotation = tileToUp ? 180 : 90; } else if (tileToLeft) { - rotation = tileToDown ? 0 : 270; + rotation = tileToDown ? 0 : 270; } else { console.error("Could not determine rotation", tile, toTile, fromTile); } - const offset = (TILE_SIZE)/2; + const offset = TILE_SIZE / 2; return ( ); } - function renderImage(tile: TilePosition, toTile: TilePosition, image: HTMLImageElement, rotationOffset=0) { + function renderImage(tile: TilePosition, toTile: TilePosition, image: HTMLImageElement, rotationOffset = 0) { if (tile === undefined) return null; const dx = tile.x - toTile?.x; const dy = tile.y - toTile?.y; - let marginOffsetX = TILE_MARGIN/2; - let marginOffsetY = TILE_MARGIN/2; + let marginOffsetX = TILE_MARGIN / 2; + let marginOffsetY = TILE_MARGIN / 2; let rotation = 0; - if (dx === 0) { // Vertical + if (dx === 0) { + // Vertical if (dy === 1) { - marginOffsetY -= TILE_MARGIN/2; + marginOffsetY -= TILE_MARGIN / 2; rotation = 180; - } - else { - marginOffsetY += TILE_MARGIN/2; + } else { + marginOffsetY += TILE_MARGIN / 2; rotation = 0; } - } else if (dy === 0) { // Horizontal + } else if (dy === 0) { + // Horizontal if (dx === 1) { - marginOffsetX -= TILE_MARGIN/2; + marginOffsetX -= TILE_MARGIN / 2; rotation = 90; - } - else { - marginOffsetX += TILE_MARGIN/2; + } else { + marginOffsetX += TILE_MARGIN / 2; rotation = 270; } } - - const offset = (TILE_SIZE - TILE_MARGIN)/2; + + const offset = (TILE_SIZE - TILE_MARGIN) / 2; return ( - ) - + ); } return ( <> - {renderRect()} - {renderImage(snake.positions[0], snake.positions[1], headImage)} - {renderImage(snake.positions[snake.positions.length - 1], snake.positions[snake.positions.length - 2], tailImage, 180)} + {renderSnakeBody()} + {renderImage(snake.positions[0], snake.positions[1], headImage)} + {renderImage( + snake.positions[snake.positions.length - 1], + snake.positions[snake.positions.length - 2], + tailImage, + 180 + )} - ) + ); } -export default Snake \ No newline at end of file +export default Snake; diff --git a/src/components/tournament/Bracket.tsx b/src/components/tournament/Bracket.tsx new file mode 100644 index 00000000..94df8ed7 --- /dev/null +++ b/src/components/tournament/Bracket.tsx @@ -0,0 +1,94 @@ +import { TournamentGame } from "../../constants/messageTypes"; +import { useNavigate } from "react-router-dom"; +import { useAppDispatch, useAppSelector } from "../../context/hooks"; +import { viewedGame } from "../../context/slices/tournamentSlice"; +import { useCallback } from "react"; + +type Props = { + tournamentGame: TournamentGame; + levelIndex: number; +}; + +function TournamentBracket({ tournamentGame, levelIndex }: Props) { + const navigate = useNavigate(); + const dispatch = useAppDispatch(); + const priorLevel = useAppSelector( + (state) => state.tournament.tournamentLevels[levelIndex - 1] + ); + const currentLevel = useAppSelector( + (state) => state.tournament.tournamentLevels[levelIndex] + ); + + function goToGame(tournamentGame: TournamentGame) { + if (tournamentGame.gamePlayed && priorLevelViewed()) { + console.log("Go to tournamentGame:", tournamentGame.gameId); + dispatch(viewedGame(tournamentGame.gameId)); + navigate(`/tournament/${tournamentGame.gameId}`, { + state: { fromTournament: true }, + }); + } else { + alert("You must view the previous round before you can view this game"); + } + } + + const priorLevelViewed = useCallback(() => { + if (levelIndex === 0) return true; + return priorLevel.tournamentGames.every((game) => game.isViewed); + }, [priorLevel, levelIndex]); + + const currentLevelViewed = useCallback(() => { + return currentLevel.tournamentGames.every((game) => game.isViewed); + }, [currentLevel]); + + function RenderPlayer(index: number) { + let player = tournamentGame.players[index]; + if (!player || !priorLevelViewed()) { + return ( +
  • + - TBD - +
  • + ); + } + + if (tournamentGame.gamePlayed && tournamentGame.isViewed) { + let className = ""; + if (player.isWinner) className += "winner "; + if (currentLevelViewed() && !player.isMovedUpInTournament) + className += "looser"; + return ( +
  • + {player.name} {player.isWinner ? null : null} + {player.points + "p"} +
  • + ); + } + + return
  • {player.name}
  • ; + } + + function RenderGameLink() { + if (!priorLevelViewed()) return null; + return ( +
    + +
    + ); + } + + return ( + <> + {[...Array(tournamentGame.expectedNoofPlayers)].map((_, index) => + RenderPlayer(index) + )} + {RenderGameLink()} + + ); +} + +export default TournamentBracket; diff --git a/src/components/LoadingPage.tsx b/src/components/tournament/LoadingPage.tsx similarity index 75% rename from src/components/LoadingPage.tsx rename to src/components/tournament/LoadingPage.tsx index 449913d0..7a8e22c2 100644 --- a/src/components/LoadingPage.tsx +++ b/src/components/tournament/LoadingPage.tsx @@ -1,6 +1,9 @@ -import { getSnakeHead } from '../constants/Images' +import { getSnakeHead } from '../../constants/Images' +import { useAppSelector } from "../../context/hooks"; function LoadingPage() { + const gameFinishedShare = useAppSelector(state => state.tournament.gameFinishedShare); + return (

    preparing the snakepit

    @@ -13,6 +16,7 @@ function LoadingPage() { snakehead snakehead snakehead +

    {Math.round(gameFinishedShare)}%

    ) } diff --git a/src/components/tournament/Schedule.tsx b/src/components/tournament/Schedule.tsx new file mode 100644 index 00000000..f730473d --- /dev/null +++ b/src/components/tournament/Schedule.tsx @@ -0,0 +1,124 @@ +import { TournamentLevel } from '../../constants/messageTypes'; +import { useAppDispatch, useAppSelector } from '../../context/hooks'; +import { declareTournamentWinner } from '../../context/slices/tournamentSlice'; +import TournamentBracket from './Bracket'; +import Podium from '../../assets/images/Podium.svg'; +import api from '../../api'; +import Arbitraryconstants from '../../constants/Arbitraryconstants'; + +function roundClassName(round: TournamentLevel) { + switch (round.level) { + case 0: + return 'first-round'; + case 1: + return 'second-round'; + case 2: + return 'third-round'; + default: + return 'final-round'; + // default to keeping it at the largest size + } +} + +function TournamentSchedule() { + const levels = useAppSelector((state) => state.tournament.tournamentLevels); + const finalGameResult = useAppSelector((state) => state.tournament.finalGameResult); + const finalGameID = useAppSelector((state) => state.tournament.finalGameID); + const declaredWinner = useAppSelector((state) => state.tournament.isWinnerDeclared); + const dispatch = useAppDispatch(); + + function lastGameViewed() { + let viewed = false; + levels.forEach((level) => + level.tournamentGames.forEach((game) => { + if (game.gameId === finalGameID) { + viewed = game.isViewed; + } + }) + ); + return viewed; + } + + function showPodium() { + if (lastGameViewed()) { + if (!declaredWinner) { + let msg = new SpeechSynthesisUtterance(); + msg.volume = Arbitraryconstants.TTS_VOLUME; + + msg.text = 'Congratulations' + finalGameResult[0].name + ', on winning the tournament!'; + speechSynthesis.speak(msg); + } + dispatch(declareTournamentWinner()); + return ( + <> +
    + podium +
    +
    +
    +

    {finalGameResult[2]?.name}

    +
    {finalGameResult[2]?.points} points
    +
    + +
    +

    {finalGameResult[0]?.name}

    +
    {finalGameResult[0]?.points} points
    +
    + +
    +

    {finalGameResult[1]?.name}

    +
    {finalGameResult[1]?.points} points
    +
    +
    + + ); + } + if (declaredWinner) { + let msg = new SpeechSynthesisUtterance(); + msg.volume = Arbitraryconstants.TTS_VOLUME; + + msg.text = 'Congratulations' + finalGameResult[0].name + ', on winning the tournament!'; + speechSynthesis.speak(msg); + } + } + + function lineBracket(index: number, level: TournamentLevel): JSX.Element { + return ( +
    +
    +

    Round {level.level}

    +
    + {level.tournamentGames.map((game, gameIndex) => ( +
      + +
    + ))} +
    +
    + {level.level > 0 ?
    :
    } +
    + ); + } + + function handleNewTournament() { + api.createTournament('Tournament'); + } + + return ( + <> +
    +

    {localStorage.getItem('tourName')}

    + {showPodium()} + +
    + {levels + .slice(0) + .reverse() + .map((level, index) => lineBracket(index, level))} +
    +
    + + ); +} + +export default TournamentSchedule; diff --git a/src/components/tournament/Settings.tsx b/src/components/tournament/Settings.tsx new file mode 100644 index 00000000..64c596e9 --- /dev/null +++ b/src/components/tournament/Settings.tsx @@ -0,0 +1,272 @@ +import { useState, useEffect } from "react"; +import { useAppDispatch, useAppSelector } from "../../context/hooks"; +import { + updateGameSettings, + settingsAreDone, +} from "../../context/slices/tournamentSlice"; + +function TournamentSettings() { + const gameSettings = useAppSelector((state) => state.tournament.gameSettings); + const [localGameSettings, setLocalGameSettings] = useState(gameSettings); + const [tourName, setTourName] = useState("My Tournament"); + const dispatch = useAppDispatch(); + + useEffect(() => { + console.log("Got default tournament settings from socket:", gameSettings); + setLocalGameSettings(gameSettings); + }, [gameSettings]); + + function handleInputChange(e: React.ChangeEvent) { + const { id, value } = e.target; + setLocalGameSettings({ ...localGameSettings, [id]: parseInt(value) }); + } + + function handleSwitchChange(e: React.ChangeEvent) { + const { id, checked } = e.target; + setLocalGameSettings({ ...localGameSettings, [id]: checked }); + } + + function handleNameChange(e: React.ChangeEvent) { + setTourName(e.target.value); + } + + return ( +
    +
    +
    +

    Tournament Settings

    +
    + +
    { + console.log("Form submitted"); + localStorage.setItem("tourName", tourName); + dispatch(updateGameSettings(localGameSettings)); + dispatch(settingsAreDone()); + e.preventDefault(); + }} + > +
    + + +
    + +
    + +
    ? + This setting will determine the amount of players in each game. + The tournament bracket will determine the amount of rounds as a geometric number sequence where + each advancing round will have half the amount of games as the previous one. To avoid cases where some + players automatically advance from the first round, this setting should be set accordingly for the amount + of players in the tournament. +
    + {setLocalGameSettings({...localGameSettings, maxNoofPlayers: parseInt(e.target.value)})}} + /> + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    +
    + + +
    + +
    + + +
    + +
    + + +
    + + + +
    +
    + + +
    +
    +
    +
    + ); +} + +export default TournamentSettings; diff --git a/src/constants/Arbitraryconstants.ts b/src/constants/Arbitraryconstants.ts index 71edead2..feea8c70 100644 --- a/src/constants/Arbitraryconstants.ts +++ b/src/constants/Arbitraryconstants.ts @@ -1,6 +1,9 @@ -export default { +const Arbitraryconstants = { SERVER_URL: window.__RUNTIME_CONFIG__.API_URL, + TTS_VOLUME: 0.5, STARTING_FREQUENCY: 200, FREQUENCY_STEP: 50, MIN_FREQUENCY: 50, -} \ No newline at end of file +}; + +export default Arbitraryconstants; \ No newline at end of file diff --git a/src/constants/BoardUtils.ts b/src/constants/BoardUtils.ts index 02a37804..5f9cc1db 100644 --- a/src/constants/BoardUtils.ts +++ b/src/constants/BoardUtils.ts @@ -7,7 +7,7 @@ const WANTED_SIZE_PX = 1000; export const TILE_SIZE = Math.floor(WANTED_SIZE_PX / MAP_WIDTH); // TILE_SIZE = 21; export const TILE_MARGIN = 2; -export const TILE_OFFSET_X = 1; // To center the tile on the x-axis (Might be a better way to do this) +export const TILE_OFFSET_X = 0; // To center the tile on the x-axis (Might be a better way to do this) export const TILE_OFFSET_Y = 0; // To center the tile on the y-axis (Might be a better way to do this) export const MAP_HEIGHT_PX = MAP_HEIGHT * TILE_SIZE; diff --git a/src/constants/Colors.ts b/src/constants/Colors.ts index 064c186e..93694847 100644 --- a/src/constants/Colors.ts +++ b/src/constants/Colors.ts @@ -13,9 +13,11 @@ const snakeColors = [ '#9BF3F0', ]; -export default { - getSnakeColor(i: number) { - return snakeColors[i]; +const Colors = { + getSnakeColor: (index: number) => { + return snakeColors[index % snakeColors.length]; }, DEAD_SNAKE: '#999999', -}; +} + +export default Colors; diff --git a/src/constants/Images.ts b/src/constants/Images.ts index cfcb1c82..07624f4d 100644 --- a/src/constants/Images.ts +++ b/src/constants/Images.ts @@ -25,7 +25,7 @@ import deadSnakeHead100 from '../assets/snakes/999999/grey-dead-head-100.svg'; import starPath from '../assets/images/star.svg'; import blackholePath from '../assets/images/blackhole.webp'; -import type { SnakeData, TilePosition } from '../context/slices/currentFrameSlice'; +import type { SnakeData } from '../context/slices/currentFrameSlice'; function createImg(src: string) { const img = new Image(); @@ -146,27 +146,4 @@ export function getSnakeTail(color: string) { default: return snakeTails[10]; } -} - -// export function getRotation(firstPosition: TilePosition, secondPosition: TilePosition | undefined) { -// if (secondPosition === undefined) return 0; - -// const xDiff = secondPosition.x - firstPosition.x; -// const yDiff = secondPosition.y - firstPosition.y; -// if(xDiff === 0 && yDiff === 0) return 0; -// else if(xDiff === 0 && yDiff === 1){ -// return 0; -// } else if(xDiff === 1 && yDiff === 1){ -// return Math.PI/4; -// } else if(xDiff === 1 && yDiff === 0){ -// return Math.PI*3/2; -// } else if(xDiff === 0 && yDiff === -1){ -// return Math.PI; -// } else if(xDiff === -1 && yDiff === 0){ -// return Math.PI/2; -// } else { -// console.error('Error in getRotation'); -// return 0; -// } -// } - +} \ No newline at end of file diff --git a/src/constants/TournamentEnums.ts b/src/constants/TournamentEnums.ts new file mode 100644 index 00000000..ada6d5a4 --- /dev/null +++ b/src/constants/TournamentEnums.ts @@ -0,0 +1,8 @@ +export enum TournamentEnums { + LOADINGPAGE = 'LOADINGPAGE', + SETTINGSPAGE = 'SETTINGSPAGE', + PLAYERLIST = 'PLAYERLIST', + SCHEDULE = 'SCHEDULE', +} + +export default TournamentEnums; diff --git a/src/constants/messageTypes.ts b/src/constants/messageTypes.ts index f30ab5e8..89b34e68 100644 --- a/src/constants/messageTypes.ts +++ b/src/constants/messageTypes.ts @@ -1,4 +1,4 @@ -export default{ +const MessageTypes = { GAME_HISTORY: "se.cygni.snake.eventapi.history.GameHistory", GAME_CREATED_EVENT: 'se.cygni.snake.api.event.GameCreatedEvent', GAME_STARTING_EVENT: 'se.cygni.snake.api.event.GameStartingEvent', @@ -17,7 +17,13 @@ export default{ CREATE_TOURNAMENT : 'se.cygni.snake.event.CreateTournament', START_TOURNAMENT : 'se.cygni.snake.event.StartTournament', START_TOURNAMENT_GAME : 'se.cygni.snake.event.StartTournamentGame', -} +}; + +export default MessageTypes; + +// ################################################## +// ########### Typescript message types ############# +// ################################################## export type Message = { gameId: string; diff --git a/src/context/Actions.ts b/src/context/Actions.ts deleted file mode 100644 index 8a2d0b55..00000000 --- a/src/context/Actions.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createAction } from '@reduxjs/toolkit'; -import * as types from '../constants/messageTypes'; - -export default { - mapUpdateEvent: createAction("MAP_UPDATE_EVENT", (message: types.MapUpdateMessage) => ({payload: message})), - gameCreatedEvent: createAction("GAME_CREATED_EVENT", (message: types.GameCreatedMessage) => ({payload: message})), - gameStartingEvent: createAction("GAME_STARTING_EVENT", (message: types.GameStartingEvent) => ({payload: message})), - gameEndedEvent: createAction("GAME_ENDED_EVENT", (message: types.GameEndedMessage) => ({payload: message})), - // gameHistoryEvent: createAction("GAME_HISTORY_EVENT", (message: Message) => ({payload: message})), - snakeDiedEvent: createAction("SNAKE_DEAD_EVENT", (message: types.SnakeDiedMessage) => ({payload: message})), - gameResultEvent: createAction("GAME_RESULT_EVENT", (message: types.GameResultMessage) => ({payload: message})), -} - diff --git a/src/context/messageDispatch.ts b/src/context/messageDispatch.ts index 804d2e94..4b4a4306 100644 --- a/src/context/messageDispatch.ts +++ b/src/context/messageDispatch.ts @@ -1,80 +1,85 @@ import { store } from './store'; -import * as types from '../constants/messageTypes' +import * as types from '../constants/messageTypes'; import messageTypes from '../constants/messageTypes'; -import Actions from './Actions'; -import { nextMessage } from './slices/gameDataSlice'; +import { incrementMessageIndex } from './slices/gameDataSlice'; import { tournamentCreated, setGamePlan, tournamentEnded, setLoggedIn } from './slices/tournamentSlice'; -import api from '../api'; +import { + gameCreatedEvent, + mapUpdateEvent, + snakeDiedEvent, + gameResultEvent, + gameEndedEvent, +} from './slices/currentFrameSlice'; export default function dataDispatch(increaseCounter: boolean = true) { - let index = store.getState().gameData.counter; - const message = store.getState().gameData.messages[index]; - let messageType = message.type; - - console.log("Message dispatched:", messageType, message); - switch(messageType){ - case messageTypes.GAME_CREATED_EVENT: - store.dispatch(Actions.gameCreatedEvent(message as types.GameCreatedMessage)); - break; - - case messageTypes.GAME_STARTING_EVENT: - store.dispatch(Actions.gameStartingEvent(message as types.GameStartingEvent)); - break; - - case messageTypes.MAP_UPDATE_EVENT: - store.dispatch(Actions.mapUpdateEvent(message as types.MapUpdateMessage)); - break; - - case messageTypes.SNAKE_DEAD_EVENT: - store.dispatch(Actions.snakeDiedEvent(message as types.SnakeDiedMessage)); - break; - - case messageTypes.GAME_RESULT_EVENT: - store.dispatch(Actions.gameResultEvent(message as types.GameResultMessage)); - break; - - case messageTypes.GAME_ENDED_EVENT: - store.dispatch(Actions.gameEndedEvent(message as types.GameEndedMessage)); - break; - - default: - console.error("MISSING MESSAGE TYPE", messageType); - break; - } - - if (increaseCounter) store.dispatch(nextMessage()); + let index = store.getState().gameData.counter; + const message = store.getState().gameData.messages[index]; + let messageType = message.type; + + console.log('Message dispatched:', messageType, message); + switch (messageType) { + case messageTypes.GAME_CREATED_EVENT: + store.dispatch(gameCreatedEvent(message as types.GameCreatedMessage)); + break; + + case messageTypes.GAME_STARTING_EVENT: + break; + + case messageTypes.MAP_UPDATE_EVENT: + store.dispatch(mapUpdateEvent(message as types.MapUpdateMessage)); + break; + + case messageTypes.SNAKE_DEAD_EVENT: + store.dispatch(snakeDiedEvent(message as types.SnakeDiedMessage)); + break; + + case messageTypes.GAME_RESULT_EVENT: + store.dispatch(gameResultEvent(message as types.GameResultMessage)); + break; + + case messageTypes.GAME_ENDED_EVENT: + store.dispatch(gameEndedEvent(message as types.GameEndedMessage)); + break; + + default: + console.error('MISSING MESSAGE TYPE', messageType); + break; + } + + if (increaseCounter) store.dispatch(incrementMessageIndex()); } export function onSocketMessage(jsonData: string) { - const message: types.SocketMessage = JSON.parse(jsonData); - console.log("Socket: Message received:", message.type, message); - - switch (message.type) { - case messageTypes.UNAUTHORIZED: - console.error("UNAUTHORIZED"); - localStorage.removeItem("token"); - store.dispatch(setLoggedIn(false)); - // Navigate to loginView - window.location.href = "/login"; - break; - - case messageTypes.TOURNAMENT_CREATED: - store.dispatch(tournamentCreated(message as types.TournamentCreatedMessage)); - break; - - case messageTypes.ACTIVE_GAMES_LIST: - break; - - case messageTypes.TOURNAMENT_GAME_PLAN: - store.dispatch(setGamePlan(message as types.TournamentGamePlanMessage)); - break; - - case messageTypes.TOURNAMENT_ENDED_EVENT: - store.dispatch(tournamentEnded(message as types.TournamentEndedMessage)); - break; - - default: - console.error("Unknown message type:", message.type); - break; - } -} \ No newline at end of file + const message: types.SocketMessage = JSON.parse(jsonData); + console.log('Socket: Message received:', message.type, message); + + switch (message.type) { + case messageTypes.UNAUTHORIZED: + console.error('UNAUTHORIZED'); + localStorage.removeItem('token'); + store.dispatch(setLoggedIn(false)); + // Navigate to loginView + window.location.href = '/login'; + break; + + case messageTypes.TOURNAMENT_CREATED: + console.log('Tournament created:'); + store.dispatch(tournamentCreated(message as types.TournamentCreatedMessage)); + break; + + case messageTypes.ACTIVE_GAMES_LIST: + break; + + case messageTypes.TOURNAMENT_GAME_PLAN: + store.dispatch(setGamePlan(message as types.TournamentGamePlanMessage)); + break; + + case messageTypes.TOURNAMENT_ENDED_EVENT: + store.dispatch(tournamentEnded(message as types.TournamentEndedMessage)); + break; + + default: + console.error('Unknown message type:', message.type); + break; + } +} diff --git a/src/context/slices/currentFrameSlice.ts b/src/context/slices/currentFrameSlice.ts index 4059c1a0..4159b1e6 100644 --- a/src/context/slices/currentFrameSlice.ts +++ b/src/context/slices/currentFrameSlice.ts @@ -1,9 +1,17 @@ import colors from '../../constants/Colors'; -import { createSlice } from '@reduxjs/toolkit'; -import Actions from '../Actions'; -import {convertCoords} from '../../constants/BoardUtils' - -export type TilePosition = { x: number, y: number }; +import { createSlice, Draft, PayloadAction } from '@reduxjs/toolkit'; +import { convertCoords } from '../../constants/BoardUtils'; +import { + GameCreatedMessage, + GameEndedMessage, + GameMap, + GameResultMessage, + MapUpdateMessage, + SnakeDiedMessage, +} from '../../constants/messageTypes'; +import Arbitraryconstants from '../../constants/Arbitraryconstants'; + +export type TilePosition = { x: number; y: number }; export type SnakeData = { name: string; @@ -11,15 +19,15 @@ export type SnakeData = { color: string; positions: TilePosition[]; alive: boolean; -} +}; export type playerRanks = { name: string; -} +}; export type playerPoints = { points: number; -} +}; interface FrameState { IDs: string[]; @@ -27,12 +35,12 @@ interface FrameState { snakesData: { [key: string]: SnakeData; }; - playerRanks: string[], - playerPoints: number [], + playerRanks: string[]; + playerPoints: number[]; obstaclePositions: TilePosition[]; foodPositions: TilePosition[]; - + gameEnded: boolean; } const initialState: FrameState = { @@ -43,81 +51,94 @@ const initialState: FrameState = { playerPoints: [], obstaclePositions: [], foodPositions: [], -} + gameEnded: false, +}; export const snakesSlice = createSlice({ - name: 'snakes', - initialState, - reducers: { - // Clear data - clearCurrentFrame: (state) => { - Object.assign(state, initialState); - } + name: 'snakes', + initialState, + reducers: { + clearCurrentFrame: (state) => { + // Reset state + Object.assign(state, initialState); }, - extraReducers: (builder) => { - builder - .addCase(Actions.gameCreatedEvent, (state, action) => { - // Reset state - Object.assign(state, initialState); - }) - .addCase(Actions.mapUpdateEvent, (state, action) => { - // Initialize snakes - if (Object.keys(state.snakesData).length === 0) { - action.payload.map.snakeInfos.forEach(snake => { - state.IDs.push(snake.id); - state.snakesData = {...state.snakesData, - [snake.id]: - {name: snake.name, - points: snake.points, - color: colors.getSnakeColor(state.colorIndex), - positions: [], - alive: true}}; - - state.colorIndex++; - }); - } - - // Update snake positions, points and alive status - action.payload.map.snakeInfos.forEach(snake => { - state.snakesData[snake.id].positions = snake.positions.map(position => convertCoords(position)); - state.snakesData[snake.id].points = snake.points; - if (snake.positions.length === 0) { - state.snakesData[snake.id].alive = false; - } else { - state.snakesData[snake.id].alive = true; - } - }); - - // Update food positions - state.foodPositions = action.payload.map.foodPositions.map(position => convertCoords(position)); - - // Update obstacle positions - state.obstaclePositions = action.payload.map.obstaclePositions.map(position => convertCoords(position)); - }) - .addCase(Actions.snakeDiedEvent, (state, action) => { - console.log("Snake has died!", action.payload); - state.snakesData[action.payload.playerId].alive = false; - - let msg = new SpeechSynthesisUtterance(); - - msg.text = state.snakesData[action.payload.playerId].name + ' died from ' + action.payload.deathReason; - speechSynthesis.speak(msg); - - }) - .addCase(Actions.gameResultEvent, (state, action) => { - action.payload.playerRanks.forEach(player => { - state.playerRanks.push(player.playerName); - state.playerPoints.push(player.points); - }); - - }) - + gameCreatedEvent: (state, action: PayloadAction) => { + // Reset state + Object.assign(state, initialState); + }, + + mapUpdateEvent: (state, action: PayloadAction) => { + updateMap(state, action.payload.map); + state.gameEnded = false; + }, + + snakeDiedEvent: (state, action: PayloadAction) => { + console.log('Snake has died!', action.payload); + state.snakesData[action.payload.playerId].alive = false; + + let msg = new SpeechSynthesisUtterance(); + msg.volume = Arbitraryconstants.TTS_VOLUME; + + msg.text = `${state.snakesData[action.payload.playerId].name} died from ${action.payload.deathReason}`; + speechSynthesis.speak(msg); + state.gameEnded = false; + }, + + gameResultEvent: (state, action: PayloadAction) => { + action.payload.playerRanks.forEach((player) => { + state.playerRanks.push(player.playerName); + state.playerPoints.push(player.points); + }); + }, + + gameEndedEvent: (state, action: PayloadAction) => { + console.log('Game has ended!'); + updateMap(state, action.payload.map); + state.gameEnded = true; + }, + }, +}); + +function updateMap(state: Draft, map: GameMap) { + // Initialize snakes + if (Object.keys(state.snakesData).length === 0) { + map.snakeInfos.forEach((snake) => { + state.IDs.push(snake.id); + state.snakesData = { + ...state.snakesData, + [snake.id]: { + name: snake.name, + points: snake.points, + color: colors.getSnakeColor(state.colorIndex), + positions: [], + alive: true, + }, + }; + + state.colorIndex++; + }); + } + + // Update snake positions, points and alive status + map.snakeInfos.forEach((snake) => { + state.snakesData[snake.id].positions = snake.positions.map((position) => convertCoords(position)); + state.snakesData[snake.id].points = snake.points; + if (snake.positions.length === 0) { + state.snakesData[snake.id].alive = false; + } else { + state.snakesData[snake.id].alive = true; } - }) - - export const { clearCurrentFrame } = snakesSlice.actions - - export default snakesSlice.reducer + }); + + // Update food positions + state.foodPositions = map.foodPositions.map((position) => convertCoords(position)); + + // Update obstacle positions + state.obstaclePositions = map.obstaclePositions.map((position) => convertCoords(position)); +} +export const { clearCurrentFrame, gameCreatedEvent, mapUpdateEvent, snakeDiedEvent, gameResultEvent, gameEndedEvent } = + snakesSlice.actions; +export default snakesSlice.reducer; diff --git a/src/context/slices/gameDataSlice.ts b/src/context/slices/gameDataSlice.ts index 64be2a11..310d98ab 100644 --- a/src/context/slices/gameDataSlice.ts +++ b/src/context/slices/gameDataSlice.ts @@ -1,53 +1,48 @@ -import { createSlice, createAction, PayloadAction } from '@reduxjs/toolkit'; -import type { Message } from '../../constants/messageTypes'; - -const testAction = createAction('testAction'); +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import type { Message } from "../../constants/messageTypes"; export type GameData = { - gameDate: string; - gameID: string; - messages: Message[]; - playerNames: string[]; - type: string; - counter: number; -} + gameDate: string; + gameID: string; + messages: Message[]; + playerNames: string[]; + counter: number; +}; const initialState: GameData = { - gameDate: "", - gameID: "", - messages: [], - playerNames: [], - type: "", - counter: 0 -} + gameDate: "", + gameID: "", + messages: [], + playerNames: [], + counter: 0, +}; export const gameDataSlice = createSlice({ - name: 'gameData', - initialState, - reducers: { - // Set entire data received from - setGameData: (state, action: PayloadAction) => { - // Reset state - Object.assign(state, initialState); - - state.gameDate = action.payload.gameDate; - state.gameID = action.payload.gameID; - state.messages = action.payload.messages; - state.playerNames = action.payload.playerNames; - state.type = action.payload.type; - state.counter = 0; - }, - - nextMessage: (state) => { - state.counter += 1; - }, - - setCounter: (state, action: PayloadAction) => { - state.counter = action.payload; - } + name: "gameData", + initialState, + reducers: { + // Set entire data received from + setGameData: (state, action: PayloadAction) => { + // Reset state + Object.assign(state, initialState); + + state.gameDate = action.payload.gameDate; + state.gameID = action.payload.gameID; + state.messages = action.payload.messages; + state.playerNames = action.payload.playerNames; + state.counter = 0; + }, + + incrementMessageIndex: (state) => { + state.counter += 1; }, - }); - - export const { setGameData, nextMessage, setCounter } = gameDataSlice.actions - - export default gameDataSlice.reducer \ No newline at end of file + + setMessageIndex: (state, action: PayloadAction) => { + state.counter = action.payload; + }, + }, +}); + +export const { setGameData, incrementMessageIndex, setMessageIndex } = gameDataSlice.actions; + +export default gameDataSlice.reducer; diff --git a/src/context/slices/tournamentSlice.ts b/src/context/slices/tournamentSlice.ts index 1f121be0..a0d08cc0 100644 --- a/src/context/slices/tournamentSlice.ts +++ b/src/context/slices/tournamentSlice.ts @@ -1,154 +1,199 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import api from '../../api'; -import { TournamentCreatedMessage, GameSettings, Message, GameCreatedMessage, TournamentGamePlanMessage, Player, TournamentLevel, ActiveGamesListMessage, TournamentGame, TournamentEndedMessage} from "../../constants/messageTypes"; +import TournamentEnums from '../../constants/TournamentEnums'; +import { + TournamentCreatedMessage, + GameSettings, + TournamentGamePlanMessage, + Player, + TournamentLevel, + TournamentEndedMessage, +} from '../../constants/messageTypes'; const placeholdGameSettings: GameSettings = { - addFoodLikelihood: 0, - foodEnabled: false, - headToTailConsumes: false, - maxNoofPlayers: 0, - noofRoundsTailProtectedAfterNibble: 0, - obstaclesEnabled: false, - pointsPerCausedDeath: 0, - pointsPerFood: 0, - pointsPerLength: 0, - pointsPerNibble: 0, - removeFoodLikelihood: 0, - spontaneousGrowthEveryNWorldTick: 0, - startFood: 0, - startObstacles: 0, - startSnakeLength: 0, - tailConsumeGrows: false, - timeInMsPerTick: 0, - trainingGame: false, -} + addFoodLikelihood: 0, + foodEnabled: false, + headToTailConsumes: false, + maxNoofPlayers: 0, + noofRoundsTailProtectedAfterNibble: 0, + obstaclesEnabled: false, + pointsPerCausedDeath: 0, + pointsPerFood: 0, + pointsPerLength: 0, + pointsPerNibble: 0, + removeFoodLikelihood: 0, + spontaneousGrowthEveryNWorldTick: 0, + startFood: 0, + startObstacles: 0, + startSnakeLength: 0, + tailConsumeGrows: false, + timeInMsPerTick: 0, + trainingGame: false, +}; export type TournamentData = { - gameSettings: GameSettings; - tournamentId: string; - tournamentName: string; - noofLevels: number; - - players: Player[]; - tournamentLevels: TournamentLevel[]; - - isLoggedIn: boolean; - isTournamentActive: boolean; - isTournamentStarted: boolean; - allGamesPlayed: boolean; - - messages: Message[]; - counter: number; -} + gameSettings: GameSettings; + tournamentId: string; + tournamentName: string; + noofLevels: number; + + players: Player[]; + tournamentLevels: TournamentLevel[]; + finalGameID: string; + finalGameResult: { name: string; playerId: string; points: number }[]; + startedGames: { [key: string]: boolean }; + gameFinishedShare: number; + isWinnerDeclared: boolean; + + isLoggedIn: boolean; + isTournamentActive: boolean; + isTournamentStarted: boolean; + + tournamentViewState: TournamentEnums; +}; const initialState: TournamentData = { - // Default game settings - gameSettings: placeholdGameSettings, - tournamentId: "", - tournamentName: "Tournament Not Created", - noofLevels: 0, - - players: [], - tournamentLevels: [], - - isLoggedIn: localStorage.getItem("token") !== null, - isTournamentActive: false, - isTournamentStarted: false, - allGamesPlayed: false, - - messages: [], - counter: 0, -} + gameSettings: placeholdGameSettings, + tournamentId: '', + tournamentName: 'Tournament Not Created', + noofLevels: 0, + + players: [], + tournamentLevels: [], + finalGameID: '', + finalGameResult: [], + startedGames: {}, + gameFinishedShare: 0, + isWinnerDeclared: false, + + isLoggedIn: localStorage.getItem('token') !== null, + isTournamentActive: false, + isTournamentStarted: false, + + tournamentViewState: TournamentEnums.SETTINGSPAGE, +}; export const tournamentSlice = createSlice({ - name: 'gameData', - initialState, - reducers: { - addMessage: (state, action: PayloadAction) => { - state.messages.push(action.payload); - }, - - tournamentCreated: (state, action: PayloadAction) => { - // Clear out old data - Object.assign(state, initialState); - - // Set data from message - state.gameSettings = action.payload.gameSettings; - state.tournamentId = action.payload.tournamentId; - state.tournamentName = action.payload.tournamentName; - - state.isTournamentActive = true; - }, - - updateGameSettings: (state, action: PayloadAction) => { - state.gameSettings = action.payload; - - // TODO: Exception handling, what if message gets lost? - api.updateTournamentSettings(action.payload); - }, - - startTournament: (state) => { - state.isTournamentStarted = true; - }, - - setGamePlan: (state, action: PayloadAction) => { - state.noofLevels = action.payload.noofLevels; - state.players = action.payload.players; - state.tournamentId = action.payload.tournamentId; - state.tournamentLevels = action.payload.tournamentLevels; - state.tournamentName = action.payload.tournamentName; - - // Initialize isViewed for all games - state.tournamentLevels.forEach(level => { - level.tournamentGames.forEach(game => { - if (game.isViewed == undefined) game.isViewed = false; - }); - }); - - if (state.isTournamentStarted) { - // Find and play first game that has not been played - let startedGame = false; - for (let level of state.tournamentLevels) { - for (let game of level.tournamentGames) { - if (game.gameId !== null && !game.gamePlayed) { - api.startTournamentGame(game.gameId); - startedGame = true; - break; - } - } - // If we started a game, we are done - if (startedGame) break; - - } - - } - }, - - viewedGame: (state, action: PayloadAction) => { - if (action.payload == null) return; - for (let level of state.tournamentLevels) { - for (let game of level.tournamentGames) { - if (game.gameId === action.payload) { - game.isViewed = true; - } - } + name: 'tournament', + initialState, + reducers: { + clearTournament: (state) => { + Object.assign(state, initialState); + state.isLoggedIn = localStorage.getItem('token') !== null; + }, + + tournamentCreated: (state, action: PayloadAction) => { + // Clear out old data + Object.assign(state, initialState); + state.isLoggedIn = localStorage.getItem('token') !== null; + + // Set data from message + state.gameSettings = action.payload.gameSettings; + state.tournamentId = action.payload.tournamentId; + state.tournamentName = action.payload.tournamentName; + + state.isTournamentActive = true; + }, + + updateGameSettings: (state, action: PayloadAction) => { + state.gameSettings = action.payload; + api.updateTournamentSettings(action.payload); + }, + + setTournamentName: (state, action) => { + state.tournamentName = action.payload; + }, + + startTournament: (state) => { + state.isTournamentStarted = true; + state.tournamentViewState = TournamentEnums.LOADINGPAGE; + }, + + settingsAreDone: (state) => { + state.tournamentViewState = TournamentEnums.PLAYERLIST; + }, + + editSettings: (state) => { + state.tournamentViewState = TournamentEnums.SETTINGSPAGE; + }, + + declareTournamentWinner: (state) => { + state.isWinnerDeclared = true; + }, + + setGamePlan: (state, action: PayloadAction) => { + state.noofLevels = action.payload.noofLevels; + state.players = action.payload.players; + state.tournamentId = action.payload.tournamentId; + state.tournamentLevels = action.payload.tournamentLevels; + state.tournamentName = action.payload.tournamentName; + + // Initialize isViewed for all games and get amount of games played + let totalGamesPlayed = 0; + state.tournamentLevels.forEach((level) => { + level.tournamentGames.forEach((game) => { + if (game.isViewed === undefined) game.isViewed = false; + if (game.gamePlayed) totalGamesPlayed++; + }); + }); + state.gameFinishedShare = (100 * totalGamesPlayed) / Math.pow(2, state.noofLevels); + + if (state.isTournamentStarted) { + // Find and play games that has not been played + for (let level of state.tournamentLevels) { + let startNextLevel = true; + for (let game of level.tournamentGames) { + // If a game has not been played, don't start the next level + if (!game.gamePlayed) startNextLevel = false; + + if (game.gameId !== null && !state.startedGames[game.gameId]) { + state.startedGames[game.gameId] = true; + api.startTournamentGame(game.gameId); } - }, - - tournamentEnded: (state, action: PayloadAction) => { - state.allGamesPlayed = true; - }, - - setLoggedIn: (state, action: PayloadAction) => { - state.isLoggedIn = action.payload; - // if (state.isLoggedIn === false) { - // // Navigate to loginView - // window.location.href = "/login"; - // } - } + } - }}); - - export const { addMessage, tournamentCreated, updateGameSettings, startTournament, setGamePlan, viewedGame, tournamentEnded, setLoggedIn} = tournamentSlice.actions - - export default tournamentSlice.reducer \ No newline at end of file + // Start next level if all games in this level has been played + if (!startNextLevel) break; + } + } + }, + + viewedGame: (state, action: PayloadAction) => { + if (action.payload == null) return; + for (let level of state.tournamentLevels) { + for (let game of level.tournamentGames) { + if (game.gameId === action.payload) { + game.isViewed = true; + } + } + } + }, + + tournamentEnded: (state, action: PayloadAction) => { + state.tournamentViewState = TournamentEnums.SCHEDULE; + state.finalGameID = action.payload.gameId; + state.finalGameResult = action.payload.gameResult; + }, + + setLoggedIn: (state, action: PayloadAction) => { + state.isLoggedIn = action.payload; + }, + }, +}); + +export const { + clearTournament, + tournamentCreated, + updateGameSettings, + startTournament, + setGamePlan, + viewedGame, + tournamentEnded, + setLoggedIn, + settingsAreDone, + setTournamentName, + editSettings, + declareTournamentWinner, +} = tournamentSlice.actions; + +export default tournamentSlice.reducer; diff --git a/src/stylesheet.scss b/src/stylesheet.scss index a5f574e7..6b3e8572 100644 --- a/src/stylesheet.scss +++ b/src/stylesheet.scss @@ -1,1032 +1,1302 @@ -html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { - margin: 0; - padding: 0; - border: 0; - outline: 0; - font-size: 100%; - vertical-align: baseline; - background: transparent; - } - - body { - line-height: 1; - } - - ol, ul { - list-style: none; +@import url('https://fonts.googleapis.com/css2?family=Barlow+Condensed:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Barlow:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'); + +body { + overscroll-behavior-y: none; + overscroll-behavior-x: none; + min-height: 100vh; +} + +#root { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +section { + flex-grow: 1; +} + +p{ + font-family: Barlow; + font-size: 1em; +} + +label { + font-family: 'Barlow Condensed'; + font-size: 1.0em; + text-align: left; + font-weight: 600; +} + +h1{ + font-family: 'Barlow Condensed'; + font-size: 2.7em; + font-weight: 600; + margin-bottom: 0; +} + +h2{ + font-family: 'Barlow Condensed'; + font-size: 1.7em; + font-weight: 500; +} + +iframe{ + margin-top: 2rem; + margin-bottom: 2rem; +} + +/* +* START PAGE +*/ +.header-image { + width: 100%; + height: 105vh; + background: linear-gradient( rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5) ), url('./assets/images/space.png'); + background-repeat: no-repeat; + background-size: cover; + background-attachment: relative; + background-position: center bottom; + margin-top: -10rem; + + + img{ + width: 15vw; + display: block; + margin-right: auto; + margin-left: auto; } - - blockquote, q { - quotes: none; + + .header-content{ + margin-left: auto; + margin-right: auto; + padding-top: 32vh; + width: 30vw; } - - blockquote:before, blockquote:after, q:before, q:after { - content: ''; - content: none; + + h1 { + color: white; + font-size: 5em; + line-height: 1em; + text-align: center; + font-weight: 600; + padding-bottom: 2rem; } - - :focus { - outline: 0; +} + +.welcome { + margin-left: 20%; + margin-right: 20%; + padding-top: 5rem; + padding-bottom: 5rem; + + h2{ + margin-top: 3rem; } - - ins { - text-decoration: none; + + li { + font-family: Barlow; + font-size: 1em; } - - del { - text-decoration: line-through; +} + +.getting-started{ + background-color: #000735; + padding-left: 20%; + padding-right: 20%; + padding-top: 5rem; + padding-bottom: 5rem; + + p { + color: white; } - - table { - border-collapse: collapse; - border-spacing: 0; + + h1{ + color: white; } - - /* Hesselbom.se CSS e1e3e4 */ - .clear-fix:after { - clear: both; - content: ""; - display: block; - height: 0; - visibility: hidden; + + h2{ + color: white; + padding-top: 2rem; } - - $footer-height: 80px; - $gameboard-width: 1024px; - - html { - position: relative; - min-height: 100%; + + a { + color: white; } - - * { - box-sizing: border-box; +} + +.cygni-at-campus{ + padding-left: 20%; + padding-right: 20%; + padding-top: 8rem; + display: flex; + justify-content: space-between; +} + + +/* +* HEADER +*/ +header { + padding: 2rem 5rem 2rem; + background-color: #000735; + z-index: 1; + + ul { + list-style-type: none; + margin: 0; } - - body { - background-color: #141631; - font: 100%/1.3125rem 'Open Sans', sans-serif; - color: #333; - } - - .page { - background-color: #ebebeb; - padding-bottom: 4rem; - } - - .half { - margin-left: 200px; - max-width: 70%; + + li { + padding: 0 1rem; float: left; } - - .third { - max-width: 30%; + + nav { float: right; + margin-top: 2rem; + + a { + color: rgba(255, 255, 255, 0.7); + text-decoration: none; + font-size: 16px; + border-bottom: none; + transition: all 0.1s; + + &:hover { + color: rgba(255, 255, 255, 1); + text-decoration: none; + font-size: 16px; + border-bottom: 3px solid rgba(255, 255, 255, 1); + transition: all 0.1s; + } + + &.selected { + color: rgba(255, 255, 255, 1); + border-bottom: 3px solid rgba(255, 255, 255, 1); + } + } } - + img { - border: none; - width: 100%; + width: 200px; } - - /* Headlines and paragraphs */ - - h1 { - font-size: 42px; - line-height: 2.5rem; - margin-bottom: 2rem; - font-weight: 300; +} + +footer { + padding: 2rem 5rem 2rem; + background-color: #e3d9d7; + height: 7rem; + + img { + width: 150px; + float: left; + vertical-align: middle; } - - h2 { - font-size: 24px; - line-height: 1.8rem; - margin-bottom: 1rem; - font-weight: 400; + + p { + float: right; + font-size: 12px; + color: rgba(255, 255, 255, 0.7); } - - h3 { - font-size: 20px; - line-height: 1.5rem; - margin-bottom: 1rem; - font-weight: 400; +} + +.navbar-startpage{ + background-color: transparent; +} + +.loginview { + margin-left: 20%; + margin-right: 20%; + padding-bottom: 5rem; + + h1{ + text-align: center; + padding-top: 5rem; } - - h4 { - font-size: 18px; - margin-bottom: 0.5rem; - font-weight: 700; + + .logintext { + text-align: center; } - - h5 { - font-size: 16px; - margin-bottom: 0.5rem; - font-weight: 700; + + input{ + width: 20vw; + margin-left: auto; + padding-right: auto; + margin-right: auto; + display: block; + box-sizing: border-box; + + width: 100%; + padding: 12px 20px; + border-radius: 6px; + border: 1px solid #DCDCDC; + margin-bottom: 1rem; + margin-top: 0.5rem; } - - h6 { - font-size: 16px; - margin-bottom: 0.5rem; - font-weight: 700; - font-style: italic; + + .signInBtn { + width: 200px; + height: auto; + margin-top: 2rem; + color: white; + background-color: #0EBDE7; + border-radius: 6px; + border-color: white; + margin-right: auto; + margin-left: auto; + display: block; + + padding: 12px 20px; + + cursor: pointer; + + &:hover { + border-color: #0EBDE7; + } + + &:active { + transform: translateY(2px); + } } - - p { - margin-bottom: 2rem; + + .error{ + border-color: red !important; } - - a { - color: #1c5a97; - text-decoration: underline; + + .error-text{ + color: red; + font-size: 12px; } - - a:hover { - color: #eb6d1e; - } - - header { - border-bottom: 1px solid #000; - - ul { - list-style-type: none; - margin: 0; - } - - li { - padding: 0 1rem; - float: left; - } - - nav { - float: right; - margin-top: 2rem; - - a { - color: rgba(255, 255, 255, 0.7); - text-decoration: none; - font-size: 16px; - border-bottom: none; - transition: all 0.1s; - - &:hover { - color: rgba(255, 255, 255, 1); - text-decoration: none; - font-size: 16px; - border-bottom: 3px solid rgba(255, 255, 255, 1); - transition: all 0.1s; - } - - &.selected { - color: rgba(255, 255, 255, 1); - border-bottom: 3px solid rgba(255, 255, 255, 1); - } - } - } - - img { - width: 200px; - } + + span{ + font-size: 10px; + vertical-align: middle; + margin-right: 3px; + } +} + +.podium { + display: flex; + justify-content: center; + align-items: center; + margin-top: 10rem; +} + +.gameView { + margin-left: 20%; + margin-right: 20%; + padding-top: 5rem; + // padding-bottom: 5rem; + text-align: center; + + input { + width: 20vw; + padding: 12px 20px; + border-radius: 6px; + margin-top: 30px; + border: 1px solid #DCDCDC; } - - footer { - border-top: 1px solid #000; - - img { - width: 100px; - float: left; + + .searchbtn { + width: 100px; + margin-left: 1rem; + color: white; + background-color: #0EBDE7; + border-color: white; + border-radius: 6px; + //border: none; + cursor: pointer; + + &:hover { + border-color: #0EBDE7; } - - p { - float: right; - font-size: 12px; - color: rgba(255, 255, 255, 0.7); + + &:active { + transform: translateY(2px); } + } - - header, footer { - padding: 2rem 2rem 1rem; - - } - - section { - width: 100%; - padding: 2rem; - margin: auto; + + .searchintro { + margin-top: 0; + margin-bottom: 2rem; } - - article { - max-width: 960px; - margin: auto; - padding: 2rem 3rem; + + .searchresults { + display: inline-block; + list-style: none; + padding: 0; } - - .text-content { - column-count: 1; - column-gap: 4rem; + + .searchresults li { + margin-bottom: 1.2rem; } - - ul { - list-style: outside; - padding: 0 0 0 1.2rem; + + .searchheadline { margin: 0; + font-size: 16px; } - - li { - margin: 0 0 0.5rem 0; - } - - .box { - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.3); - background-color: #fff; - padding: 1.5rem; - border-radius: 4px; + + .searchheadline a { + text-decoration: none; } - - /* The game */ - .thegame { - width: 1264px; - margin: auto; + + .time { + margin-left: 0.5rem; } - - /* Player list */ - .activePlayers { - padding: 0 0.5rem 0.5rem 0.5rem; - float: left; + + .players { font-size: 14px; - width: 220px; - - figure { - display: inline-block; - margin: 0; - padding: 4px 0 0 4px; - width: 24px; - height: 24px; - background-color: #141631; - margin: 5px 5px 0 0; - border-radius: 3px; - - img { - width: 16px; - } - } - - ul { - padding: 0; - margin: 0; - - li { - margin-top: 5px; - border-top: 1px solid #ebebeb; - padding-top: 5px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - &:first-child { - border-top: none; - } - } - } - - strong { - margin: 0 0.2rem; - } - } - - /* Tournament */ - .flex { - display: flex; - flex-direction: row; - flex-wrap: nowrap; - justify-content: center; - align-items: baseline; - align-content: space-between; - } - - .round-box { - border-radius: 8px; - margin: 0; - padding: 1rem 2rem 2rem; - margin: auto; - - h2 { - font-size: 18px; - text-align: center; - font-weight: 300; - text-transform: uppercase; - } - } - - .first-round { - background-color: #ddd; - width: 100%; - min-width: 1260px; - } - - .second-round { - background-color: #ddd; - width: 80%; - min-width: 650px; - } - - .third-round { - background-color: #ddd; - width: 60%; - min-width: 410px; - } - - .final-round { - background-color: #ddd; - width: 40%; - min-width: 240px; - } - - .game { - display: inline-block; list-style: none; padding: 0; - //width: 200px; - margin: 0 1rem; - overflow: auto; - - li { - background-color: #fff; - padding: 5px 10px; - margin: 0 0 3px 0; - font-size: 14px; - border-radius: 6px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - &:hover { - background-color: #f7f7e9; - } - } } - - .spacer { - background-color: #ddd; - height: 20px; - width: 3px; - margin: auto; + + .players li { + display: inline-block; + margin: 0 0 0 1rem; + } - - .thefinalwinner { - display: block; - width: 400px; - margin: auto; - margin-bottom: 3rem; - - li { - font-size: 24px; - font-weight: bold; - text-align: center; - text-transform: uppercase; - background-color: gold; - padding: 1rem; - - &:hover { - background-color: gold; - } - } + + .players li:first-child { + margin: 0; } - - li { - &.looser { - background-color: #eee; - color: #999; - } - - .gotogame { - background-color: transparent; - text-align: center; - font-size: 12px; - font-weight: bold; - padding: 0; - } - - .gotogame:hover { - background-color: transparent; - } - + + .match { + font-style: italic; + color: #439335; } - - .points { - display: inline-block; - width: 24px; + + .searchresultsheadline { + margin-top: 1rem; + font-size: 18px; font-weight: bold; } - - .livedlongest { - width: 16px; - height: 16px; - float: right; - margin-top: 3px; + + .searchintro { + margin-bottom: 1rem; } - - /* Gameboard with controls */ - .gameboard { - ///images/backgrounds/space.png'); - background-image: url('./assets/images/space.png'); - background-repeat: no-repeat; - background-size: cover; - width: 1024px; - float: right; - // padding-top: 20px; + + .viewgame { + color: #000735; + display: inline-block; + text-decoration-line: underline; + margin-left: 10px; } - - .canvas { - background: url('./assets/images/game.svg'); - background-repeat: no-repeat; - background-size: cover 100%; - // height: 100%; - // width: 100%; - // padding-left: 0; - // padding-right: 0; - // margin-bottom: 20px; - margin: 20px; - // margin-right: 20px; - display: block; + + .date { + margin-right: 1rem; } - - .controlpanel { - width: 100%; - padding: 0 1rem; - + + .game-played{ + font-weight: 600; + display: inline-block; + margin-right: 1rem; } - - .controlbuttons { - width: 99px; + +} + +.hide{ + display: none; +} + +.no-results{ + width: 200px; + margin-left: auto; + margin-right: auto; + margin-top: 40px; + + h2{ + margin-bottom: 0; + margin-top: 5px; } - - .playButton, .forwardButton, .backwardsButton { - height: 26px; - width: 26px; - margin: 10px 2px 3px; - background-color: #141631; - border: 1px solid #333; - padding: 6px; - border-radius: 3px; - transition: all 0.1s; + + p{ + margin-top: 10px; } - - .playButton:hover, .forwardButton:hover, .backwardsButton:hover { - background-color: #06b6ee; - transition: all 0.2s; +} + +.searchH1, .loginH1, .bracketH1 { + margin-top: 5rem; +} + +.tournamentsettings { + margin-left: 20%; + margin-right: 20%; + padding-top: 2rem; //avstånd mellan formulär och rubrik + padding-bottom: 5rem; + + input { + width: 30vw; + margin-left: auto; + padding-right: auto; + margin-right: auto; + display: block; + box-sizing: border-box; + + // width: 100%; + padding: 12px 20px; + border-radius: 6px; + border: 1px solid #DCDCDC; + margin-bottom: 1rem; + margin-top: 0.5rem; } - - /* Range CSS */ - - input[type=range] { - -webkit-appearance: none; - width: 870px; - height: 16px; - margin: 14px 0px; - float: right; - border-radius: 20px; - background: #ccc; - } - - input[type=range]:focus { - outline: none; - } - - input[type=range]::-webkit-slider-runnable-track { - width: 100%; - height: 16px; - cursor: pointer; - background: #cccccc; - border-radius: 16px; - border: none; + + label { + width: 30vw; + margin-left: auto; + padding-right: auto; + margin-right: auto; + display: block; + box-sizing: border-box; + + // width: 100%; } - - input[type=range]::-webkit-slider-thumb { - border: none; - height: 16px; - width: 16px; - border-radius: 16px; - background: #141631; - cursor: pointer; - -webkit-appearance: none; - margin-top: 0px; + + h1 { + text-align: center; } - - input[type=range]:focus::-webkit-slider-runnable-track { - background: #ccc; + + + .tournament-name { + padding-bottom: 1em; } - - input[type=range]::-moz-range-track { - width: 100%; - height: 16px; - cursor: pointer; - background: #ccc; - border-radius: 16px; - border: none; + + .maxnoofplayers { + margin-left: 17px; + } - - input[type=range]::-moz-range-thumb { - border: none; - height: 16px; - width: 16px; - border-radius: 16px; - background: #141631; + + .resetDefault { + height: 50px; + width: 200px; + margin-right: 1rem; + color: white; + background-color: #000735; + border-color: white; + border-radius: 6px; cursor: pointer; + + &:hover { + border-color: #000735; + } + + &:active { + transform: translateY(2px); + } } - - input[type=range]::-ms-track { - width: 100%; - height: 16px; + + .createTournamentBtn { + height: 50px; + width: 200px; + margin-left: 1rem; + color: white; + background-color: #0EBDE7; + border-color: white; + border-radius: 6px; cursor: pointer; - background: transparent; - border-color: transparent; - color: transparent; - } - - input[type=range]::-ms-fill-lower { - background: #ccc; - border: none; - border-radius: 32px; - } - - input[type=range]::-ms-fill-upper { - background: #ccc; - border: none; - border-radius: 32px; + + &:hover { + border-color: #0EBDE7; + } + + &:active { + transform: translateY(2px); + } } - - input[type=range]::-ms-thumb { - border: none; - width: 16px; - border-radius: 16px; - background: #141631; - cursor: pointer; - height: 16px; + + .tournamentButtons { + margin-top: 4rem; + margin-left: auto; + margin-right: auto; + text-align: center; } - - input[type=range]:focus::-ms-fill-lower { - background: #cccccc; +} + +.checkboxes { + width: 30vw; + margin-left: auto; + margin-right: auto; + display: block; + margin-top: 3rem; + + .foodLabel, .headLabel, .tailLabel, .obstacleLabel { + box-sizing: border-box; + margin: 0; + width: auto; + } + + .obstacles, .food, .head { + margin-bottom: 2rem; } - - input[type=range]:focus::-ms-fill-upper { - background: #ccc; + + label{ + display: inline-block; } - - /* Forms */ - label { + + .switch { + position: relative; display: inline-block; - margin-bottom: 0.25rem; - } - - input[type=text], - input[type=password], - input[type=url], - input[type=tel], - input[type=number], - input[type=email], - textarea, - select { - display: block; - font-family: 'Open Sans', sans-serif; - font-size: 16px; - line-height: 1.2rem; - background-color: #fff; - border: none; - border: 1px solid #313131; - width: 440px; - transition: all 0.1s; - margin-bottom: 1rem; - padding: 0.4rem; - border-radius: 3px; - } - - textarea { - height: 80px; - } - - input[type=text]:hover, - input[type=password]:hover, - input[type=url]:hover, - input[type=tel]:hover, - input[type=number]:hover, - input[type=email]:hover, - textarea:hover { - background-color: #f1f8ff; - transition: all 0.2s; + width: 30px; + height: 17px; + float: right; + margin-right: 200px; } - - input[type=text]:focus, - input[type=password]:focus, - input[type=url]:focus, - input[type=tel]:focus, - input[type=number]:focus, - input[type=email]:focus, - textarea:focus { - background-color: #f8f8f5; - } - - select { - -webkit-appearance: none; - -webkit-border-radius: 0px; - appearance: none; - // background-image: url('../images/icons/arrow.svg'); - background-repeat: no-repeat; - background-position: right 10px center; - background-size: 12px; - border-radius: 3px; + + /* Hide default HTML checkbox */ + .switch input { + opacity: 0; + width: 0; + height: 0; } - - input[type=submit], input[type=reset], button { - border: none; - font-size: 16px; - border-radius: 3px; - line-height: 1rem; - font-family: 'Open Sans', sans-serif; - padding: 10px 20px 8px; + + /* The slider */ + .slider { + position: absolute; cursor: pointer; - transition: all 0.1s; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + -webkit-transition: .4s; + transition: .4s; } - - input[type=submit] { - color: #fff; - background: #459335; - border-bottom: 2px solid #459335; + + .slider:before { + position: absolute; + content: ""; + height: 13px; + width: 13px; + left: 2px; + bottom: 2px; + background-color: white; + -webkit-transition: .4s; + transition: .4s; } - - input[type=submit]:hover, input[type=submit]:focus { - background: #4ea43c; - border-bottom: 2px solid #326B24; - transition: all 0.2s; + + input:checked + .slider { + background-color: #000735; } - - input[type=reset] { - color: #313131; - background: #ccc; - border-bottom: 2px solid #ccc; + + input:focus + .slider { + box-shadow: 0 0 1px #000735; } - - input[type=reset]:hover, input[type=reset]:focus { - background: #d6d6d6; - border-bottom: 2px solid #999; - transition: all 0.2s; + + input:checked + .slider:before { + -webkit-transform: translateX(13px); + -ms-transform: translateX(13px); + transform: translateX(13px); } - - .searchfield { - float: left; + + /* Rounded sliders */ + .slider.round { + border-radius: 34px; } - - .searchbtn { - margin-left: 1rem; + + .slider.round:before { + border-radius: 50%; } - - button { - color: #fff; - background: #1c5a97; - border-bottom: 2px solid #1c5a97; - - &.button-link { - background: none !important; - border: none !important; - padding: 0 !important; - font: inherit; - border-bottom: 0; - cursor: pointer; - - color: #1c5a97; - text-decoration: underline; - - &:hover { - color: #eb6d1e; - } +} + +/*Player List Page*/ + +.playersList { + + width: 30vw; + margin-left: auto; + padding-right: auto; + margin-right: auto; + display: block; + box-sizing: border-box; + + // width: 100%; + padding: 12px 20px; + border-radius: 6px; + margin-bottom: 4rem; + margin-top: 0.5rem; + + .playerString{ + font-family: 'Barlow Condensed'; + font-size: 2em; + font-weight: 500; + text-align: center; } - } - - .gotogame { - text-align: center; - font-size: 12px; - font-weight: bold; - padding: 0; - } - - button:hover, button:focus { - background: #3b7abc; - border-bottom: 2px solid #1c5a97; - transition: all 0.2s; - } - - .formlist { - margin-bottom: 1rem; - } - - .formlist li { - margin-bottom: 0.5rem; - } - - /* Search results */ - .searchresults { - list-style: none; - padding: 0; - } - - .searchresults li { - margin-bottom: 1.2rem; - } - - .searchheadline { - margin: 0; - font-size: 16px; - } - - .searchheadline a { - text-decoration: none; - } - - .time { + + p{ + font-family: Barlow; + font-size: 1em; + text-align: center; + } +} + +.playerlistBtns { + margin-right: auto; + margin-left: auto; + display: block; + text-align: center; +} + +.playerListH1 { + padding-top: 5rem; + text-align: center; +} + +.playerInfo { + text-align: center; +} + +.createTournamentButton { + width: 200px; + margin-left: auto; + padding-right: auto; + margin-right: auto; + color: white; + background-color: #0EBDE7; + padding: 12px 20px; + border-radius: 6px; + border-color: white; + margin-bottom: 7rem; + margin-top: 0.5rem; margin-left: 0.5rem; + cursor: pointer; + + &:hover { + border-color: #0EBDE7; + } + + &:active { + transform: translateY(2px); + } +} + +.editSettingsButton { + width: 200px; + margin-left: auto; + padding-right: auto; + margin-right: auto; + color: white; + background-color: #000735; + padding: 12px 20px; + border-radius: 6px; + border-color: white; + margin-bottom: 7rem; + margin-top: 0.5rem; + margin-right: 0.5rem; + cursor: pointer; + + &:hover { + border-color: #000735; + } - - .players { - font-size: 14px; - list-style: none; - padding: 0; - } - - .players li { - display: inline-block; - margin: 0 0 0 1rem; - - } - - .players li:first-child { - margin: 0; - } - - .match { - font-style: italic; - color: #439335; + + &:active { + transform: translateY(2px); } - - .searchresultsheadline { + +} + + +/* The game */ +.thegame { + margin: 3rem auto; + width: auto; + display: table; + + .tourGameBtns { margin-top: 1rem; - font-size: 18px; - font-weight: bold; + margin-left: 69.5%; } - - .searchintro { - margin-bottom: 1rem; + + .nextBtn { + width: 180px; + height: auto; + color: white; + background-color: #0EBDE7; + border-radius: 6px; + border-color: white; + display: inline-block; + padding: 12px 20px; + margin: 0 10px; + + cursor: pointer; + + &:hover { + border-color: #0EBDE7; + } + + &:active { + transform: translateY(2px); + } } - - /* Arena */ - .arena-history-table th,td { - padding-left: 1rem; - padding-right: 1rem; - } - /* Modal */ - .darkBG { - background-color: rgba(0, 0, 0, 0.2); - width: 100vw; - height: 100vh; - z-index: 0; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - position: absolute; + + .scheduleBtn { + width: 180px; + height: auto; + color: white; + background-color: #000735; + border-radius: 6px; + border-color: white; + display: inline-block; + padding: 12px 20px; + margin: 0 10px; + + cursor: pointer; + + &:hover { + border-color: #000735; + } + + &:active { + transform: translateY(2px); + } } - - .centered { - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - } - - .modal { - width: 700px; - height: 700px; - background-image: url('./assets/images/podium.png'); - background-position: center; - background-size: cover; + /* Gameboard with controls */ + .gameboard { + background-image: url('./assets/images/space.png'); background-repeat: no-repeat; - z-index: 10; - border-radius: 16px; - box-shadow: 0 5px 20px 0 rgba(0, 0, 0, 0.04); - } - - .modalHeader { - height: 50px; - background: white; - overflow: hidden; - border-top-left-radius: 16px; - border-top-right-radius: 16px; + background-size: cover; + // padding: 1rem; // Border around canvas + display: inline-block; + border-radius: 20px; } - .imageTextP1 { - - position: absolute; - right: 40%; - left: 44%; - bottom: 6%; + .canvas { + background: url('./assets/images/game.svg'); + background-repeat: no-repeat; + background-size: cover 100%; + display: block; + margin: 20px; + margin-bottom: 10px; } - .pointsTextP1 { - - position: absolute; - right: 40%; - left: 49%; - bottom: 20%; - } + /* Controls */ + .controlpanel { + background-color: #fff; + // border-radius: 4px; + border-radius: 10px; + padding: 2px 1rem; - .imageTextP2 { - position: absolute; - right: 40%; - left: 20%; - bottom: 9%; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.3); + background-color: #fff; + // padding: 2px; } - .pointsTextP2 { - - position: absolute; - right: 40%; - left: 22%; - bottom: 17%; + .playButton, .forwardButton, .backwardsButton { + background-color: #141631; + // border: 1px solid #333; + border-radius: 4px; + height: 26px; + margin: 10px 2px 3px; + padding: 6px; + box-sizing: border-box; + transition: all 0.1s; + width: 26px; } - .imageTextP3 { - position: absolute; - right: 40%; - left: 73%; - bottom: 9%; + .playButton:hover, .forwardButton:hover, .backwardsButton:hover { + background-color: #06b6ee; + transition: all 0.2s; } - .pointsTextP3 { - - position: absolute; - right: 40%; - left: 73%; - bottom: 17%; - } - - .heading { - margin: 0; - padding: 10px; - color: #2c3e50; - font-weight: 500; - font-size: 18px; - text-align: center; - } - - .modalContent { - padding: 10px; - font-size: 14px; - color: #2c3e50; - text-align: center; + input[type=range] { + -webkit-appearance: none; + width: 870px; + height: 16px; + margin: 14px 0px; + float: right; + border-radius: 20px; + background: #ccc; + + outline: none; } - - .modalActions { - position: absolute; - bottom: 2px; - margin-bottom: 10px; - width: 100%; +} + +.newTournamentButton { + margin-left: auto; + padding-right: auto; + margin-right: auto; + color: white; + background-color: #0EBDE7; + padding: 12px 20px; + border-radius: 6px; + border-color: white; + cursor: pointer; + + &:hover { + border-color: #0EBDE7; } - - .actionsContainer { - display: flex; - justify-content: space-around; - align-items: center; + + &:active { + transform: translateY(2px); } - - .closeBtn { - cursor: pointer; - font-weight: 500; - padding: 4px 8px; - border-radius: 8px; - border: none; - font-size: 18px; - color: #2c3e50; - background: white; - transition: all 0.25s ease; - box-shadow: 0 5px 20px 0 rgba(0, 0, 0, 0.06); - position: absolute; - right: 0; - top: 0; - align-self: flex-end; - margin-top: -7px; - margin-right: -7px; +} + + +/** +* Scoreboard +*/ +.activePlayers { + padding: 0 0.5rem 0.5rem 0.5rem; + margin-right: 2rem; + float: left; + font-size: 14px; + width: 310px; + + h2 { + margin: 0 0.5rem 1rem; } - - .closeBtn:hover { - box-shadow: 0 5px 20px 0 rgba(0, 0, 0, 0.04); - transform: translate(-4px, 4px); + + .leaderboard{ + border: 1px solid rgb(223,223,223); + border-radius: 10px; + text-align: left; + padding-right: 1em; + padding-left: 1em; } - - .deleteBtn { - margin-top: 10px; - cursor: pointer; - font-weight: 500; - padding: 11px 28px; - border-radius: 12px; - font-size: 0.8rem; - border: none; - color: #fff; - background: #ff3e4e; - transition: all 0.25s ease; - } - - .deleteBtn:hover { - box-shadow: 0 10px 20px -10px rgba(255, 62, 78, 0.6); - transform: translateY(-5px); - background: #ff3e4e; - } - - .cancelBtn { - margin-top: 10px; - cursor: pointer; - font-weight: 500; - padding: 11px 28px; - border-radius: 12px; - font-size: 0.8rem; - border: none; - color: #2c3e50; - background: #fcfcfc; - transition: all 0.25s ease; + + ul { + padding: 0; + margin-top: 0; + margin-bottom: 0; + margin-right: auto; + margin-left: auto; + display: block; + list-style: none; + + li { + border-top: 1px solid rgb(223,223,223); + padding-top: 1em; + padding-bottom: 1em; + //margin: 1rem; + overflow: hidden; + display: grid; + grid-template-columns: 1fr 3fr; + + .list-content{ + //vertical-align: middle; + display: table; + + } + + &:first-child { + border-top: none; + } + + p { + font-weight: 600; + display: inline-block; + vertical-align: middle; + margin-top: auto; + margin-bottom: auto; + display: table-cell; + text-align: center; + vertical-align: middle; + } + + img { + float: left; + width: 1.25rem; + border-radius: 1.25rem; + padding: 1rem; + } + + .points { + color: rgb(223,223,223); + width: auto; + padding-right: 20px; + text-align: right; + } + } } - - .cancelBtn:hover { - box-shadow: none; - transform: none; - background: whitesmoke; +} + + + + + + + + + + + + + + + + + + + +/* Modal */ + +.darkBG { + background-color: rgba(0, 0, 0, 0.2); + width: 100vw; + height: 100vh; + z-index: 0 !important; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + position: fixed; +} + + .centered { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + +.modal { + width: 900px; + height: 500px; + background-color: white; + z-index: 10 !important; + border-radius: 16px; + box-shadow: 0 5px 20px 0 rgba(0, 0, 0, 0.04); + padding-top: 2rem; + + img{ + margin-left: auto; + margin-right: auto; + display: block; + width: 50%; } - .loading { - background-color: #141631; + h1{ text-align: center; - padding: 20%; + margin-bottom: 2rem; } +} + +.players{ + margin-left: auto; + margin-right: auto; + display: block; + text-align: center; - .loading span { + .player{ display: inline-block; - vertical-align: middle; - width: 1.5em; - height: 1.5em; - margin: .19em; - border-radius: .6em; - animation: loading 1s infinite alternate; - } + margin: 0px 50px; + text-align: center; - .loading h2 { - color: #ccc; - margin: 0; - font: 1em verdana; - text-transform: uppercase; - letter-spacing: .1em; - margin-bottom: 10px; - } - - /* - * Loading animation - */ - .loading span:nth-of-type(2) { - animation-delay: 0.2s; + h3{ + font-family: 'Barlow Condensed'; + font-size: 1.2em; + font-weight: 500; + margin-bottom: 5px; + } + + h5{ + font-family: 'Barlow Condensed'; + color: #b9b9b9; + font-size: 1em; + font-weight: 400; + margin-top: 0; + } } - .loading span:nth-of-type(3) { - animation-delay: 0.4s; +} + +.buttons{ + margin-right: auto; + margin-left: auto; + display: block; + text-align: center; + + button{ + width: 180px; + height: auto; + margin-top: 2rem; + color: white; + background-color: #0EBDE7; + border-radius: 6px; + border: none; + display: inline-block; + padding: 12px 20px; + margin: 0 10px; } - .loading span:nth-of-type(4) { - animation-delay: 0.6s; + + button:nth-child(1) { + background: #141631; } - .loading span:nth-of-type(5) { - animation-delay: 0.8s; +} + + +.closeBtn { + cursor: pointer; + border: none; + background-color: transparent; + color: #b9b9b9; + position: absolute; + right: 0; + top: 0; + align-self: flex-end; + margin-top: 30px; + margin-right: 30px; + + p{ + margin: 0; + font-weight: 600; + font-size: 30px; } - .loading span:nth-of-type(6) { - animation-delay: 1.0s; +} + +.loading { + background-color: #000735; + text-align: center; + height: 1500px; + padding: 20%; +} + +.loading img { + width: 20px; + height: 20px; +} + +.loading span { + display: inline-block; + vertical-align: middle; + width: 1.5em; + height: 1.5em; + margin: .19em; + border-radius: .6em; + animation: loading 1s infinite alternate; +} + +.loading h1 { + margin-top: 3rem; + color: #ccc; +} + +.loading h2 { + color: #ccc; + margin: 0; + font: 1em verdana; + text-transform: uppercase; + letter-spacing: .1em; + margin-bottom: 10px; +} + +/* + * Loading animation + */ +.loading span:nth-of-type(2) { + animation-delay: 0.2s; +} +.loading span:nth-of-type(3) { + animation-delay: 0.4s; +} +.loading span:nth-of-type(4) { + animation-delay: 0.6s; +} +.loading span:nth-of-type(5) { + animation-delay: 0.8s; +} +.loading span:nth-of-type(6) { + animation-delay: 1.0s; +} +.loading span:nth-of-type(7) { + animation-delay: 1.2s; +} +.loading span:nth-of-type(8) { + animation-delay: 1.4s; +} +.loading span:nth-of-type(9) { + animation-delay: 1.6s; +} +.loading span:nth-of-type(10) { + animation-delay: 1.8s; +} + + + + +/* + * Animation keyframes + * Use transition opacity instead of keyframes? + */ +@keyframes loading { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +.flex { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: center; + align-items: baseline; + align-content: space-between; +} + +.round-box { +border-radius: 8px; +margin: 0; +padding: 1rem 2rem 2rem; +margin: auto; +text-align: center; + +h2 { + font-size: 30px; + font-weight: 400; + color: #DCDCDC; +} +} + +.game { +display: inline-block; +list-style: none; +padding: 0; +//width: 200px; +margin: 0 1rem; +overflow: auto; + +li { + background-color: #F5F5F5; + padding: 10px 15px; + margin: 0 0 7px 0; + font-size: 14px; + border-radius: 6px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-align: center; + + &:hover { + background-color: #f7f7e9; } - .loading span:nth-of-type(7) { - animation-delay: 1.2s; +} +} + +.spacer { +background-color: #ddd; +height: 2px; +width: 80%; +margin: auto; +} + +.thefinalwinner { +display: block; +width: 400px; +margin: auto; +margin-bottom: 3rem; + +li { + font-size: 24px; + font-weight: bold; + text-align: center; + text-transform: uppercase; + background-color: gold; + padding: 1rem; + + &:hover { + background-color: gold; } - .loading span:nth-of-type(8) { - animation-delay: 1.4s; +} +} + +li { +&.looser { + text-decoration: line-through; + background-color: #D3D3D3; +} + +&.winner { + background: linear-gradient(90deg, #000735 70%, #0EBDE7 30%); + color: white; +} + +.gotogame:hover { + background-color: transparent; +} + +} + +.points { + margin-left: 4rem; +display: inline-block; +width: 24px; +font-weight: bold; +} + +.livedlongest { +width: 16px; +height: 16px; +float: right; +margin-top: 3px; +} + +.gotogame { + background-color: transparent; + text-align: center; + font-size: 12px; + font-weight: bold; + padding: 0; + + .button-link { + font-size: 15px; + background: none!important; + border: none; + padding: 0!important; + font-family: 'Barlow Condensed'; + text-decoration: underline; + cursor: pointer; } - .loading span:nth-of-type(9) { - animation-delay: 1.6s; +} + + + +@media only screen and (min-width: 2500px) { + .welcome{ + margin-left: 30%; + margin-right: 30%; } - .loading span:nth-of-type(10) { - animation-delay: 1.8s; + + .getting-started{ + padding-left: 30%; + padding-right: 30%; } - - - - /* - * Animation keyframes - * Use transition opacity instead of keyframes? - */ - @keyframes loading { - 0% { - opacity: 0; - } - 100% { - opacity: 1; - } + .cygni-at-campus{ + padding-left: 30%; + padding-right: 30%; } - \ No newline at end of file + + +} + +.tooltip { + position: relative; + float: right; + border-style: solid; + border-radius: 50%; + padding-left: 4px; + padding-right: 4px; + border-color: black; + text-decoration: solid; + margin-top: 16px; + right: 12vw; +} + +.tooltip .tooltiptext { + visibility: hidden; + width: 300px; + background-color: #000735; + color: #fff; + text-align: center; + padding: 5px 0; + border-radius: 6px; + position: absolute; + z-index: 1; +} + +.tooltip:hover .tooltiptext { + visibility: visible; +} + diff --git a/src/views/AboutView.tsx b/src/views/AboutView.tsx deleted file mode 100644 index 852b5f38..00000000 --- a/src/views/AboutView.tsx +++ /dev/null @@ -1,46 +0,0 @@ - -function AboutView() { - return ( - <> -
    -
    -

    About

    -
    -

    - We at Cygni love programming. We also love a friendly competetion over - a couple of beers. What better way to combine these two things than a - battle in programming! -

    -

    - Feel free to hack your own Snake Bot and train it in the Training room. - From time to time we hold tournaments where you will be able to face - other player's Snake Bots. -

    -

    Game rules

    -

    - The rules are configurable per game, upon every game start the clients - will be notified of the current game settings. - Here are the default rules: -

    -
      -
    • Snake grows every third game tick
    • -
    • Each client must respond within 250ms
    • -
    • 1 point per Snake growth
    • -
    • 2 points per star consumed
    • -
    • 10 points per tail nibble
    • -
    • 5 points per caused death (another snake crashes and dies into your snake)
    • -
    • 5 black holes
    • -
    • A nibbled tail is protected for 3 game ticks
    • -
    • The last surviving Snake always wins. - The ranking for dead snakes is based on accumulated points -
    • -
    -
    -
    -
    - - - ) -} - -export default AboutView \ No newline at end of file diff --git a/src/views/GameboardView.tsx b/src/views/GameboardView.tsx index 9f818c0d..2590e9ed 100644 --- a/src/views/GameboardView.tsx +++ b/src/views/GameboardView.tsx @@ -1,77 +1,113 @@ -import { useParams, useNavigate } from "react-router-dom" +import { useParams, useNavigate, useLocation } from 'react-router-dom'; import ControllBar from '../components/ControllBar'; -import ScoreBoard from "../components/ScoreBoard"; +import ScoreBoard from '../components/ScoreBoard'; -import api from "../api"; -import { useEffect } from "react"; -import { setGameData } from "../context/slices/gameDataSlice"; -import messageDispatch from "../context/messageDispatch"; -import { clearCurrentFrame } from "../context/slices/currentFrameSlice"; -import { useAppDispatch, useAppSelector } from "../context/hooks"; +import api from '../api'; +import { useEffect } from 'react'; +import { setGameData } from '../context/slices/gameDataSlice'; +import messageDispatch from '../context/messageDispatch'; +import { clearCurrentFrame } from '../context/slices/currentFrameSlice'; +import { useAppDispatch, useAppSelector } from '../context/hooks'; // For drawing the gameboard -import {MAP_HEIGHT_PX, MAP_WIDTH_PX } from "../constants/BoardUtils"; -import { Layer, Stage } from "react-konva"; -import Snake from "../canvasComponents/Snake"; -import Obstacles from "../canvasComponents/Obstacles"; -import Stars from "../canvasComponents/Stars"; +import { MAP_HEIGHT_PX, MAP_WIDTH_PX } from '../constants/BoardUtils'; +import { Layer, Stage } from 'react-konva'; +import Snake from '../components/canvas/Snake'; +import Obstacles from '../components/canvas/Obstacles'; +import Stars from '../components/canvas/Food'; +import { viewedGame } from '../context/slices/tournamentSlice'; function GameboardView() { - let { gameID } = useParams(); - const navigate = useNavigate(); - const dispatch = useAppDispatch(); - const currentFrameState = useAppSelector(state => state.currentFrame); - const tournamentStarted = useAppSelector(state => state.tournament.isTournamentStarted); + let { gameID } = useParams(); + const navigate = useNavigate(); + const dispatch = useAppDispatch(); + const currentFrameState = useAppSelector((state) => state.currentFrame); + const tournamentLevels = useAppSelector((state) => state.tournament.tournamentLevels); + const location = useLocation(); - // Initialize the game - useEffect(() => { - // Reset the current frame state - dispatch(clearCurrentFrame()); + // Initialize the game + useEffect(() => { + // Reset the current frame state + dispatch(clearCurrentFrame()); - // Setup the game - api.getGame(gameID!).then(game => { - console.log("Fetched game", game); - dispatch(setGameData(game)); + // Setup the game + api.getGame(gameID!).then((game) => { + console.log('Fetched game', game); + if (JSON.stringify(game) === '{}'){ + alert('Game not found or might still be running'); + navigate('/'); + } + dispatch(setGameData(game)); - // dispatch 3 times so we get the first map update - messageDispatch(); - messageDispatch(); - messageDispatch(); - }); - }, [gameID]); + // dispatch 3 times so we get the first map update + messageDispatch(); + messageDispatch(); + messageDispatch(false); + }); + }, [dispatch, gameID, navigate]); + + // Display button to go back to the tournament bracket + function BracketNavigation() { + const locationState: any | undefined = location.state; + if (locationState?.fromTournament) { + return ( + <> + + + + ); + } + } - // Display button to go back to the tournament bracket - function BracketNavigation() { - if (tournamentStarted) { - return ( - - ) + const _moveToNextTournamentGame = (id: string) => { + let gameIndex = 0; + const currentLevel = tournamentLevels.find((level) => + level.tournamentGames.find((game, index) => { + if (game.gameId === id) { + gameIndex = index; + return true; } + return false; + }) + ); + + if (currentLevel) { + const nextGame = currentLevel.tournamentGames[gameIndex + 1]; + if (nextGame) { + navigate(`/tournament/${nextGame.gameId}`, { + state: { fromTournament: true }, + }); + dispatch(viewedGame(nextGame.gameId)); + } else { + navigate('/tournament'); + } } + }; return ( -
    - {BracketNavigation()} - -
    +
    +
    -
    - - - {currentFrameState.IDs.map((snakeID, i) => { - const snake = currentFrameState.snakesData[snakeID]; - return ( - - ); - })} - - - - - -
    +
    + + + {currentFrameState.IDs.map((snakeID, i) => { + const snake = currentFrameState.snakesData[snakeID]; + return ; + })} + + + + +
    +
    {BracketNavigation()}
    +
    - ) + ); } -export default GameboardView \ No newline at end of file +export default GameboardView; diff --git a/src/views/GamesearchView.tsx b/src/views/GamesearchView.tsx index 33d22056..df20dc70 100644 --- a/src/views/GamesearchView.tsx +++ b/src/views/GamesearchView.tsx @@ -1,9 +1,10 @@ -import { useState } from "react" -import { Link } from 'react-router-dom'; +import { useState } from "react"; +import { Link } from "react-router-dom"; import api from "../api"; import type { Game } from "../api"; +import search from "../assets/icons/search-icon.svg"; - function GamesearchView() { +function GamesearchView() { const [snakeName, setSnakeName] = useState(""); const [hasSearched, setHasSearched] = useState(false); const [searchResults, setSearchResults] = useState([]); @@ -19,38 +20,49 @@ import type { Game } from "../api"; function Results() { if (searchResults.length === 0 && hasSearched) { return ( -

    No result found

    ); - } else { - return ( -
      { - [...searchResults].reverse().map((game: Game, index: number) => ( +
      +
      + {"searchIcon"} +

      No results found

      +

      + Make sure the spelling is correct or try searching for something + else. +

      +
      +
      + ); + } else { + return ( +
        + {" "} + {[...searchResults].reverse().map((game: Game, index: number) => (
      • -

        - - Date: {game.gameDate} +

        + Game Played: + {game.gameDate}{" "} + + View Game -

        -
          { +

          + {/*
            { game.players.map((player: string, i: number) => (
          • { player }
          • ))} -
          +
        */}
      • ))} -
      ); - - } +
    + ); } + } return ( -
    + <> +
    -

    Search for old games

    +

    Search for old games

    You can find old games here by searching for the snake name.

    @@ -66,12 +78,12 @@ import type { Game } from "../api"; /> -

    Results

    - { Results() } + {Results()}
    - ) + + ); } -export default GamesearchView \ No newline at end of file +export default GamesearchView; diff --git a/src/views/GettingStartedView.tsx b/src/views/GettingStartedView.tsx deleted file mode 100644 index abbef4d8..00000000 --- a/src/views/GettingStartedView.tsx +++ /dev/null @@ -1,49 +0,0 @@ - -function GettingStartedView() { - return ( - <> -
    -
    -

    Getting started

    -
    -

    - Your mission is to write the best Snake Bot and survive within the - game world. We have prepared several language bindings for you to make it - really easy to get started. All the boring stuff concerning server-client - communication, message parsing and event handling is already implemented. -

    -

    General principles

    -

    - The game progresses through Game Ticks. For each Game Tick participating - Snake Bots have to choose an action (and they have to do it fast, - response is expected within 250ms). Actions are defined by a direction to - move the Snake head in. A Snake head may move UP, DOWN, RIGHT or LEFT. -

    -

    - On every Game Tick each Snake Bot receives the current Map. The map contains - the positions of all the objects in the map. -

    -

    Language bindings

    -

    Below are listed the currently implemented (and up to date) language - bindings. Each project has a Readme file that explains how to get - going. -

    - -
    -
    -
    - - - ) -} - -export default GettingStartedView \ No newline at end of file diff --git a/src/views/HomeView.tsx b/src/views/HomeView.tsx deleted file mode 100644 index f339b94a..00000000 --- a/src/views/HomeView.tsx +++ /dev/null @@ -1,52 +0,0 @@ - -function HomeView() { - return ( - <> -
    -
    -

    Welcome!

    -
    -

    - Remember the old game of Snake? One of the first common - implementations was available on the phone Nokia 3310.
    - Snake Record - Nokia 3310 - -

    -

    - This game is a bit different. To play you need to program your own - Snake Bot and you will be competing against other bots! - The concept is simple, your snake can move UP, DOWN, RIGHT - or LEFT and the winner is the last snake alive. Extra points are awarded - when eating stars or nibbling on other snake's tails. Look out for the - black holes though! -

    -

    - {/* Getting started is really easy. - We have implementations in several popular programming languages. Clone - an example Snake bot and get going! */} -

    -

    - Checkout the screencasts below: -

    -

    - +

    + We at Cygni love programming. We also love a friendly competetion over a couple of beers. What better way to + combine these two things than a battle in programming! +

    +

    + Hack your own Snake Bot and train it in the Training room. From time to time we hold tournaments where you + will be able to face other player's Snake Bots. +

    + +

    Game rules

    +

    + The rules are configurable per game, upon every game start the clients will be notified of the current game + settings. Here are the default rules: +

    +
      +
    • Snake grows every third game tick
    • +
    • Each client must respond within 250ms
    • +
    • 1 point per Snake growth
    • +
    • 2 points per star consumed
    • +
    • 10 points per tail nibble
    • +
    • 5 points per caused death (another snake crashes and dies into your snake)
    • +
    • 5 black holes
    • +
    • A nibbled tail is protected for 3 game ticks
    • +
    • The last surviving Snake always wins. The ranking for dead snakes is based on accumulated points
    • +
    +
    + +
    +

    Getting started

    +

    + Your mission is to write the best Snake Bot and survive within the game world. All the boring stuff concerning + server-client communication, message parsing and event handling is already implemented. +

    + +

    General principles

    +

    + The game progresses through Game Ticks. For each Game Tick participating Snake Bots have to choose an action + (and they have to do it fast, response is expected within 250ms). Actions are defined by a direction to move + the Snake head in. A Snake head may move UP, DOWN, RIGHT or LEFT. +

    +

    + On every Game Tick each Snake Bot receives the current Map. The map contains the positions of all the objects + in the map. +

    + +

    JavaScript client

    +

    + The client is written in JavaScript and the project can be cloned through the GitHub repository via the link + below. We recommend using VS Code for editing this project but feel free to use any editor you prefer. The + project has a Readme file that explains how to get going. +

    + Github Repository +
    + +
    +
    +

    Cygni at your campus?

    +

    + Follow @cygniatcampus at Instagram to find out more about the Cygni activities at your Campus and to get + info about our next Snakebot event. +

    +
    + Cygni-instagram +
    +
    + ); +} + +export default StartView; diff --git a/src/views/TournamentView.tsx b/src/views/TournamentView.tsx index a5eaf059..1f858e5e 100644 --- a/src/views/TournamentView.tsx +++ b/src/views/TournamentView.tsx @@ -1,37 +1,49 @@ -import { useEffect } from 'react'; -import api from '../api'; -import { useAppSelector } from '../context/hooks'; -import TournamentSettings from '../components/Tournament/TournamentSettings'; -import TournamentSchedule from '../components/Tournament/TournamentSchedule'; -import LoadingPage from '../components/LoadingPage'; +import { useEffect } from "react"; +import { useNavigate } from 'react-router-dom'; +import api from "../api"; +import { useAppSelector } from "../context/hooks"; +import TournamentSettings from "../components/tournament/Settings"; +import TournamentSchedule from "../components/tournament/Schedule"; +import LoadingPage from "../components/tournament/LoadingPage"; +import PlayerList from "../components/PlayerList"; +import TournamentEnums from "../constants/TournamentEnums"; -function TournamentView() { - const tournament = useAppSelector(state => state.tournament); - const allGamesPlayed = useAppSelector(state => state.tournament.allGamesPlayed); +function TournamentView() { + const isTournamentActive = useAppSelector((state) => state.tournament.isTournamentActive); + const activeTournamentView = useAppSelector((state) => state.tournament.tournamentViewState); + const isLoggedIn = useAppSelector((state) => state.tournament.isLoggedIn); + const navigate = useNavigate(); // Create tournament on mount useEffect(() => { - if (!tournament.isTournamentActive) { + if (!isTournamentActive) { api.createTournament("Tournament"); } - }, []); + }, [isTournamentActive]); - function selectView(){ - if(!tournament.isTournamentStarted){ - return ; - } - else if(allGamesPlayed){ - return ; + // If not logged in, redirect to login page + useEffect(()=>{ + if (!isLoggedIn) navigate('/login'); + }); + + function selectView(page: TournamentEnums) { + switch (page) { + case TournamentEnums.PLAYERLIST: + return ; + case TournamentEnums.SCHEDULE: + return ; + case TournamentEnums.SETTINGSPAGE: + return ; + case TournamentEnums.LOADINGPAGE: + return ; } - return ; } - + return (
    - - {selectView()} + {selectView(activeTournamentView)}
    - ) + ); } -export default TournamentView \ No newline at end of file +export default TournamentView;