diff --git a/client/src/Board.js b/client/src/Board.js index 3a7dfef..950a936 100755 --- a/client/src/Board.js +++ b/client/src/Board.js @@ -44,6 +44,7 @@ const base = process.env.PUBLIC_URL; const assetsPath = `${base}/assets`; const gamesPath = `${base}/games`; +const images = {}; const useStyles = makeStyles((theme) => ({ root: { @@ -87,12 +88,29 @@ const hexagonRatio = 1.1547005, angle: 0 } ]; + +const loadImage = (board, file) => { + if (file in images) { + return images[file]; + } + const image = new Image(); + images[file] = image; + + image.addEventListener("load", board.imageLoaded); + image.addEventListener("error", board.imageLoadError); + + image.src = `${assetsPath}/gfx/${file}`; + + return image; +} + const Tiles = (board) => { const tiles = board.game.tiles; [ "robber", "brick", "wood", "sheep", "stone", "wheat" ].forEach((type) => { - const image = new Image(), - file = "tiles-" + type + ".png"; + const file = "tiles-" + type + ".png", + image = loadImage(board, file); + tiles.forEach((tile) => { if (tile.type === type) { tile.image = image; @@ -101,31 +119,14 @@ const Tiles = (board) => { } tile.jitter = 0; }); - image.addEventListener("load", (event) => { - console.log(`Done loading ${file}`); - window.requestAnimationFrame(board.drawFrame); - }); - image.addEventListener("error", (event) => { - this.setState({message: `Error loading ${file}`}); - }); - image.src = `${assetsPath}/gfx/${file}`; }); return tiles; }; const Pips = (board) => { - const image = new Image(), - file = 'pip-numbers.png'; - - image.addEventListener("load", (event) => { - console.log(`Done loading ${file}`); - window.requestAnimationFrame(board.drawFrame); - }); - image.addEventListener("error", (event) => { - this.setState({message: `Error loading ${file}`}); - }); - image.src = `${assetsPath}/gfx/${file}`; + const file = 'pip-numbers.png', + image = loadImage(board, file); return { image: image, @@ -134,29 +135,17 @@ const Pips = (board) => { }; const Border = (board, border) => { - const image = new Image(), file = border.file + const file = border.file, + image = loadImage(board, file); + border.image = image; - image.addEventListener("load", (event) => { - console.log(`Done loading ${file}`); - window.requestAnimationFrame(board.drawFrame); - }); - image.addEventListener("error", (event) => { - board.setState({ message: `Error loading ${file}` }); - }); - image.src = `${assetsPath}/gfx/${file}`; return border; }; const Table = (board) => { - const image = new Image(), file = "table.png"; - image.addEventListener("load", (event) => { - console.log(`Done loading ${file}`); - window.requestAnimationFrame(board.drawFrame); - }); - image.addEventListener("error", (event) => { - board.setState({ message: `Error loading ${file}` }); - }); - image.src = `${assetsPath}/gfx/${file}`; + const file = "table.png", + image = loadImage(board, file); + return image; }; @@ -418,6 +407,9 @@ class Board extends React.Component { this.rollDice = this.rollDice.bind(this); this.setGameState = this.setGameState.bind(this); this.shuffleBoard = this.shuffleBoard.bind(this); + this.updateGame = this.updateGame.bind(this); + this.imageLoadError = this.imageLoadError.bind(this); + this.imageLoaded = this.imageLoaded.bind(this); this.mouse = { x: 0, y: 0 }; this.radius = 0.317; @@ -440,6 +432,15 @@ class Board extends React.Component { this.id = (props.router && props.router.params.id) ? props.router.params.id : 0; } + imageLoaded(event) { + console.log(`Done loading ${event.target.src}`); + window.requestAnimationFrame(this.drawFrame); + } + + imageLoadError(event) { + this.setState({message: `Error loading ${event.target.src}`}); + } + shuffleBoard() { if (this.loadTimer) { window.clearTimeout(this.loadTimer); @@ -460,15 +461,8 @@ class Board extends React.Component { return res.json(); }).then((game) => { console.log (`Board shuffled!`); - this.game = game; - this.setState({ game: game, message: "" }); - this.pips = Pips(this); - this.tiles = Tiles(this); - this.table = Table(this); - - this.borders = this.game.borders.map((file) => { - return Border(this, file); - }); + this.updateGame(game); + this.setState({ game: game, message: "Board shuffled!" }); }).catch((error) => { console.error(error); this.setState({message: error.message}); @@ -500,8 +494,8 @@ class Board extends React.Component { }).then((game) => { console.log (`Dice rolled!`); console.log(game.dice); - this.game = game; - this.setState({ game: { ...this.state.game, dice: game.dice }, message: ""} ); + this.updateGame(game); + this.setState({ game: { ...this.state.game, dice: game.dice }, message: "Dice rolled!"} ); }).catch((error) => { console.error(error); this.setState({message: error.message}); @@ -537,7 +531,7 @@ class Board extends React.Component { return res.json(); }).then((game) => { console.log (`Game ${game.id} loaded ${moment().format()}.`); - this.game = game; + this.updateGame(game); this.setState({ game: game, message: "" }); }).catch((error) => { console.error(error); @@ -579,7 +573,7 @@ class Board extends React.Component { return res.json(); }).then((game) => { console.log (`Game state changed.`); - this.game = game; + this.updateGame(game); this.setState({ game: game, message: "" }); }).catch((error) => { console.error(error); @@ -613,8 +607,8 @@ class Board extends React.Component { return res.json(); }).then((game) => { console.log (`Game state set to ${game.state}!`); - this.game = game; - this.setState({ game: { ...this.state.game, state: game.state }, message: ""} ); + this.updateGame(game); + this.setState({ game: { ...this.state.game, state: game.state }, message: `Game state now ${game.state}.` }); }).catch((error) => { console.error(error); this.setState({message: error.message}); @@ -1144,6 +1138,20 @@ class Board extends React.Component { }); } + updateGame(game) { + this.game = game; + + this.pips = Pips(this); + this.tiles = Tiles(this); + this.table = Table(this); + + this.borders = this.game.borders.map((file) => { + return Border(this, file); + }); + + window.requestAnimationFrame(this.drawFrame); + } + componentDidMount() { this.start = new Date(); @@ -1159,7 +1167,7 @@ class Board extends React.Component { params.method = "GET" } else { console.log("Requesting new game."); - params.url = `${base}/api/v1/games`; + params.url = `${base}/api/v1/games/`; params.method = "POST"; } @@ -1185,7 +1193,7 @@ class Board extends React.Component { console.log(message); this.setState({ message: message }); - params.url = `${base}/api/v1/games`; + params.url = `${base}/api/v1/games/${this.id}`; params.method = "POST"; return window.fetch(params.url, { @@ -1205,16 +1213,8 @@ class Board extends React.Component { history.push(`${gamesPath}/${game.id}`); } - this.game = game; + this.updateGame(game); this.setState({ game: game, message: "" }); - - this.pips = Pips(this); - this.tiles = Tiles(this); - this.table = Table(this); - - this.borders = this.game.borders.map((file) => { - return Border(this, file); - }); }).catch((error) => { console.error(error); this.setState({message: error.message}); @@ -1233,6 +1233,13 @@ class Board extends React.Component { clearTimeout(this.updateSizeTimer); this.updateSizeTimer = 0; } + + for (let image in images) { + image.removeEventListener("load", this.imageLoaded); + image.removeEventListener("error", this.imageLoadError); + delete images[image]; + } + document.removeEventListener("keyup", this.keyUp); window.removeEventListener("mousemove", this.mouseMove); window.removeEventListener("touchmove", this.mouseMove); diff --git a/server/routes/games.js b/server/routes/games.js index b4944db..b921ea3 100755 --- a/server/routes/games.js +++ b/server/routes/games.js @@ -5,7 +5,8 @@ const express = require("express"), moment = require("moment"), crypto = require("crypto"), util = require("util"), - Promise = require("bluebird"); + Promise = require("bluebird"), + { readFile, writeFile } = require("fs").promises; let gameDB; @@ -164,14 +165,28 @@ router.put("/:id/:action/:value?", (req, res) => { return sendGame(res, req, game); }) -router.get("/:id", (req, res/*, next*/) => { +router.get("/:id", async (req, res/*, next*/) => { console.log("GET games/" + req.params.id); + let error; if (req.params.id in games) { const game = games[req.params.id]; return sendGame(res, req, game) + } + + if (/^.|\//.exec(req.params.id)) { + error = `Requested game ID is invalid`; + return res.status(400).send(error); + } + const game = await readFile(`games/${req.params.id}`) + .catch(() => { + return null; + }); + if (!game) { + error = `Unable to load game: ${req.params.id}`; + res.status(404).send(error); } else { - const error = `Game not found: ${req.params.id}`; - return res.status(404).send(error); + games[req.params.id] = game; + return sendGame(game); } }); @@ -222,7 +237,13 @@ router.put("/:id", (req, res/*, next*/) => { } }); -const sendGame = (res, req, game) => { +const sendGame = async (res, req, game) => { + await writeFile(`games/${game.id}`, JSON.stringify(game, null, 2)) + .catch((error) => { + console.error(`Unable to write to games/${games.id}`); + console.error(error); + }); + return res.status(200).send(Object.assign({}, game, { timestamp: Date.now(), activePlayer: (req.session && req.session.activePlayer) ? @@ -230,8 +251,15 @@ const sendGame = (res, req, game) => { })); } -router.post("/", (req, res/*, next*/) => { +router.post("/:id?", (req, res/*, next*/) => { console.log("POST games/"); + const id = req.params.id; + if (id && id in games) { + const error = `Can not create new game for ${id} -- it already exists.` + console.error(error); + return res.status(400).send(error); + } + const game = { startTime: Date.now(), state: "lobby", /* lobby, in-game, finished */ @@ -255,7 +283,7 @@ router.post("/", (req, res/*, next*/) => { longestRoad: null, largestArmy: null, chat: [ { from: "R", date: Date.now(), message: "Server initialized!" } ], - id: crypto.randomBytes(8).toString('hex') + id: id ? id : crypto.randomBytes(8).toString('hex') }; games[game.id] = game;