diff options
| author | Maks Jopek <maksymilian.jopek@prym-soft.pl> | 2021-04-24 00:34:07 +0200 |
|---|---|---|
| committer | Maks Jopek <maksymilian.jopek@prym-soft.pl> | 2021-04-24 00:34:07 +0200 |
| commit | 13dcc2c8fc1d11e3b94432fffc4c4f186bd94d29 (patch) | |
| tree | 077a963bae31729dffa6c7c678af112680bb6d38 | |
| parent | 457ee1d7caa899bbe86bf7a1390f940b5b3176b7 (diff) | |
| download | fia-13dcc2c8fc1d11e3b94432fffc4c4f186bd94d29.tar.gz fia-13dcc2c8fc1d11e3b94432fffc4c4f186bd94d29.tar.zst fia-13dcc2c8fc1d11e3b94432fffc4c4f186bd94d29.zip | |
Playing almost working
| -rw-r--r-- | backend/ts/api/getCurrentGameState.ts | 25 | ||||
| -rw-r--r-- | backend/ts/api/login.ts | 46 | ||||
| -rw-r--r-- | backend/ts/api/readyStateToggle.ts | 46 | ||||
| -rw-r--r-- | backend/ts/api/saveGameBoard.ts | 16 | ||||
| -rw-r--r-- | backend/ts/api/startPoint.ts | 12 | ||||
| -rw-r--r-- | backend/ts/gameBoard.ts | 99 | ||||
| -rw-r--r-- | backend/ts/index.ts | 4 | ||||
| -rw-r--r-- | backend/ts/startGame.ts | 41 | ||||
| -rw-r--r-- | frontend/ts/login.ts | 10 | ||||
| -rw-r--r-- | frontend/ts/startGame.ts | 451 | ||||
| -rw-r--r-- | frontend/ts/timer.ts | 30 | ||||
| -rw-r--r-- | helpers/helpers.ts | 33 | ||||
| -rw-r--r-- | helpers/helpersBack.ts | 92 | ||||
| -rw-r--r-- | package.json | 2 | ||||
| -rw-r--r-- | public/css/fia.css | 77 | ||||
| -rw-r--r-- | public/index.html | 1 | ||||
| -rw-r--r-- | public/map.html | 21 |
17 files changed, 820 insertions, 186 deletions
diff --git a/backend/ts/api/getCurrentGameState.ts b/backend/ts/api/getCurrentGameState.ts index 7efa009..028cf04 100644 --- a/backend/ts/api/getCurrentGameState.ts +++ b/backend/ts/api/getCurrentGameState.ts @@ -1,16 +1,25 @@ import { Request, Response } from "express"; -import { Data, makeQuery } from "../../../helpers/helpersBack"; +import { Data, makeQuery, getData, API_RES, GameBoard } from "../../../helpers/helpersBack"; export default async function getCurrentGameState(req: Request, res: Response) { - if (req.session === undefined /* || req.body.state === undefined */) { - res.status(500); - res.send(""); + // console.log("request.session", req.session) + if (req.session === undefined || req.session.gid === undefined) { + res.status(400); + res.send(API_RES.failure); return; } - let dbRes: {gameBoard: Data, currentPlayer: number} - = (await makeQuery("SELECT `gameBoard`, `currentPlayer` FROM `fia` WHERE `id` = ?", [req.session.gid]))[0], - currentPlayer = dbRes.currentPlayer === req.session.index; + let dbRes: { gameBoard: GameBoard, currentPlayer: number, started: number, data: Data, timeTillTurnEnd: number } + = (await makeQuery("SELECT `gameBoard`, `currentPlayer`, `started`, `data`, `timeTillTurnEnd`" + + " FROM `fia` WHERE `id` = ?", + [req.session.gid]))[0]; + + + if (dbRes.started === 0) { + res.send(JSON.stringify({ data: dbRes.data })); + } else { + res.send(JSON.stringify(dbRes)); + } + - res.send(JSON.stringify({gameBoard: dbRes.gameBoard, currentPlayer})); }
\ No newline at end of file diff --git a/backend/ts/api/login.ts b/backend/ts/api/login.ts index 0c6ef75..b21a506 100644 --- a/backend/ts/api/login.ts +++ b/backend/ts/api/login.ts @@ -1,6 +1,7 @@ import { Request, Response } from "express"; -import { fiaTable, dbRes, Color, Colors, Helpers, makeQuery, Chequer, tColors } from "../../../helpers/helpersBack"; +import { fiaTable, dbRes, Color, Colors, Helpers, makeQuery, Chequer, tColors, waitForQueue } from "../../../helpers/helpersBack"; import startGame from "../startGame"; +import { global } from "../index"; export default async function apiLogin(req: Request, res: Response) { if (req.session === undefined) { @@ -11,27 +12,39 @@ export default async function apiLogin(req: Request, res: Response) { req.session.uid = req.session.id; req.session.name = req.body.name; - let dbRes = (await makeQuery("SELECT * FROM `fia` WHERE `started`= 0 LIMIT 1", [])) as Array<dbRes>, + if (global.stop === true) { + await waitForQueue(); + } + global.stop = true; + + let dbRes = (await makeQuery("SELECT * FROM `fia` WHERE `started`= 0 LIMIT 1", [])) as Array<fiaTable>, row: fiaTable; - if (dbRes.length === 0) { - let color = Helpers.getRandomInt(0, 5); + if (dbRes[0] === undefined) { + let color = Helpers.getRandomInt(0, 4); row = {} as fiaTable; row.data = [{ id: req.session.uid, index: 0, - color: Color[color as keyof Colors], + ready: false, + color: color, name: req.body.name, }]; - req.session.color = Color.blue; + req.session.color = color; - await makeQuery('INSERT INTO `fia`(`data`, `started`) VALUES (?, ?)', [JSON.stringify(row.data), 0]); - req.session.gid = (await makeQuery('SELECT `id` FROM `fia` WHERE `data`= ?', [JSON.stringify(row.data)]) as any)[0].id; + await makeQuery('INSERT INTO `fia`(`data`) VALUES (?)', [row.data]); + req.session.gid = (await makeQuery('SELECT `id` FROM `fia` WHERE `data`= ?', [row.data]) as any)[0].id; } else { - row = Helpers.dbResToFiaTable(dbRes[0]); + // row = Helpers.dbResToFiaTable(dbRes[0]); + row = dbRes[0]; let playersColors: Array<tColors> = [], colors = [...Array(4).keys()]; + if (row.data.filter(el => el.id !== req.session?.uid).length === 0) { + res.send(JSON.stringify({ data: row.data, uid: req.session.uid })); + return; + } + for (let playerData of row.data) playersColors.push(playerData.color); @@ -42,6 +55,7 @@ export default async function apiLogin(req: Request, res: Response) { row.data.push({ id: req.session.uid, index: index, + ready: false, color: color, name: req.body.name, }); @@ -49,12 +63,14 @@ export default async function apiLogin(req: Request, res: Response) { req.session.gid = row.id; req.session.index = index; - makeQuery('UPDATE `fia` SET `data`= ? WHERE `id`= ?', [JSON.stringify(row.data), row.id]); + await makeQuery('UPDATE `fia` SET `data`= ? WHERE `id`= ?', [row.data, row.id]); + + if (row.data.length >= 4) + startGame(req.session.gid); - if (row.data.length === 4) { - startGame(req, res); - } } - makeQuery("UPDATE `fia` SET `playersCount`=`playersCount`+1 WHERE `id`= ?", [req.session.gid]); - res.send(JSON.stringify(row.data)); + + await makeQuery("UPDATE `fia` SET `playersCount`=`playersCount`+1 WHERE `id`= ?", [req.session.gid]); + global.stop = false; + res.send(JSON.stringify({ data: row.data, uid: req.session.uid })); } diff --git a/backend/ts/api/readyStateToggle.ts b/backend/ts/api/readyStateToggle.ts index 6bca5b2..e1b08b2 100644 --- a/backend/ts/api/readyStateToggle.ts +++ b/backend/ts/api/readyStateToggle.ts @@ -1,19 +1,43 @@ import { Request, Response } from "express"; -import { makeQuery } from "../../../helpers/helpersBack"; +import { makeQuery, Data, getData, API_RES, waitForQueue } from "../../../helpers/helpersBack"; import startGame from "../startGame"; +import {global} from "../index"; export default async function (req: Request, res: Response) { - if (req.session === undefined || req.body.state === undefined) { + if (req.session === undefined || req.body.state === undefined || req.session.gid === undefined) { res.status(500); - res.send(""); + res.send(API_RES.failure); return; } - await makeQuery("UPDATE `fia` SET `playersAreReady`=`playersAreReady`+" + (req.body.state ? 1 : -1) - + " WHERE `id`=" + req.session.gid, []); - let start = ( - await makeQuery('SELECT playersAreReady=playersCount and playersCount > 1 "start" FROM `fia` WHERE `id`= ?', - [req.session.gid]) - )[0].start; - if(start === 1) - startGame(req, res); + + if(global.stop === true) { + await waitForQueue(); + } + global.stop = true; + + let data: { data: Data, started: number } = + (await makeQuery("SELECT `data`, `started` FROM `fia` WHERE `id` = ?", [req.session.gid]))[0], + i = 1, + started = false; + + if (data.started === 0) { + + for (let player of data.data) { + if (player.id === req.session.uid) + player.ready = req.body.state ? true : false; + else if (player.ready === true) + i++; + } + + await makeQuery("UPDATE `fia` SET `data` = ?, `playersAreReady`=`playersAreReady`+ ? WHERE `id`= ?", + [data.data, (req.body.state ? 1 : -1), req.session.gid]); + + if (data.data.length > 1 && data.data.length === i) { + startGame(req.session.gid); + started = true; + } + + } + global.stop = false; + res.send(`{"success": true, "started": ${started}}`); } diff --git a/backend/ts/api/saveGameBoard.ts b/backend/ts/api/saveGameBoard.ts new file mode 100644 index 0000000..551c185 --- /dev/null +++ b/backend/ts/api/saveGameBoard.ts @@ -0,0 +1,16 @@ +import { Request, Response } from "express"; +import { API_RES, makeQuery } from "../../../helpers/helpersBack"; + +export default async function saveGameBoard(req: Request, res: Response) { + if(req.session === undefined || req.body.gameBoard === undefined) { + res.status(400); + res.send(API_RES.failure); + return; + } + + await makeQuery("UPDATE `fia` SET `gameBoard` = ? WHERE `id` = ? ", [ + req.body.gameBoard, req.session.gid + ]); + + res.send(API_RES.success); +}
\ No newline at end of file diff --git a/backend/ts/api/startPoint.ts b/backend/ts/api/startPoint.ts index d635622..20d7b79 100644 --- a/backend/ts/api/startPoint.ts +++ b/backend/ts/api/startPoint.ts @@ -2,16 +2,12 @@ import { Request, Response } from "express"; import { makeQuery } from "../../../helpers/helpersBack"; export default async function (req: Request, res: Response) { - if (req.session === undefined) { - res.send(/* html */ `<script>alert("Something went wrong, try to reload the page")</script>`); - return; - } - // console.log("uid:", req.session.uid, "gid:", req.session.gid); - if (req.session.uid !== undefined) { - let data = await makeQuery('SELECT `data` FROM `fia` WHERE `id`=?', [req.session.gid]); + if (req.session !== undefined && req.session.gid !== undefined) { + let data = (await makeQuery('SELECT `data` FROM `fia` WHERE `id`=?', [req.session.gid]))[0]; res.send(JSON.stringify({ status: true, - data: data + data: data, + uid: req.session.uid })) } else { diff --git a/backend/ts/gameBoard.ts b/backend/ts/gameBoard.ts new file mode 100644 index 0000000..89a8902 --- /dev/null +++ b/backend/ts/gameBoard.ts @@ -0,0 +1,99 @@ +import { Color, GameBoard } from '../../helpers/helpersBack'; +export const gameBoard: GameBoard = { + map: [ + { y: 0, x: 4, chequers: [], start: Color.red }, + { y: 1, x: 4, chequers: [], start: -1 }, + { y: 2, x: 4, chequers: [], start: -1 }, + { y: 3, x: 4, chequers: [], start: -1 }, + { y: 4, x: 4, chequers: [], start: -1 }, + { y: 4, x: 3, chequers: [], start: -1 }, + { y: 4, x: 2, chequers: [], start: -1 }, + { y: 4, x: 1, chequers: [], start: -1 }, + { y: 4, x: 0, chequers: [], start: -1 }, + { y: 5, x: 0, chequers: [], start: -1 }, + { y: 6, x: 0, chequers: [], start: Color.blue }, + { y: 6, x: 1, chequers: [], start: -1 }, + { y: 6, x: 2, chequers: [], start: -1 }, + { y: 6, x: 3, chequers: [], start: -1 }, + { y: 6, x: 4, chequers: [], start: -1 }, + { y: 7, x: 4, chequers: [], start: -1 }, + { y: 8, x: 4, chequers: [], start: -1 }, + { y: 9, x: 4, chequers: [], start: -1 }, + { y: 10, x: 4, chequers: [], start: -1 }, + { y: 10, x: 5, chequers: [], start: -1 }, + { y: 10, x: 6, chequers: [], start: Color.green }, + { y: 9, x: 6, chequers: [], start: -1 }, + { y: 8, x: 6, chequers: [], start: -1 }, + { y: 7, x: 6, chequers: [], start: -1 }, + { y: 6, x: 6, chequers: [], start: -1 }, + { y: 6, x: 7, chequers: [], start: -1 }, + { y: 6, x: 8, chequers: [], start: -1 }, + { y: 6, x: 9, chequers: [], start: -1 }, + { y: 6, x: 10, chequers: [], start: -1 }, + { y: 5, x: 10, chequers: [], start: -1 }, + { y: 4, x: 10, chequers: [], start: Color.yellow }, + { y: 4, x: 9, chequers: [], start: -1 }, + { y: 4, x: 8, chequers: [], start: -1 }, + { y: 4, x: 7, chequers: [], start: -1 }, + { y: 4, x: 6, chequers: [], start: -1 }, + { y: 3, x: 6, chequers: [], start: -1 }, + { y: 2, x: 6, chequers: [], start: -1 }, + { y: 1, x: 6, chequers: [], start: -1 }, + { y: 0, x: 6, chequers: [], start: -1 }, + { y: 0, x: 5, chequers: [], start: -1 }, + ], + // @ts-ignore + bases: { + [Color.red]: [ + { x: 1, y: 1, chequer: -1 }, + { x: 1, y: 2, chequer: -1 }, + { x: 2, y: 2, chequer: -1 }, + { x: 2, y: 1, chequer: -1 }, + ], + [Color.blue]: [ + { x: 8, y: 1, chequer: -1 }, + { x: 9, y: 1, chequer: -1 }, + { x: 9, y: 2, chequer: -1 }, + { x: 8, y: 2, chequer: -1 }, + ], + [Color.green]: [ + { x: 8, y: 8, chequer: -1 }, + { x: 9, y: 8, chequer: -1 }, + { x: 9, y: 9, chequer: -1 }, + { x: 8, y: 9, chequer: -1 }, + ], + [Color.yellow]: [ + { x: 1, y: 8, chequer: -1 }, + { x: 2, y: 8, chequer: -1 }, + { x: 2, y: 9, chequer: -1 }, + { x: 1, y: 9, chequer: -1 }, + ], + }, + // @ts-ignore + homes: { + [Color.red]: [ + { x: 5, y: 1, chequer: -1 }, + { x: 5, y: 2, chequer: -1 }, + { x: 5, y: 3, chequer: -1 }, + { x: 5, y: 4, chequer: -1 }, + ], + [Color.blue]: [ + { x: 1, y: 5, chequer: -1 }, + { x: 2, y: 5, chequer: -1 }, + { x: 3, y: 5, chequer: -1 }, + { x: 4, y: 5, chequer: -1 }, + ], + [Color.green]: [ + { x: 5, y: 9, chequer: -1 }, + { x: 5, y: 8, chequer: -1 }, + { x: 5, y: 7, chequer: -1 }, + { x: 5, y: 6, chequer: -1 }, + ], + [Color.yellow]: [ + { x: 9, y: 5, chequer: -1 }, + { x: 8, y: 5, chequer: -1 }, + { x: 7, y: 5, chequer: -1 }, + { x: 6, y: 5, chequer: -1 }, + ], + } +}
\ No newline at end of file diff --git a/backend/ts/index.ts b/backend/ts/index.ts index d0899dd..e4ec5ee 100644 --- a/backend/ts/index.ts +++ b/backend/ts/index.ts @@ -6,6 +6,9 @@ import login from "./api/login"; import startPoint from "./api/startPoint"; import readyStateToggle from "./api/readyStateToggle"; import getCurrentGameState from "./api/getCurrentGameState"; +import saveGameBoard from "./api/saveGameBoard"; + +export const global = { stop: false }; dotenv.config(); const app = express(); @@ -25,6 +28,7 @@ app.get("/startPoint", startPoint) app.post("/login", login); app.post("/readyStateToggle", readyStateToggle); app.post("/getCurrentGameState", getCurrentGameState); +app.post("/saveGameBoard", saveGameBoard); app.listen(PORT, () => { diff --git a/backend/ts/startGame.ts b/backend/ts/startGame.ts index 4188d29..eb19bc0 100644 --- a/backend/ts/startGame.ts +++ b/backend/ts/startGame.ts @@ -1,41 +1,16 @@ import { } from "../../helpers/helpers"; -import { makeQuery, Color, GameBoard, Chequer, Coordinates, Data } from "../../helpers/helpersBack"; -import { Request, Response } from "express"; +import { makeQuery, nextTurnEndsAt, Color, GameBoard, Chequer, Coordinates, Data, getData, HomesOrBases, Square, tColors } from "../../helpers/helpersBack"; -export default async function startGame(req: Request, res: Response) { - if (req.session === undefined) { - res.status(500); - res.send(""); - return; - } - - makeQuery("UPDATE `fia` SET `started` = 1 WHERE `id`= ?", [req.session.gid]); - let dbRes: {gameBoard: GameBoard, data: Data} - = (await makeQuery("SELECT `gameBoard`, `playersCount` FROM `fia` WHERE `id` = ?", [req.session.gid]))[0]; +export default async function startGame(gid: number) { + let dbRes: {gameBoard: GameBoard, data: Data} = + (await makeQuery("SELECT `gameBoard`, `data` FROM `fia` WHERE `id` = ?", [gid]))[0]; for (let player of dbRes.data) { - let chequer: Chequer = { color: player.color, x: 1, y: 1 }, - start: Coordinates; - - switch (player.color) { - case Color.red: start = { x: 1, y: 1 }; break; - case Color.blue: start = { x: 8, y: 1 }; break; - case Color.yellow: start = { x: 1, y: 8 }; break; - default /* Color.green */: start = { x: 8, y: 8 }; break; - // case Color.green: start = { x: 8, y: 8 }; break; + for(let square of dbRes.gameBoard.bases[player.color as keyof HomesOrBases]) { + square.chequer = player.color; } - - for (let i = 0; i < 2; i++) { - for (let j = 0; j < 2; j++) { - //@ts-ignore - 8 + 1 <= 11 which is max of Coordinates - [chequer.x, chequer.y] = [start.x + i, start.y + j]; - } - } - - dbRes.gameBoard.push(chequer); } - await makeQuery("UPDATE `fia` SET `gameBoard` = ? WHERE `id`= ?", [dbRes.gameBoard, req.session.gid]); - - res.send(""); + await makeQuery("UPDATE `fia` SET `gameBoard` = ?, `started` = 1, `timeTillTurnEnd` = ? WHERE `id`= ?", + [dbRes.gameBoard, nextTurnEndsAt(), gid]); }
\ No newline at end of file diff --git a/frontend/ts/login.ts b/frontend/ts/login.ts index ec4defa..2e50884 100644 --- a/frontend/ts/login.ts +++ b/frontend/ts/login.ts @@ -5,7 +5,7 @@ class Login { static html = /* html */ ` <div class="container text-center fs-1" style="margin-top: 45vh;"> <label for="username">Vad heter du?</label> - <input type="text" name="username" value="maks" required> + <input type="text" name="username" required> <button type="button" class="btn btn-light btn-lg" onclick="Login.login">Starta spelet</button> </div> `; @@ -13,12 +13,14 @@ class Login { static async start() { let res = await (await fetch("/startPoint")).json(); if (res.status === true) { - new StartGame(JSON.parse(res.data[0].data)); + new StartGame({ data: res.data.data, uid: res.uid }); } else { document.body.innerHTML = Login.html; document.getElementsByTagName("button")[0].onclick = () => Login.login(); // !! + document.getElementsByTagName("input")[0].value = "maks " + Helpers.getRandomInt(1, 100); + document.title = document.getElementsByTagName("input")[0].value; document.getElementsByTagName("button")[0].click(); } } @@ -26,9 +28,9 @@ class Login { static async login(username = "") { if (username === "") username = document.getElementsByTagName("input")[0].value; - + let res = await Helpers.mFetch("/login", { name: username }); - new StartGame(await res.json()); + new StartGame(res); } } Login.start();
\ No newline at end of file diff --git a/frontend/ts/startGame.ts b/frontend/ts/startGame.ts index e97f4ea..71a9abf 100644 --- a/frontend/ts/startGame.ts +++ b/frontend/ts/startGame.ts @@ -1,131 +1,418 @@ -import { Data } from "../../helpers/helpersBack"; +import { Data, Coordinates, tColors, HomesOrBases, Square, HoBSquare, Ghost, mFetch } from "../../helpers/helpersBack"; import Helpers from "../../helpers/helpers.js"; import { Chequer, GameBoard } from "../../helpers/helpersBack.js"; +import Timer from "./timer.js"; export default class StartGame { - colors = ["primary", "danger", "success", "warning"]; + bgColors = ["primary", "danger", "success", "warning"]; + colors = ["blue", "red", "green", "yellow"]; radius = 20; + numberOfSquaresOnGameBoard = 11; + startInterval = () => { this.getData(); this.interval = setInterval(() => this.getData(), 2000); } + + uid!: string; + color!: tColors; + data!: Data; + timeTillTurnEnd!: number; + gameBoard!: GameBoard; + index!: number; + oldIndex!: number; - ctx!: CanvasRenderingContext2D; - canvas!: HTMLCanvasElement; interval!: any; - img!: HTMLImageElement; - - chequers: Array<Chequer> = []; - gameBoard: GameBoard = []; - gmString = "[\n"; + timer!: Timer; + + btnStateToggle!: HTMLInputElement; + gameTable!: HTMLTableElement; + btnRollDice!: HTMLButtonElement; + divDice!: HTMLDivElement; + + mineTurn = false; + gameStarted = false; + toggleStop = false; - constructor(data: Data) { + constructor(data: { data: Data, uid: string }) { this.init(data); } - async init(data: Data) { + async init(data: { data: Data, uid: string }) { document.body.innerHTML = await (await fetch("/map.html")).text(); - document.getElementsByTagName("input")[0].onclick = this.readyStateToggle; - [this.canvas, this.ctx] = this.getCanvasAndCtx(); - this.setOpponents(data); + this.uid = data.uid; + this.data = data.data; + this.index = data.data.filter(el => el.id === this.uid)[0].index; + this.color = data.data.filter(el => el.id === this.uid)[0].color; - this.canvas.onclick = e => this.canvasClickHandler(e); + this.setOpponents(data.data, true); - this.img = await this.loadImage("https://jopek.eu/maks/szkola/apkKli/fiaFiles/fia.png"); - - await this.getData(); - this.ctx.drawImage(this.img, 0, 0, this.img.width, this.img.height, 0, 0, this.canvas.width, this.canvas.height); - this.drawChequer({ x: 4, y: 1, color: 1 }); + this.gameTable = document.getElementsByTagName("table")[1]; + this.btnStateToggle = document.getElementsByTagName("input")[0]; + this.btnRollDice = document.getElementsByTagName("button")[0]; + this.divDice = document.getElementById("dice") as HTMLDivElement; + this.timer = new Timer(); - // this.interval = setInterval(this.getData, 2000); - } + this.btnStateToggle.onclick = this.readyStateToggle; + this.btnRollDice.onclick = this.rollDice; + + this.drawGameTable(); - getCanvasAndCtx(): [HTMLCanvasElement, CanvasRenderingContext2D] { - let canvas = document.getElementsByTagName("canvas")[0], - ctx = canvas.getContext("2d"); - if (ctx === null) { - document.write("stop using IE ..."); - throw new Error("Player is using browser older than ..."); - } else return [canvas, ctx]; + this.startInterval(); } - async setOpponents(data: Data): Promise<void> { + async setOpponents(data: Data, firstTime: boolean): Promise<void> { let tds = (document.getElementsByClassName("player-name") as HTMLCollectionOf<HTMLElement>), i = 0; - + for (let player of data) { - if (player.ready === true) - tds[i].classList.add("bg-" + this.colors[player.color]); + tds[i].className = tds[i].className.replace(/\bbg-.*\b/, ''); + + if (player.ready === true) { + tds[i].classList.add("bg-" + this.bgColors[player.color]); + if (this.uid === player.id) + document.getElementsByTagName("input")[0].checked = true; + } else tds[i].classList.add("bg-secondary"); - tds[i].innerText = player.name; - console.log("player, i", player, i); + (tds[i].children[0] as HTMLElement).innerText = player.name; i++; } - for (; i < 4; i++) { - console.log("i", i); - tds[i].classList.add("bg-secondary"); - tds[i].innerText = "?"; + if (firstTime === true) { + for (; i < 4; i++) { + tds[i].classList.add("bg-secondary"); + (tds[i].children[0] as HTMLElement).innerText = "?"; + } + } + } + + readyStateToggle = async (e: MouseEvent): Promise<void> => { + if (this.toggleStop) { + this.btnStateToggle.checked = true; + return; } + this.toggleStop = true; + let res = await Helpers.mFetch("/readyStateToggle", { + state: this.btnStateToggle.checked + }); + if (res.started === true) + this.toggleStop = true; + else + this.toggleStop = false; + clearInterval(this.interval); + this.getData(); + this.startInterval(); } - isIntersect(point: { x: number, y: number }, circle: Chequer) { - return Math.sqrt((point.x - circle.x) ** 2 + (point.y - circle.y) ** 2) < this.radius; + rollDice = async (e: MouseEvent): Promise<void> => { + let number = Helpers.getRandomInt(1, 7); + this.divDice.classList.add(`dice-${number}`); + this.btnRollDice.style.display = "none"; + this.calcMoves(number); } - canvasClickHandler(e: MouseEvent) { - const mousePos = { - x: e.clientX - this.canvas.offsetLeft, - y: e.clientY - this.canvas.offsetTop - }; + calcMoves(number: number) { + if ([1, 6].includes(number)) + this.moveFromBase(); - if (Math.round((mousePos.x - 30) / 60) == 0 && Math.round((mousePos.y - 28) / 60) == 0) { - console.log(this.gmString + "]"); - this.gmString = "[\n" - } + this.moveInMap(number); + this.moveInHome(number); + + this.drawGameBoard(); + console.log("checking for ghosts"); + if (this.anyGhosts() === false) + this.sendGameBoard(); else - this.gmString += JSON.stringify({ x: Math.round((mousePos.x - 30) / 60), y: Math.round((mousePos.y - 28) / 60) }) + ",\n"; + console.log("ghost were created"); + } + moveFromBase() { + let start: Square, i: number; - this.chequers.forEach(chequer => { - if (this.isIntersect(mousePos, chequer)) { - alert('click on circle: ' + chequer); + this.gameBoard.map.forEach((square, j) => { + if (square.start === this.color) { + start = square; + i = j; } - }); + }) + + this.gameBoard.bases[this.color as keyof HomesOrBases].forEach((chequer, j) => { + if (chequer.chequer > -1) { + chequer.ghost = { + where: { + from: "base", oldIndex: j, + to: "map", newIndex: i + }, + color: this.color, + coords: { x: start.x, y: start.y } + }; + } + }) } + moveInMap(number: number) { + this.gameBoard.map.forEach((square, i) => { + if (square.chequers.length > 0 && square.chequers[0].color === this.color) { + + for (let chequer of square.chequers) { + let x: number | undefined, y: number | undefined, coords: Coordinates | undefined, inMap: boolean, + homeIndex: number | undefined; + + if (i + number < this.gameBoard.map.length) { + x = this.gameBoard.map[i + number].x; + y = this.gameBoard.map[i + number].y; + inMap = true; + } + else { + homeIndex = this.gameBoard.map.length - i - 1 - number - 1; + inMap = true; + + if (homeIndex < this.gameBoard.homes[0].length) { + x = this.gameBoard.homes[this.color as keyof HomesOrBases][homeIndex].x; + y = this.gameBoard.homes[this.color as keyof HomesOrBases][homeIndex].y; + } + else { + x = y = undefined; + } + } + + if (x === undefined || y === undefined) + coords = undefined; + else + coords = { x, y }; + + chequer.ghost = { + where: { + from: "map", oldIndex: i, + to: (inMap === true ? "map" : "home"), newIndex: homeIndex || i + number + }, + color: this.color, + coords, + } + } + } + }) + } + moveInHome(number: number) { + let home = this.gameBoard.homes[this.color as keyof HomesOrBases]; + + home.forEach((chequer, i) => { + if (chequer.chequer > -1) { + let x: number | undefined, y: number | undefined, coords: Coordinates | undefined; + if (home[i + number] !== undefined) { + x = home[i + number].x; + y = home[i + number].y; + } else { + x = y = undefined; + } + + if (x === undefined || y === undefined) + coords = undefined; + else + coords = { x, y }; - readyStateToggle(e: MouseEvent): void { - Helpers.mFetch("/readyStateToggle", { - state: (e.target as HTMLInputElement).checked + chequer.ghost = { + where: { + from: "home", oldIndex: i, + to: "home", newIndex: i + number + }, + color: this.color, + coords, + }; + } }); } - async loadImage(src: string): Promise<HTMLImageElement> { - return new Promise(resolve => { - let img = new Image() - img.src = src - img.onload = () => resolve(img) - }) + showGhost(chequer: Chequer | HoBSquare) { + if (chequer.ghost === undefined || chequer.ghost.coords === undefined) { + console.log("showChequer::chequer.ghost.coords === undefined", chequer.ghost?.coords === undefined); + return; + } + this.showChequer(chequer.ghost.coords, this.color) + } + hideGhost(chequer: Chequer | HoBSquare): void { + if (chequer.ghost === undefined || chequer.ghost.coords === undefined) { + console.log("hideChequer::chequer.ghost.coords === undefined", chequer.ghost?.coords === undefined); + return; + } + this.hideChequer(chequer.ghost.coords); + } + doTurn(chequer: Chequer | HoBSquare): void { + if (chequer.ghost === undefined || chequer.ghost.coords === undefined) { + console.log("hideChequer::chequer.ghost.coords === undefined", chequer.ghost?.coords === undefined); + return; + } + this.mineTurn = false; + let where = chequer.ghost.where; + + if (where.from === "map") { + this.gameBoard.map[where.oldIndex].chequers.pop(); + } else if (where.from === "base") { + this.gameBoard.bases[this.color as keyof HomesOrBases][where.oldIndex].chequer = -1; + } else { + this.gameBoard.homes[this.color as keyof HomesOrBases][where.oldIndex].chequer = -1; + } + + if (where.to === "map") { + this.gameBoard.map[where.newIndex].chequers.push({ color: this.color }); + } else if (where.to === "base") { + this.gameBoard.bases[this.color as keyof HomesOrBases][where.newIndex].chequer = this.color; + } else { + this.gameBoard.homes[this.color as keyof HomesOrBases][where.newIndex].chequer = this.color; + } + + this.sendGameBoard(); + } + removeGhosts(): void { + for (let square of this.gameBoard.map) { + for (let chequer of square.chequers) { + chequer.ghost = undefined; + } + } + for (let chequer of this.gameBoard.bases[this.color as keyof HomesOrBases]) { + chequer.ghost = undefined; + } + for (let chequer of this.gameBoard.homes[this.color as keyof HomesOrBases]) { + chequer.ghost = undefined; + } + } + anyGhosts(): boolean { + for (let square of this.gameBoard.map) { + for (let chequer of square.chequers) { + if (chequer.ghost !== undefined) + return true; + } + } + for (let chequer of this.gameBoard.bases[this.color as keyof HomesOrBases]) { + if (chequer.ghost !== undefined) + return true; + } + for (let chequer of this.gameBoard.homes[this.color as keyof HomesOrBases]) { + if (chequer.ghost !== undefined) + return true; + } + return false; } - drawChequer(chequer: Chequer): void { - let x = chequer.x * 60 + 30, - y = chequer.y * 60 + 28; + drawGameBoard(): void { - this.ctx.beginPath(); - this.ctx.arc(x, y, this.radius, 0, 2 * Math.PI); - this.ctx.fillStyle = this.colors[chequer.color]; - this.ctx.fill(); - this.ctx.lineWidth = 5; - this.ctx.strokeStyle = 'grey'; - this.ctx.closePath(); - this.ctx.stroke(); + for (let square of this.gameBoard.map) { + if (square.chequers.length !== 0) { + for (let chequer of square.chequers) + this.drawChequer({ x: square.x, y: square.y }, chequer.color, chequer); + } + } + for (let baseOfColor in this.gameBoard.bases) { + for (let square of this.gameBoard.bases[baseOfColor as unknown as keyof HomesOrBases]) { + if (square.chequer > -1) + this.drawChequer({ x: square.x, y: square.y }, square.chequer, square); + } + } + for (let baseOfColor in this.gameBoard.homes) { + for (let square of this.gameBoard.homes[baseOfColor as unknown as keyof HomesOrBases]) { + if (square.chequer > -1) + this.drawChequer({ x: square.x, y: square.y }, square.chequer, square); + } + } + } - this.chequers.push(chequer); + drawGameTable(): void { + for (let i = 0; i < this.numberOfSquaresOnGameBoard; i++) { + let tr = document.createElement("tr"); + for (let j = 0; j < this.numberOfSquaresOnGameBoard; j++) { + let td = document.createElement("td"); + td.classList.add("chequer"); + td.dataset.count = '0'; + tr.appendChild(td); + } + this.gameTable.appendChild(tr); + } + } + + async sendGameBoard() { + this.divDice.className.replace(/dice.*/, ''); + this.removeGhosts(); + await Helpers.mFetch("/saveGameBoard", { gameBoard: this.gameBoard }); + } + + getTd(coords: Coordinates): HTMLElement { + return this.gameTable.children[coords.y].children[coords.x] as HTMLElement; + } + showChequer(coords: Coordinates, color: tColors): void { + let td = this.getTd(coords); + if (td.dataset.count === undefined) + throw new Error("td.dataset.count is undefined!!!") + + if (td.dataset.count === '0') { + td.classList.add("chequer-" + this.colors[color]); + td.dataset.count = "1"; + } else { + td.dataset.count = (parseInt(td.dataset.count) + 1).toString(); + td.innerHTML = 'x' + td.dataset.count; + } + } + hideChequer(coords: Coordinates, all = false) { + let td = this.getTd(coords); + if (td.dataset.count === undefined) + throw new Error("td.dataset.count is undefined!!!") + + if (td.dataset.count === '0') + throw new Error("Cannot remove from empty td!!!"); + + if (td.dataset.count === '1' || all === true) { + td.className = td.className.replace(/chequer-.*/, ''); + td.innerHTML = 'x' + td.dataset.count; + } else { + td.dataset.count = (parseInt(td.dataset.count) - 1).toString(); + td.innerHTML = 'x' + td.dataset.count; + } + } + + drawChequer(coords: Coordinates, color: tColors, chequer: Chequer | HoBSquare): void { + let td = this.getTd(coords); + this.showChequer(coords, color); + if (this.mineTurn === true && chequer.ghost !== undefined) { + td.onmouseenter = () => { console.log("mouseenter"); this.showGhost(chequer); } + td.onmouseleave = () => { console.log("mouseleave"); this.hideGhost(chequer); } + td.onclick = () => { console.log("mouseclick"); this.doTurn(chequer); } + } + } + removeChequer(coords: Coordinates) { + let td = this.getTd(coords); + this.hideChequer(coords, true); + td.onmouseenter = () => null; + td.onmouseleave = () => null; + td.onclick = () => null; + } + + async startTimer(currentPlayer: number) { + await this.timer.start(currentPlayer, this.timeTillTurnEnd); + this.sendGameBoard(); } async getData() { - let data = await Helpers.mFetch("/getCurrentGameState", {}); - // this.setOpponents(JSON.parse(data.data)); - // TODO: assign data to this.gameBoard and draw it - // TODO: add D6 dice - // TODO: if currentPLayer === true, call to logic or calc logic here + let data: { gameBoard: GameBoard, currentPlayer: number, started: number, data: Data, timeTillTurnEnd: number } + = await Helpers.mFetch("/getCurrentGameState", {}); + if (data.started === 1) { + if (this.oldIndex !== data.currentPlayer) { + this.oldIndex = data.currentPlayer; + if (data.currentPlayer === this.index && this.mineTurn === false) { + this.mineTurn = true; + this.btnRollDice.style.display = "block"; + }// else if (JSON.stringify(data.gameBoard) !== JSON.stringify(this.gameBoard)) { + this.gameBoard = data.gameBoard; + this.drawGameBoard(); + // } + this.timeTillTurnEnd = data.timeTillTurnEnd; + this.startTimer(data.currentPlayer); + } + } + + if (JSON.stringify(data.data) !== JSON.stringify(this.data)) { + console.log("startGame::getData::data", data.data); + this.data = data.data; + this.setOpponents(this.data, false); + } + + // TODO: Animation: blinking of chequers with ghosts + // TODO: Ending turn + // TODO: Ghost not working + // TODO: Moving not working } -}
\ No newline at end of file +} diff --git a/frontend/ts/timer.ts b/frontend/ts/timer.ts new file mode 100644 index 0000000..f8dab25 --- /dev/null +++ b/frontend/ts/timer.ts @@ -0,0 +1,30 @@ +export default class Timer { + divTimer!: HTMLElement; + + interval!: any; + async start(playerIndex: number, finishTime: number): Promise<void> { + return new Promise(resolve => { + let divTimer = document.getElementById(`${playerIndex}-player-time`); + if (divTimer === null) + throw new Error("divTimer doesn't Exist"); + else + this.divTimer = divTimer; + + this.interval = setInterval(() => { + this.updateDisplay(finishTime - Date.now(), resolve); + }, 1000); + }) + } + + updateDisplay(time: number, resolve: Function): void { + let secondsElapsed = Math.floor(time / 1000); + this.divTimer.innerHTML = secondsElapsed.toString(); + if (secondsElapsed === 0) { + resolve(); + clearInterval(this.interval); + } + } + stop(): void { + clearInterval(this.interval); + } +}
\ No newline at end of file diff --git a/helpers/helpers.ts b/helpers/helpers.ts index 03bc7d7..80fb5ab 100644 --- a/helpers/helpers.ts +++ b/helpers/helpers.ts @@ -23,8 +23,11 @@ export default class Helpers { } static dbResToFiaTable(dbRes: dbRes): fiaTable { - if (typeof dbRes.data === "string") + if (typeof dbRes.data === "string") { + console.log("dbRes.data: ", dbRes.data); + console.log("dbRes.data as string: ", JSON.stringify(dbRes.data)); dbRes.data = JSON.parse(dbRes.data); + } else throw new Error("dbResToFiaTable : wrong $1 type; "); @@ -32,11 +35,11 @@ export default class Helpers { return dbRes; } - static checkApiRes(res: any, nRes = {} as Response): any { - if (res.api === false) { + static checkApiRes(res: any, nRes = {} as Response): boolean { + if (res.success === false) { if (Object.keys(nRes).length === 0) { // @ts-ignore - here error is because backend tsc is checking this file, where lib: DOM is not present - alert("API Error!"); + // alert("API Error!"); throw new Error("API Error!"); } else { nRes.status(500); @@ -46,19 +49,24 @@ export default class Helpers { } if (Object.keys(nRes).length !== 0) nRes.status(200); - return res; + return true; } static async mFetch(url: string, body: object): Promise<any> { //@ts-ignore - this doesn't exist for backend, but its never used there - return fetch(url, { + let res: any = await (await fetch(url, { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json" }, body: JSON.stringify(body) - }).then(Helpers.checkApiRes); + })).json(); + if (this.checkApiRes(res) == true) + return res; + else + throw new Error("API Error!");; + } static getRandomInt(min: number, max: number) { @@ -66,4 +74,15 @@ export default class Helpers { max = Math.floor(max); return Math.floor(Math.random() * (max - min) + min); //The maximum is exclusive and the minimum is inclusive } + + static Color: Colors = { + blue: 0, + red: 1, + green: 2, + yellow: 3, + 0: 0, + 1: 1, + 2: 2, + 3: 3 + }; }
\ No newline at end of file diff --git a/helpers/helpersBack.ts b/helpers/helpersBack.ts index d594c03..5aaae77 100644 --- a/helpers/helpersBack.ts +++ b/helpers/helpersBack.ts @@ -1,10 +1,20 @@ import fetch from "node-fetch"; import { Response } from "express"; import Helpers from "./helpers"; +import { global } from "../backend/ts/index"; export { Helpers }; -export async function makeQuery(query: string, params: Array<any>): Promise<any> { - let t = await ( +export const API_RES = { + success: '{"success": true}', + failure: '{"success": false}', +}; +export async function makeQuery(query: string, params: Array<any>): Promise<Array<any>> { + // console.log("params are ", JSON.stringify({ + // apiKey: process.env.API_KEY, + // query: query, + // params: params + // })); + let t: any = await ( await fetch("https://jopek.eu/maks/szkola/apkKli/fiaFiles/fia.php", { method: "POST", headers: { @@ -18,8 +28,28 @@ export async function makeQuery(query: string, params: Array<any>): Promise<any> }) }) ).text(); - // console.log(t); - return JSON.parse(t); + try { + t = JSON.parse(t)[0]; + } catch (e) { + console.log(`dbResposnse to "${query}"`, ` is '${t}' (typeof res ${typeof t})`); + console.log(`dbResposnse as object as string: '${JSON.stringify(t)}'`); + } + for (const p in t) { + if (["data", "gameBoard"].includes(p)) + t[p] = JSON.parse(t[p]); + } + return [t]; +} + +export async function getData(gid: number): Promise<Data> { + let data = (await makeQuery("SELECT `data` FROM `fia` WHERE `id` = ?", [gid]))[0].data; + if (typeof data === "string") { + data = JSON.parse(data) as Data; + return data; + } else { + console.log(data); + throw new Error("Data is not a object"); + } } export async function mFetch(url: string, body: object, res: Response): Promise<Response> { @@ -33,6 +63,19 @@ export async function mFetch(url: string, body: object, res: Response): Promise< }).then(fRes => Helpers.checkApiRes(fRes, res)) as Promise<Response>; } +export function nextTurnEndsAt(): number { + return Date.now() + (60 * 1000); +} + +export async function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} +export async function waitForQueue(): Promise<void> { + while(global.stop === true) { + await sleep(200); + } +} + export interface Colors { blue: number; red: number; @@ -60,18 +103,49 @@ export type Data = Array<DataObject>; export interface DataObject { id: any; index: number; + ready: boolean; color: tColors; name: string; } -export interface Chequer extends Coordinates { +export interface GhostWhere { + from: "base" | "map" | "home"; + oldIndex: number; + to: "base" | "map" | "home"; + newIndex: number; +} +export interface Ghost { + where: GhostWhere; color: tColors; + coords?: Coordinates; +} +export interface Chequer { + color: tColors; + ghost?: Ghost; // number; // 1 | 2 | 3 | 4 | 5 | 6; }; - export interface Coordinates { - x: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11; - y: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11; + x: number; //0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11; + y: number; //0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11; +} +export interface Square extends Coordinates { + chequers: Array<Chequer>; + start: -1 | tColors; } -export type GameBoard = Array<Chequer>; +export interface HoBSquare extends Coordinates { + chequer: tColors; + ghost?: Ghost; // number; // 1 | 2 | 3 | 4 | 5 | 6; +} +export interface HomesOrBases { + 0: Array<HoBSquare>; + 1: Array<HoBSquare>; + 2: Array<HoBSquare>; + 3: Array<HoBSquare>; +} + +export interface GameBoard { + map: Array<Square>; + homes: HomesOrBases; + bases: HomesOrBases; +}; export interface fiaTable { id: number; data: Data; diff --git a/package.json b/package.json index eb707c4..7836d62 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "start:build-front": "tsc -p ./frontend/tsconfig.json -w", "start:build-back": "tsc -p ./backend/tsconfig.json -w", "start:start": "nodemon ./backend/js/backend/ts/index.js", - "start": "concurrently \"yarn:start:*\"" + "start": "concurrently yarn:start:*" }, "dependencies": { "dotenv": "^8.2.0", diff --git a/public/css/fia.css b/public/css/fia.css new file mode 100644 index 0000000..fc660aa --- /dev/null +++ b/public/css/fia.css @@ -0,0 +1,77 @@ +.game-table { + width: 660px; + height: 660px; + margin: 0 auto; + background-image: url("https://jopek.eu/maks/szkola/apkKli/fiaFiles/fia.png"); + background-size: contain; + border-collapse: separate; + padding: 0; + border-spacing: 0; +} +.game-table tr { + padding: 0; + margin: 0; +} +.game-table td { + margin: 0; + padding: 0; + width: 60px; + height: 60px; + border-collapse: separate; +} +#timer { + height: 80px; + width: 160px; + border: 2px solid red; +} +#dice { + margin: 50px auto; + height: 100px; + width: 100px; + border: 2px solid blue; + background-size: contain; +} +.dice-1 { + background-image: url("https://jopek.eu/maks/szkola/apkKli/fiaFiles/dice-1.png"); +} +.dice-2 { + background-image: url("https://jopek.eu/maks/szkola/apkKli/fiaFiles/dice-2.png"); +} +.dice-3 { + background-image: url("https://jopek.eu/maks/szkola/apkKli/fiaFiles/dice-3.png"); +} +.dice-4 { + background-image: url("https://jopek.eu/maks/szkola/apkKli/fiaFiles/dice-4.png"); +} +.dice-5 { + background-image: url("https://jopek.eu/maks/szkola/apkKli/fiaFiles/dice-5.png"); +} +.dice-6 { + background-image: url("https://jopek.eu/maks/szkola/apkKli/fiaFiles/dice-6.png"); +} + +.player-name :nth-child(even) { + float: right; +} + +.chequer { + background-size: contain; + border-radius: 50%; + color: palevioletred; +} +.chequer-red { + background-image: url("https://jopek.eu/maks/szkola/apkKli/fiaFiles/chequer-red.png"); + border: 4px solid black; +} +.chequer-blue { + background-image: url("https://jopek.eu/maks/szkola/apkKli/fiaFiles/chequer-blue.png"); + border: 4px solid black; +} +.chequer-green { + background-image: url("https://jopek.eu/maks/szkola/apkKli/fiaFiles/chequer-green.png"); + border: 4px solid black; +} +.chequer-yellow { + background-image: url("https://jopek.eu/maks/szkola/apkKli/fiaFiles/chequer-yellow.png"); + border: 4px solid black; +} diff --git a/public/index.html b/public/index.html index d7ea523..a5b174a 100644 --- a/public/index.html +++ b/public/index.html @@ -7,6 +7,7 @@ <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Fia</title> <link rel="stylesheet" href="css/bootstrap.min.css"> + <link rel="stylesheet" href="css/fia.css"> </head> <body class="bg-dark text-white"> diff --git a/public/map.html b/public/map.html index 23ef7de..980cebe 100644 --- a/public/map.html +++ b/public/map.html @@ -2,13 +2,13 @@ <table class="table table-dark table-sm"> <tbody> <tr> - <td class="player-name rounded-pill text-center"></td> + <td class="player-name rounded-pill text-center"><span></span><span id="0-player-time"></span></td> <td></td> - <td class="player-name rounded-pill text-center"></td> + <td class="player-name rounded-pill text-center"><span></span><span id="1-player-time"></span></td> <td></td> - <td class="player-name rounded-pill text-center"></td> + <td class="player-name rounded-pill text-center"><span></span><span id="2-player-time"></span></td> <td></td> - <td class="player-name rounded-pill text-center"></td> + <td class="player-name rounded-pill text-center"><span></span><span id="3-player-time"></span></td> <td></td> <td class="text-center rounded-pill"> <input class="form-check-input" type="checkbox" /> @@ -18,9 +18,14 @@ </tbody> </table> <div class="container-flex text-center"> - <canvas id="map" width="660" height="660ZZZZ" class="border border-danger"> - Din webbläsare stöder inte canvas, vilket betyder att du inte kan spela - fia - </canvas> + <div class="row"> + <div class="col-8"> + <table class="game-table" id="map"></table> + </div> + <div class="col-2"> + <div id="dice"></div> + <button type="button" class="btn btn-light" style="display: none;">Kasta tärning</button> + </div> + </div> </div> </div> |
