diff --git a/client/public/assets/gfx/sheep.png b/client/public/assets/gfx/sheep.png new file mode 100755 index 0000000..ff93c3b Binary files /dev/null and b/client/public/assets/gfx/sheep.png differ diff --git a/client/src/App.js b/client/src/App.js index 839b692..83fc084 100755 --- a/client/src/App.js +++ b/client/src/App.js @@ -28,10 +28,12 @@ import { Winner } from "./Winner.js"; import { HouseRules } from "./HouseRules.js"; import { Dice } from "./Dice.js"; import { assetsPath } from "./Common.js"; +import { Sheep } from "./Sheep.js"; import history from "./history.js"; import "./App.css"; import equal from "fast-deep-equal"; +import { purple } from "@material-ui/core/colors"; /* const Pip = () => {
{ setDirection(Math.floor(birdAngles * newAngle / 360.)); }, [time, setCell, speed, rotation, setDirection]); - useEffect(() => { - - }, [angle]); - return
{ }}/>
; + } else if (tile.type === 'sheep') { + div =
+
; } else { div = { + // Use useRef for mutable variables that we want to persist + // without triggering a re-render on their change + const requestRef = React.useRef(); + + const animate = time => { + callback(time) + requestRef.current = requestAnimationFrame(animate); + } + + React.useEffect(() => { + requestRef.current = requestAnimationFrame(animate); + return () => cancelAnimationFrame(requestRef.current); + }, []); // Make sure the effect runs only once +} + +const Sheep = ({ radius, speed, size, style }) => { + const [time, setTime] = useState(0); + const [direction, setDirection] = useState(Math.random() * 2 * Math.PI); + const [y, setY] = useState((Math.random() - 0.5) * radius); + const [frame, setFrame] = useState(0); + const [x, setX] = useState((Math.random() - 0.5) * radius); + + const previousTimeRef = React.useRef(); + + useAnimationFrame(time => { + if (previousTimeRef.current !== undefined) { + const deltaTime = time - previousTimeRef.current; + previousTimeRef.current = time; + setTime(deltaTime); + } else { + previousTimeRef.current = time; + } + }); + + useEffect(() => { + let alpha = time / speed; + const sheepSpeed = 0.05; + if (alpha > 1.0) { + alpha = 0.1; + } + let newX = x + sheepSpeed * Math.sin(direction) * alpha, + newY = y + sheepSpeed * Math.cos(direction) * alpha; + if (Math.sqrt((newX * newX) + (newY * newY)) > Math.sqrt(radius * radius)) { + let newDirection = direction + Math.PI + 0.5 * (Math.random() - 0.5) * Math.PI; + while (newDirection >= 2 * Math.PI) { + newDirection -= 2 * Math.PI; + } + while (newDirection <= -2 * Math.PI) { + newDirection += 2 * Math.PI; + } + setDirection(newDirection); + newX += sheepSpeed * Math.sin(newDirection) * alpha; + newY += sheepSpeed * Math.cos(newDirection) * alpha; + } + setX(newX); + setY(newY); + setFrame(frame + sheepSteps * alpha); + }, [time, speed, setDirection]); + + const cell = Math.floor(frame) % sheepSteps; + + return
0 ? +1 : -1}, 1)`, + ...style + }} + >
; +}; + + + +const Herd = ({count, style}) => { + const [sheep, setSheep] = useState([]); + useEffect(() => { + const tmp = []; + for (let i = 0; i < count; i++) { + const scalar = Math.random(); + tmp.push() + } + setSheep(tmp); + }, [count, setSheep]); + + return
{ sheep }
; +}; + +export { Sheep, Herd }; diff --git a/original/sheep-alpha.xcf b/original/sheep-alpha.xcf new file mode 100755 index 0000000..346169f Binary files /dev/null and b/original/sheep-alpha.xcf differ diff --git a/original/sheep.png b/original/sheep.png new file mode 100755 index 0000000..ff93c3b Binary files /dev/null and b/original/sheep.png differ diff --git a/server/ai/app.js b/server/ai/app.js index da5bbff..c7d9206 100644 --- a/server/ai/app.js +++ b/server/ai/app.js @@ -1,5 +1,6 @@ const fetch = require('node-fetch'); const WebSocket = require('ws'); +const fs = require('fs').promises; const version = '0.0.1'; @@ -18,7 +19,7 @@ For example: const server = process.argv[2]; const gameId = process.argv[3]; let session = undefined; -const user = process.argv[4]; +const name = process.argv[4]; const game = {}; @@ -30,19 +31,29 @@ const error = (e) => { const connect = async () => { let loc = new URL(server), new_uri; - const res = await fetch(`${server}/api/v1/games`, { - method: 'GET', - cache: 'no-cache', - credentials: 'same-origin', /* include cookies */ - headers: { - 'Content-Type': 'application/json' + let player; + try { + const data = JSON.parse(await fs.readFile(`${name}.json`, 'utf-8')); + player = data.player; + } catch (_) { + const res = await fetch(`${server}/api/v1/games`, { + method: 'GET', + cache: 'no-cache', + credentials: 'same-origin', /* include cookies */ + headers: { + 'Content-Type': 'application/json' + } + }); + if (!res) { + throw new Error(`Unable to connect to ${server}`); } - }); - if (!res) { - throw new Error(`Unable to connect to ${server}`); - } - const { player } = JSON.parse(await res.text()); + player = JSON.parse(await res.text()).player; + await fs.writeFile(`${name}.json`, JSON.stringify({ + name, + player + })); + } console.log(`Connecting to ${server} as ${player}`); if (loc.protocol === "https:") { @@ -68,52 +79,264 @@ const connect = async () => { resolve(ws); }; + const connection = (ws) => { + console.log("connection request cookie: ", ws.upgradeReq.headers.cookie); + }; + const close = (e) => { console.log(`ws - close`); }; ws.on('open', open); + ws.on('connect', () => { connect(ws); }); ws.on('headers', headers); ws.on('close', close); ws.on('error', error); - ws.on('message', (data) => { message(ws, data) }); + ws.on('message', (data) => { message(ws, data); }); }); }; +const createPlayer = (ws) => { + const send = (data) => { + ws.send(JSON.stringify(data)); + }; + + if (game.name === '') { + send({ type: 'player-name', name }); + return; + } + + if (game.state !== 'lobby') { + return; + } + + if (game.unselected.indexOf(name) === -1) { + return; + } + + const slots = []; + for (let color in game.players) { + if (game.players[color].status === 'Not active') { + slots.push(color); + } + } + if (slots.length === 0) { + return; + } + const index = Math.floor(Math.random() * slots.length); + console.log(`Requesting to play as ${slots[index]}.`); + game.unselected = game.unselected.filter( + color => color === slots[index]); + send({ + type: 'set', + field: 'color', + value: slots[index] + }); + send({ + type: 'chat', + message: `Woohoo! Robot AI ${version} is alive!` + }); +}; + +const tryBuild = (ws) => { + const send = (data) => { + console.log(`ws - send`); + ws.send(JSON.stringify(data)); + }; + let trying = false; + if (game.private.wood + && game.private.brick + && game.private.sheep + && game.private.wheat) { + send({ + type: 'buy-settlement' + }); + trying = true; + } + + if (game.private.wood && game.private.brick) { + send({ + type: 'buy-road' + }); + trying = true; + } + + return trying; +}; + const message = (ws, data) => { + const send = (data) => { + console.log(`ws - send: ${data.type}`); + ws.send(JSON.stringify(data)); + }; + data = JSON.parse(data); switch (data.type) { case 'game-update': + console.log(`ws - receive - `, + Object.assign({}, data.update, { + activities: 'filtered out', + chat: 'filtered out' + }) + ); + Object.assign(game, data.update); - if (game.name === '') { - ws.send(JSON.stringify({ type: 'player-name', name: user })); - } - if (game.state === 'lobby' && game.unselected.indexOf(user) !== -1) { - const slots = []; - for (let color in game.players) { - if (game.players[color].status === 'Not active') { - slots.push(color); + console.log(`state - ${game.state}`); + + switch (game.state) { + case undefined: + case 'lobby': + createPlayer(ws); + break; + + case 'game-order': + if (!game.color) { + console.log(`game-order - player not active`); + return; + } + console.log(`game-order - `, { + color: game.color, + players: game.players + }); + if (!game.players[game.color].orderRoll) { + console.log(`Time to roll as ${game.color}`); + send({ type: 'roll' }); + } + break; + + case 'initial-placement': { + console.log({ color: game.color, state: game.state, turn: game.turn }); + if (game.turn.color !== game.color) { + break; + } + + let index; + const type = game.turn.actions[0]; + if (type === 'place-road') { + console.log({ roads: game.turn.limits.roads }); + index = game.turn.limits.roads[Math.floor( + Math.random() * game.turn.limits.roads.length)]; + } else if (type === 'place-settlement') { + console.log({ corners: game.turn.limits.corners }); + index = game.turn.limits.corners[Math.floor( + Math.random() * game.turn.limits.corners.length)]; + } + console.log(`Selecting ${type} at ${index}`); + send({ + type, index + }); + } break; + + case 'normal': + if (game.turn.color !== game.color) { + return; + } + if (game.turn.actions && game.turn.actions.indexOf('place-road') !== -1) { + index = game.turn.limits.roads[Math.floor( + Math.random() * game.turn.limits.roads.length)]; + send({ + type: 'place-road', index + }); + return; + } + + if (game.turn.actions && game.turn.actions.indexOf('place-settlement') !== -1) { + console.log({ corners: game.turn.limits.corners }); + index = game.turn.limits.corners[Math.floor( + Math.random() * game.turn.limits.corners.length)]; + send({ + type: 'place-settlement', index + }); + return; + } + + if (!game.dice) { + console.log(`Rolling...`); + send({ + type: 'roll' + }); + return; + } + + if (game.private.mustDiscard) { + let mustDiscard = game.private.mustDiscard; + const cards = [], + discards = {}; + const types = ['wheat', 'sheep', 'stone', 'brick', 'wood']; + types.forEach(type => { + for (let i = 0; i < game.private[type]; i++) { + cards.push(type); + } + }); + while (mustDiscard--) { + const type = cards[Math.floor(Math.random() * cards.length)]; + if (!(type in discards)) { + discards[type] = 1; + } else { + discards[type]++; + } + } + console.log(`discarding - `, discards); + send({ + type: 'discard', + discards + }); + return; + } + + if (game.turn.actions + && game.turn.actions.indexOf('place-robber') !== -1) { + console.log({ pips: game.turn.limits.pips }); + const index = game.turn.limits.pips[Math.floor(Math.random() * game.turn.limits.pips.length)]; + console.log(`placing robber - ${index}`) + send({ + type: 'place-robber', + index + }); + return; + } + + if (game.turn.actions && game.turn.actions.indexOf('steal-resource') !== -1) { + const { color } = game.turn.limits.players[Math.floor(Math.random() * game.turn.limits.players.length)]; + console.log(`stealing resouce from ${game.players[color].name}`); + send({ + type: 'steal-resource', + color + }); + return; + } + + if (game.turn.robberInAction) { + console.log({ turn: game.turn }); + } else { + console.log({ + turn: game.turn, + wheat: game.private.wheat, + sheep: game.private.sheep, + stone: game.private.stone, + brick: game.private.brick, + wood: game.private.wood, + }); + if (!tryBuild(ws)) { + send({ + type: 'pass' + }); } } - if (slots.length !== 0) { - const index = Math.floor(Math.random() * slots.length); - console.log(`Requesting to play as ${slots[index]}.`); - game.unselected = game.unselected.filter( - color => color === slots[index]); - ws.send(JSON.stringify({ - type: 'set', - field: 'color', - value: slots[index] - })); - ws.send(JSON.stringify({ - type: 'chat', - message: `Woohoo! Robot AI ${version} is alive!` - })); - } - } + break; + default: + console.log({ state: game.state, turn: game.turn }); + break; + } break; case 'ping': + if (!game.state) { + console.log(`ping received with no game. Sending update request`); + ws.send(JSON.stringify({ + type: 'game-update' + })); + } break; default: @@ -123,14 +346,10 @@ const message = (ws, data) => { } const ai = async (ws) => { - ws.send(JSON.stringify({ - type: 'game-update' - })); } connect().then((ws) => { - ai(ws).then(() => { - }) + ai(ws) .catch((error) => { console.error(error); ws.close(); diff --git a/server/routes/games.js b/server/routes/games.js index 8ec7834..011b4da 100755 --- a/server/routes/games.js +++ b/server/routes/games.js @@ -753,7 +753,7 @@ const canGiveBuilding = (game) => { } } -const adminActions = (game, action, value, query) => { +const adminCommands = (game, action, value, query) => { let color, player, parts, session, corners, error; switch (action) { @@ -962,6 +962,15 @@ const adminActions = (game, action, value, query) => { return; } return `Unable to find active session for ${colorToWord(color)} (${value})`; + case "state": + if (game.state !== 'lobby') { + return `Game already started.`; + } + if (game.active < 2) { + return `Not enough players in game to start.`; + } + game.state = 'game-order'; + break; default: return `Invalid admin action ${action}.`; @@ -1864,7 +1873,7 @@ router.put("/:id/:action/:value?", async (req, res) => { if (req.headers['private-token'] !== req.app.get('admin')) { error = `Invalid admin credentials.`; } else { - error = adminActions(game, action, value, req.query); + error = adminCommands(game, action, value, req.query); } if (!error) { sendGameToPlayers(game); @@ -2500,21 +2509,25 @@ const playCard = (game, session, card) => { const placeSettlement = (game, session, index) => { const player = session.player; index = parseInt(index); + if (game.state !== 'initial-placement' && game.state !== 'normal') { - return `You cannot purchase a development card unless the game is active (${game.state}).`; + return `You cannot place a settlement unless the game is active (${game.state}).`; } + if (session.color !== game.turn.color) { return `It is not your turn! It is ${game.turn.name}'s turn.`; } + /* index out of range... */ if (game.placements.corners[index] === undefined) { return `You have requested to place a settlement illegally!`; } + /* If this is not a valid road in the turn limits, discard it */ - if (game.turn - && game.turn.limits - && game.turn.limits.corners - && game.turn.limits.corners.indexOf(index) === -1) { + if (!game.turn + || !game.turn.limits + || !game.turn.limits.corners + || game.turn.limits.corners.indexOf(index) === -1) { return `You tried to cheat! You should not try to break the rules.`; } const corner = game.placements.corners[index]; @@ -2650,19 +2663,26 @@ const placeRoad = (game, session, index) => { const player = session.player; index = parseInt(index); if (game.state !== 'initial-placement' && game.state !== 'normal') { - return `You cannot purchase a development card unless the game is active (${game.state}).`; + return `You cannot purchase a place a road unless the game is active (${game.state}).`; } + if (session.color !== game.turn.color) { return `It is not your turn! It is ${game.turn.name}'s turn.`; } + /* Valid index location */ if (game.placements.roads[index] === undefined) { return `You have requested to place a road illegally!`; } + /* If this is not a valid road in the turn limits, discard it */ - if (game.turn && game.turn.limits && game.turn.limits.roads && game.turn.limits.roads.indexOf(index) === -1) { + if (!game.turn + || !game.turn.limits + || !game.turn.limits.roads + || game.turn.limits.roads.indexOf(index) === -1) { return `You tried to cheat! You should not try to break the rules.`; } + const road = game.placements.roads[index]; if (road.color) { return `This location already has a road belonging to ${game.players[road.color].name}!`; @@ -2930,6 +2950,9 @@ const discard = (game, session, discards) => { } sum += parseInt(discards[type]); } + if (sum > player.mustDiscard) { + return `You can not discard that many cards! You can only discard ${player.mustDiscard}.`; + } /* if (sum !== player.mustDiscard) { return `You need to discard ${player.mustDiscard} cards.`; @@ -2943,13 +2966,13 @@ const discard = (game, session, discards) => { player.resources -= count; } addChatMessage(game, null, `${session.name} discarded ${sum} resource cards.`); - if (player.mustDiscard) { + if (player.mustDiscard > 0) { addChatMessage(game, null, `${session.name} did not discard enough and must discard ${player.mustDiscard} more cards.`); } let move = true; for (let color in game.players) { - const discard = game.players[color].mustDiscard; + const discard = game.players[color].mustDiscard > 0; if (discard) { move = false; } @@ -3231,11 +3254,15 @@ const placeCity = (game, session, index) => { if (session.color !== game.turn.color) { return `It is not your turn! It is ${game.turn.name}'s turn.`; } + /* Valid index check */ if (game.placements.corners[index] === undefined) { return `You have requested to place a city illegally!`; } /* If this is not a placement the turn limits, discard it */ - if (game.turn && game.turn.limits && game.turn.limits.corners && game.turn.limits.corners.indexOf(index) === -1) { + if (!game.turn + || !game.turn.limits + || !game.turn.limits.corners + || game.turn.limits.corners.indexOf(index) === -1) { return `You tried to cheat! You should not try to break the rules.`; } const corner = game.placements.corners[index]; @@ -4371,11 +4398,15 @@ router.ws("/ws/:id", async (ws, req) => { resetDisconnectCheck(game, req); console.log(`${short}: Game ${id} - WebSocket connect from ${getName(session)}`); - - if (session.keepAlive) { + + /* Send initial ping to initiate communication with client */ + if (!session.keepAlive) { + console.log(`${short}: Sending initial ping`); + ping(session); + } else { clearTimeout(session.keepAlive); + session.keepAlive = setTimeout(() => { ping(session); }, 2500); } - session.keepAlive = setTimeout(() => { ping(session); }, 2500); }); const debugChat = (game, preamble) => {