You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
275 lines
7.5 KiB
275 lines
7.5 KiB
const uuid = require('node-uuid');
|
|
const UrlParser = require('url');
|
|
const DEBUG = (process.env.NODE_ENV !== "production");
|
|
|
|
const STATE = {
|
|
COUNTDOWN: 'COUNTDOWN',
|
|
PLAY: 'PLAY',
|
|
REPLAY: 'REPLAY'
|
|
};
|
|
|
|
const Ricochet = function({ messenger }) {
|
|
this.messenger = messenger;
|
|
this.squaresPerSide = 20;
|
|
|
|
// Properties that will be emitted
|
|
this.players = {};
|
|
this.robots = this.freshRobots();
|
|
this.state = STATE.PLAY;
|
|
this.walls = this.freshWalls();
|
|
this.objective = this.freshObjective();
|
|
|
|
this.countdownTimer = null;
|
|
this.countdownTimestamp = null;
|
|
|
|
this.winningPlayerId = null;
|
|
this.winningStack = null;
|
|
};
|
|
|
|
//===== Connection management
|
|
|
|
Ricochet.prototype.onConnect = function(ws, req) {
|
|
const url = UrlParser.parse(req.url, true);
|
|
const query = url.query;
|
|
const santizedName = (query.name || 'Unknown').replace(/[^\w ]/g, '');
|
|
|
|
DEBUG && console.log(`Connected: ${santizedName} (${ws.id.substr(0, 8)}) via ${req.url}`);
|
|
|
|
this.addPlayer(ws.id, santizedName);
|
|
|
|
this.messenger.messageOne(ws, { type: 'connected', body: ws.id});
|
|
this.messenger.messageAll({ type: 'players', body: this.players });
|
|
this.messenger.messageOne(ws, { type: 'robots', body: this.robots});
|
|
this.messenger.messageOne(ws, { type: 'objective', body: this.objective});
|
|
this.messenger.messageOne(ws, { type: 'state', body: this.state});
|
|
this.messenger.messageOne(ws, { type: 'walls', body: this.walls});
|
|
};
|
|
|
|
Ricochet.prototype.onDisconnect = function(ws) {
|
|
DEBUG && console.log(`Disconnected: ${this.players[ws.id]} (${ws.id.substr(0, 8)})`);
|
|
|
|
this.removePlayer(ws.id);
|
|
|
|
this.messenger.messageAll({ type: 'players', body: this.players });
|
|
};
|
|
|
|
Ricochet.prototype.addPlayer = function(id, name) {
|
|
if (!this.players[id]) {
|
|
this.players[id] = name;
|
|
}
|
|
};
|
|
|
|
Ricochet.prototype.removePlayer = function(id) {
|
|
this.players[id] = undefined;
|
|
delete this.players[id];
|
|
};
|
|
|
|
|
|
//===== State generators
|
|
|
|
Ricochet.prototype.freshRobots = function() {
|
|
const robots = ['#E00000', '#00C000', '#0000FF', '#00C0C0', '#F000F0'];
|
|
const icons = ['assets/comet.svg', 'assets/moon.svg', 'assets/planet.svg', 'assets/rocket.svg', 'assets/spacesuit.svg'];
|
|
// spider.svg, ufo.svg
|
|
|
|
return robots.map((color, idx) => ({
|
|
i: this.randomSquare(),
|
|
j: this.randomSquare(),
|
|
color,
|
|
id: uuid.v4(),
|
|
icon: icons[idx]
|
|
}));
|
|
};
|
|
|
|
Ricochet.prototype.freshWalls = function() {
|
|
// Edge IDs are of the form [i1-j1-i2-j2]. Top left is 0, 0.
|
|
|
|
// Leave here for testing.
|
|
// return [
|
|
// "1-9-1-10",
|
|
// "9-1-10-1",
|
|
// "9-19-10-19",
|
|
// "19-9-19-10"
|
|
// ];
|
|
|
|
// console.log("Generating walls.");
|
|
|
|
// Squares per side has quadratic relationship with wall/corner requirements.
|
|
const numberOfCorners = Math.ceil(Math.pow((this.squaresPerSide / 10), 2));
|
|
const numberOfWalls = Math.ceil(Math.pow((this.squaresPerSide / 5), 2));
|
|
|
|
const edges = [];
|
|
|
|
// DO NUMBER OF CORNERS FIRST AFTER TESTING
|
|
for (let n = 0; n < numberOfWalls; n++) {
|
|
const ri = this.randomSquare();
|
|
const rj = this.randomSquare();
|
|
|
|
|
|
const isHorizontal = Math.random() < 0.5;
|
|
const isBackward = Math.random() < 0.5;
|
|
|
|
let i1, j1, i2, j2;
|
|
|
|
if (isHorizontal) {
|
|
i1 = isBackward ? ri - 1 : ri;
|
|
i2 = isBackward ? ri : ri + 1;
|
|
|
|
j1 = rj;
|
|
j2 = rj;
|
|
} else {
|
|
i1 = ri;
|
|
i2 = ri;
|
|
|
|
j1 = isBackward ? rj - 1 : rj;
|
|
j2 = isBackward ? rj : rj + 1;
|
|
}
|
|
|
|
const edge = `${i1}-${j1}-${i2}-${j2}`;
|
|
|
|
if (edges.includes(edge)) {
|
|
n--;
|
|
} else {
|
|
edges.push(edge);
|
|
}
|
|
}
|
|
|
|
return edges;
|
|
};
|
|
|
|
Ricochet.prototype.freshObjective = function() {
|
|
const isValid = ({ i, j }) => {
|
|
// Square has a robot on it.
|
|
for (let k = 0; k < this.robots.length; k++) {
|
|
if (this.robots[k].i === i && this.robots[k].j === j) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
const objective = { i: this.robots[0].i, j: this.robots[0].j };
|
|
let counter = 0;
|
|
|
|
while (isValid(objective) === false && counter < 20) {
|
|
counter++;
|
|
objective.i = this.randomSquare();
|
|
objective.j = this.randomSquare();
|
|
}
|
|
|
|
const rand = Math.floor(Math.random() * this.robots.length);
|
|
objective.id = this.robots[0].id;
|
|
// objective.id = this.robots[rand].id;
|
|
return objective;
|
|
};
|
|
|
|
Ricochet.prototype.onMessage = function(ws, rawBody) {
|
|
try {
|
|
const message = JSON.parse(rawBody);
|
|
|
|
DEBUG && console.log('Received message: ');
|
|
DEBUG && console.log(message);
|
|
|
|
if (!message.type) {
|
|
DEBUG && console.warn("Unprocessable message: ")
|
|
DEBUG && console.warn(message);
|
|
return;
|
|
}
|
|
|
|
switch (message.type) {
|
|
case 'objective': this.msgObjective(); break;
|
|
case 'robots': this.msgRobots(); break;
|
|
case 'skip': this.msgSkip(); break;
|
|
case 'solve': this.msgSolve(message.rawBody); break;
|
|
case 'walls': this.msgWalls(); break;
|
|
default: console.warn("Unknown message type: ", message.type)
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
};
|
|
|
|
//===== Message handlers
|
|
Ricochet.prototype.msgObjective = function() {
|
|
this.objective = this.freshObjective();
|
|
this.messenger.messageAll({ type: 'objective', body: this.objective });
|
|
};
|
|
|
|
Ricochet.prototype.msgRobots = function() {
|
|
this.robots = this.freshRobots();
|
|
this.messenger.messageAll({ type: 'robots', body: this.robots});
|
|
};
|
|
|
|
Ricochet.prototype.msgWalls = function() {
|
|
this.walls = this.freshWalls();
|
|
this.messenger.messageAll({ type: 'walls', body: this.walls });
|
|
};
|
|
|
|
Ricochet.prototype.msgSkip = function() {
|
|
this.onCountdownComplete();
|
|
};
|
|
|
|
Ricochet.prototype.msgSolve = function(message) {
|
|
clearTimeout(this.countdownTimer);
|
|
|
|
const duration = 15;
|
|
|
|
this.countdownTimer = setTimeout(this.onCountdownComplete.bind(this), duration * 1000);
|
|
this.countdownTimestamp = new Date().getTime();
|
|
|
|
this.state = STATE.COUNTDOWN;
|
|
|
|
this.winningStack = this.sanitizeStack(message.stack);
|
|
|
|
this.winningPlayerId = this.sanitizeId(message.id);
|
|
|
|
console.log("=========", this.winningStack.length, this.robots.length);
|
|
|
|
this.messenger.messageAll({
|
|
type: 'countdown',
|
|
body: {
|
|
duration,
|
|
id: this.winningPlayerId,
|
|
moveCount: this.winningStack.length - this.robots.length,
|
|
timestamp: this.countdownTimestamp
|
|
}
|
|
});
|
|
};
|
|
|
|
//===== Helper functions
|
|
|
|
Ricochet.prototype.randomSquare = function() {
|
|
return Math.floor(Math.random() * this.squaresPerSide);
|
|
};
|
|
|
|
Ricochet.prototype.sanitizeStack = function(rawMoveStack) {
|
|
const sanitizedStack = rawMoveStack.map(move => {
|
|
const sanitizedRobotId = this.sanitizeId(move.id);
|
|
const sanitizedI = move.i * 1;
|
|
const sanitizedJ = move.j * 1;
|
|
|
|
return {
|
|
i: sanitizedI,
|
|
j: sanitizedJ,
|
|
id: sanitizedRobotId
|
|
};
|
|
});
|
|
|
|
return sanitizedStack;
|
|
};
|
|
|
|
Ricochet.prototype.sanitizeId = function(id) {
|
|
return id.replace(/[^0-9a-zA-Z\-]/g, '');
|
|
};
|
|
|
|
Ricochet.prototype.onCountdownComplete = function() {
|
|
clearTimeout(this.countdownTimer);
|
|
|
|
this.countdownTimestamp = null;
|
|
this.state = STATE.REPLAY;
|
|
|
|
this.messenger.messageAll({ type: 'win', body: { id: this.winningPlayerId, stack: this.winningStack } });
|
|
};
|
|
|
|
module.exports = Ricochet;
|
|
|