1
0

Add sheeps

Signed-off-by: James Ketrenos <james_eikona@ketrenos.com>
This commit is contained in:
James Ketrenos 2022-06-23 14:07:53 -07:00
parent 6a861e7841
commit 588777af90
10 changed files with 440 additions and 61 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -28,10 +28,12 @@ import { Winner } from "./Winner.js";
import { HouseRules } from "./HouseRules.js"; import { HouseRules } from "./HouseRules.js";
import { Dice } from "./Dice.js"; import { Dice } from "./Dice.js";
import { assetsPath } from "./Common.js"; import { assetsPath } from "./Common.js";
import { Sheep } from "./Sheep.js";
import history from "./history.js"; import history from "./history.js";
import "./App.css"; import "./App.css";
import equal from "fast-deep-equal"; import equal from "fast-deep-equal";
import { purple } from "@material-ui/core/colors";
/* /*
const Pip = () => { const Pip = () => {
<div className="Pip" <div className="Pip"

View File

@ -50,10 +50,6 @@ const Bird = ({ origin, radius, speed, size, style }) => {
setDirection(Math.floor(birdAngles * newAngle / 360.)); setDirection(Math.floor(birdAngles * newAngle / 360.));
}, [time, setCell, speed, rotation, setDirection]); }, [time, setCell, speed, rotation, setDirection]);
useEffect(() => {
}, [angle]);
return <div className={`Bird`} return <div className={`Bird`}
style={{ style={{
top: `${50 + 100 * radius * Math.sin(2 * Math.PI * (180 + angle) / 360.)}%`, top: `${50 + 100 * radius * Math.sin(2 * Math.PI * (180 + angle) / 360.)}%`,

View File

@ -4,6 +4,7 @@ import { assetsPath } from "./Common.js";
import "./Board.css"; import "./Board.css";
import { GlobalContext } from "./GlobalContext.js"; import { GlobalContext } from "./GlobalContext.js";
import { Flock } from "./Bird.js"; import { Flock } from "./Bird.js";
import { Herd } from "./Sheep.js";
const rows = [3, 4, 5, 4, 3, 2]; /* The final row of 2 is to place roads and corners */ const rows = [3, 4, 5, 4, 3, 2]; /* The final row of 2 is to place roads and corners */
@ -546,6 +547,17 @@ const Board = () => {
}}/><Tile }}/><Tile
tile={tile} tile={tile}
/></div>; /></div>;
} else if (tile.type === 'sheep') {
div = <div key={`tile-${order}`}>
<Herd count={Math.floor(1 + animationSeeds[index] * 4)}
style={{
top: `${tile.top - tileImageHeight * 0.5}px`,
left: `${tile.left - tileImageWidth * 0.5}px`,
width: `${tileImageWidth}px`,
height: `${tileImageHeight}px`
}} /><Tile
tile={tile}
/></div>;
} else { } else {
div = <Tile div = <Tile
key={`tile-${order}`} key={`tile-${order}`}

12
client/src/Sheep.css Normal file
View File

@ -0,0 +1,12 @@
.Herd {
z-index: 25; /* below pips */
position: absolute;
pointer-events: none;
}
.Sheep {
z-index: 50; /* Above Tile (5,10), Road (12), Corner (20) */
position: absolute;
background-size: 1200% auto;
background-repeat: no-repeat;
}

107
client/src/Sheep.js Normal file
View File

@ -0,0 +1,107 @@
import React, { useEffect, useState } from "react";
import { assetsPath } from "./Common.js";
import "./Sheep.css";
const
sheepSteps = 12;
const useAnimationFrame = callback => {
// 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 <div className={`Sheep`}
style={{
top: `${Math.floor(50 + 50 * y)}%`,
left: `${Math.floor(50 + 50 * x)}%`,
width: `${size * 60}px`,
height: `${size * 52}px`,
backgroundRepeat: 'no-repeat',
backgroundImage: `url(${assetsPath}/gfx/sheep.png)`,
backgroundPositionX: `${100. * cell / (sheepSteps - 1)}%`,
transformOrigin: `50% 50%`,
transform: `translate(-50%, -50%) scale(${Math.sin(direction) > 0 ? +1 : -1}, 1)`,
...style
}}
></div>;
};
const Herd = ({count, style}) => {
const [sheep, setSheep] = useState([]);
useEffect(() => {
const tmp = [];
for (let i = 0; i < count; i++) {
const scalar = Math.random();
tmp.push(<Sheep
speed={1000. + 500 * scalar}
size={0.25}
radius={0.8}
key={i}
/>)
}
setSheep(tmp);
}, [count, setSheep]);
return <div className="Herd" style={style}>{ sheep }</div>;
};
export { Sheep, Herd };

BIN
original/sheep-alpha.xcf Executable file

Binary file not shown.

BIN
original/sheep.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -1,5 +1,6 @@
const fetch = require('node-fetch'); const fetch = require('node-fetch');
const WebSocket = require('ws'); const WebSocket = require('ws');
const fs = require('fs').promises;
const version = '0.0.1'; const version = '0.0.1';
@ -18,7 +19,7 @@ For example:
const server = process.argv[2]; const server = process.argv[2];
const gameId = process.argv[3]; const gameId = process.argv[3];
let session = undefined; let session = undefined;
const user = process.argv[4]; const name = process.argv[4];
const game = {}; const game = {};
@ -30,6 +31,11 @@ const error = (e) => {
const connect = async () => { const connect = async () => {
let loc = new URL(server), new_uri; let loc = new URL(server), new_uri;
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`, { const res = await fetch(`${server}/api/v1/games`, {
method: 'GET', method: 'GET',
cache: 'no-cache', cache: 'no-cache',
@ -42,7 +48,12 @@ const connect = async () => {
throw new Error(`Unable to connect to ${server}`); 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}`); console.log(`Connecting to ${server} as ${player}`);
if (loc.protocol === "https:") { if (loc.protocol === "https:") {
@ -68,52 +79,264 @@ const connect = async () => {
resolve(ws); resolve(ws);
}; };
const connection = (ws) => {
console.log("connection request cookie: ", ws.upgradeReq.headers.cookie);
};
const close = (e) => { const close = (e) => {
console.log(`ws - close`); console.log(`ws - close`);
}; };
ws.on('open', open); ws.on('open', open);
ws.on('connect', () => { connect(ws); });
ws.on('headers', headers); ws.on('headers', headers);
ws.on('close', close); ws.on('close', close);
ws.on('error', error); ws.on('error', error);
ws.on('message', (data) => { message(ws, data) }); ws.on('message', (data) => { message(ws, data); });
}); });
}; };
const message = (ws, data) => { const createPlayer = (ws) => {
data = JSON.parse(data); const send = (data) => {
switch (data.type) { ws.send(JSON.stringify(data));
case 'game-update': };
Object.assign(game, data.update);
if (game.name === '') { if (game.name === '') {
ws.send(JSON.stringify({ type: 'player-name', name: user })); send({ type: 'player-name', name });
return;
} }
if (game.state === 'lobby' && game.unselected.indexOf(user) !== -1) {
if (game.state !== 'lobby') {
return;
}
if (game.unselected.indexOf(name) === -1) {
return;
}
const slots = []; const slots = [];
for (let color in game.players) { for (let color in game.players) {
if (game.players[color].status === 'Not active') { if (game.players[color].status === 'Not active') {
slots.push(color); slots.push(color);
} }
} }
if (slots.length !== 0) { if (slots.length === 0) {
return;
}
const index = Math.floor(Math.random() * slots.length); const index = Math.floor(Math.random() * slots.length);
console.log(`Requesting to play as ${slots[index]}.`); console.log(`Requesting to play as ${slots[index]}.`);
game.unselected = game.unselected.filter( game.unselected = game.unselected.filter(
color => color === slots[index]); color => color === slots[index]);
ws.send(JSON.stringify({ send({
type: 'set', type: 'set',
field: 'color', field: 'color',
value: slots[index] value: slots[index]
})); });
ws.send(JSON.stringify({ send({
type: 'chat', type: 'chat',
message: `Woohoo! Robot AI ${version} is alive!` 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);
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'
});
}
}
break;
default:
console.log({ state: game.state, turn: game.turn });
break;
} }
break; break;
case 'ping': case 'ping':
if (!game.state) {
console.log(`ping received with no game. Sending update request`);
ws.send(JSON.stringify({
type: 'game-update'
}));
}
break; break;
default: default:
@ -123,14 +346,10 @@ const message = (ws, data) => {
} }
const ai = async (ws) => { const ai = async (ws) => {
ws.send(JSON.stringify({
type: 'game-update'
}));
} }
connect().then((ws) => { connect().then((ws) => {
ai(ws).then(() => { ai(ws)
})
.catch((error) => { .catch((error) => {
console.error(error); console.error(error);
ws.close(); ws.close();

View File

@ -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; let color, player, parts, session, corners, error;
switch (action) { switch (action) {
@ -962,6 +962,15 @@ const adminActions = (game, action, value, query) => {
return; return;
} }
return `Unable to find active session for ${colorToWord(color)} (${value})`; 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: default:
return `Invalid admin action ${action}.`; 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')) { if (req.headers['private-token'] !== req.app.get('admin')) {
error = `Invalid admin credentials.`; error = `Invalid admin credentials.`;
} else { } else {
error = adminActions(game, action, value, req.query); error = adminCommands(game, action, value, req.query);
} }
if (!error) { if (!error) {
sendGameToPlayers(game); sendGameToPlayers(game);
@ -2500,21 +2509,25 @@ const playCard = (game, session, card) => {
const placeSettlement = (game, session, index) => { const placeSettlement = (game, session, index) => {
const player = session.player; const player = session.player;
index = parseInt(index); index = parseInt(index);
if (game.state !== 'initial-placement' && game.state !== 'normal') { 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) { if (session.color !== game.turn.color) {
return `It is not your turn! It is ${game.turn.name}'s turn.`; return `It is not your turn! It is ${game.turn.name}'s turn.`;
} }
/* index out of range... */
if (game.placements.corners[index] === undefined) { if (game.placements.corners[index] === undefined) {
return `You have requested to place a settlement illegally!`; return `You have requested to place a settlement illegally!`;
} }
/* If this is not a valid road in the turn limits, discard it */ /* If this is not a valid road in the turn limits, discard it */
if (game.turn if (!game.turn
&& game.turn.limits || !game.turn.limits
&& game.turn.limits.corners || !game.turn.limits.corners
&& game.turn.limits.corners.indexOf(index) === -1) { || game.turn.limits.corners.indexOf(index) === -1) {
return `You tried to cheat! You should not try to break the rules.`; return `You tried to cheat! You should not try to break the rules.`;
} }
const corner = game.placements.corners[index]; const corner = game.placements.corners[index];
@ -2650,19 +2663,26 @@ const placeRoad = (game, session, index) => {
const player = session.player; const player = session.player;
index = parseInt(index); index = parseInt(index);
if (game.state !== 'initial-placement' && game.state !== 'normal') { 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) { if (session.color !== game.turn.color) {
return `It is not your turn! It is ${game.turn.name}'s turn.`; return `It is not your turn! It is ${game.turn.name}'s turn.`;
} }
/* Valid index location */
if (game.placements.roads[index] === undefined) { if (game.placements.roads[index] === undefined) {
return `You have requested to place a road illegally!`; return `You have requested to place a road illegally!`;
} }
/* If this is not a valid road in the turn limits, discard it */ /* 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.`; return `You tried to cheat! You should not try to break the rules.`;
} }
const road = game.placements.roads[index]; const road = game.placements.roads[index];
if (road.color) { if (road.color) {
return `This location already has a road belonging to ${game.players[road.color].name}!`; 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]); 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) { if (sum !== player.mustDiscard) {
return `You need to discard ${player.mustDiscard} cards.`; return `You need to discard ${player.mustDiscard} cards.`;
@ -2943,13 +2966,13 @@ const discard = (game, session, discards) => {
player.resources -= count; player.resources -= count;
} }
addChatMessage(game, null, `${session.name} discarded ${sum} resource cards.`); 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.`); addChatMessage(game, null, `${session.name} did not discard enough and must discard ${player.mustDiscard} more cards.`);
} }
let move = true; let move = true;
for (let color in game.players) { for (let color in game.players) {
const discard = game.players[color].mustDiscard; const discard = game.players[color].mustDiscard > 0;
if (discard) { if (discard) {
move = false; move = false;
} }
@ -3231,11 +3254,15 @@ const placeCity = (game, session, index) => {
if (session.color !== game.turn.color) { if (session.color !== game.turn.color) {
return `It is not your turn! It is ${game.turn.name}'s turn.`; return `It is not your turn! It is ${game.turn.name}'s turn.`;
} }
/* Valid index check */
if (game.placements.corners[index] === undefined) { if (game.placements.corners[index] === undefined) {
return `You have requested to place a city illegally!`; return `You have requested to place a city illegally!`;
} }
/* If this is not a placement the turn limits, discard it */ /* 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.`; return `You tried to cheat! You should not try to break the rules.`;
} }
const corner = game.placements.corners[index]; const corner = game.placements.corners[index];
@ -4372,10 +4399,14 @@ router.ws("/ws/:id", async (ws, req) => {
resetDisconnectCheck(game, req); resetDisconnectCheck(game, req);
console.log(`${short}: Game ${id} - WebSocket connect from ${getName(session)}`); 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); clearTimeout(session.keepAlive);
}
session.keepAlive = setTimeout(() => { ping(session); }, 2500); session.keepAlive = setTimeout(() => { ping(session); }, 2500);
}
}); });
const debugChat = (game, preamble) => { const debugChat = (game, preamble) => {