Optimize flood-fill and fix OBOB + player bug

The flood-fill had allocated a good amount of memory ~1MB each time we visit a square. This is reduced to the grid size squared, only allocated if the square we are on now is valid.

The OBOB error occured when server was updating one time too few when a player is initialized.

The player bug occurs when a new player joins the game. Normally each new player gets about 60ish frames of lag to start up. However, the new player client incorrectly assumes all other players are new as well.
This commit is contained in:
theKidOfArcrania 2017-03-01 00:56:49 +00:00
parent e8fd5d1f7c
commit 8352433fa5
7 changed files with 236 additions and 197 deletions

View File

@ -120,6 +120,7 @@ function connectServer() {
renderer.addPlayer(pl); renderer.addPlayer(pl);
}); });
user = renderer.getPlayerFromNum(data.num); user = renderer.getPlayerFromNum(data.num);
if (!user) throw new Error();
renderer.setUser(user); renderer.setUser(user);
//Load grid. //Load grid.
@ -136,7 +137,7 @@ function connectServer() {
}); });
var waiting = false; var waiting = false;
socket.on('notifyFrame', function(data, fn) { socket.on('notifyFrame', function(data) {
if (timeout != undefined) if (timeout != undefined)
clearTimeout(timeout); clearTimeout(timeout);
@ -149,6 +150,7 @@ function connectServer() {
socket.emit('requestFrame'); //Restore data. socket.emit('requestFrame'); //Restore data.
waiting = true; waiting = true;
return; return;
//TODO: cache frames when this happen.
} }
frame++; frame++;
@ -184,6 +186,19 @@ function connectServer() {
renderer.update(frame); renderer.update(frame);
var locs = {};
for (var i = 0; i < renderer.playerSize(); i++)
{
var p = renderer.getPlayer(i);
locs[p.num] = [p.posX, p.posY, p.waitLag];
}
socket.emit("verify", {
frame: frame,
locs: locs
}, function(frame, success, msg) {
if (!success) console.error(frame + ": " + msg);
}.bind(this, frame));
dirty = true; dirty = true;
requestAnimationFrame(function() { requestAnimationFrame(function() {
paintLoop(); paintLoop();
@ -191,8 +206,7 @@ function connectServer() {
timeout = setTimeout(function() { timeout = setTimeout(function() {
console.warn("Server has timed-out. Disconnecting."); console.warn("Server has timed-out. Disconnecting.");
socket.disconnect(); socket.disconnect();
}, 500); }, 3000);
fn();
}); });
socket.on('disconnect', function(){ socket.on('disconnect', function(){

View File

@ -1,6 +1,5 @@
var ANIMATE_FRAMES = 24; var ANIMATE_FRAMES = 24;
var CELL_WIDTH = 40; var CELL_WIDTH = 40;
var NEW_PLAYER_LAG = 60; //wait for a second at least.
//TODO: remove constants. //TODO: remove constants.
exports.initPlayer = function(grid, player) exports.initPlayer = function(grid, player)
@ -10,7 +9,7 @@ exports.initPlayer = function(grid, player)
if (!grid.isOutOfBounds(dr + player.row, dc + player.col)) if (!grid.isOutOfBounds(dr + player.row, dc + player.col))
grid.set(dr + player.row, dc + player.col, player); grid.set(dr + player.row, dc + player.col, player);
}; };
exports.updateFrame = function(grid, players, newPlayerFrames, dead, notifyKill, frame) exports.updateFrame = function(grid, players, dead, notifyKill)
{ {
var adead = []; var adead = [];
if (dead instanceof Array) if (dead instanceof Array)
@ -24,15 +23,7 @@ exports.updateFrame = function(grid, players, newPlayerFrames, dead, notifyKill,
//Move players. //Move players.
var tmp = players.filter(function(val) { var tmp = players.filter(function(val) {
if (!newPlayerFrames[val.num]) val.move();
newPlayerFrames[val.num] = 0;
if (newPlayerFrames[val.num] < ANIMATE_FRAMES + NEW_PLAYER_LAG)
newPlayerFrames[val.num]++;
else
//TODO: remove frame
val.move(frame);
if (val.dead) if (val.dead)
adead.push(val); adead.push(val);
return !val.dead; return !val.dead;
@ -102,12 +93,12 @@ exports.updateFrame = function(grid, players, newPlayerFrames, dead, notifyKill,
} }
tmp = tmp.filter(function(val, i) { tmp = tmp.filter(function(val, i) {
if (removing[i] && val.num !== 1) /** GHOST PLAYER **/ if (removing[i])
{ {
adead.push(val); adead.push(val);
val.die(); val.die();
} }
return !removing[i] || val.num === 1; return !removing[i];
}); });
players.length = tmp.length; players.length = tmp.length;
for (var i = 0; i < tmp.length; i++) for (var i = 0; i < tmp.length; i++)

View File

@ -37,7 +37,7 @@ $(function () {
var allowAnimation = true; var allowAnimation = true;
var animateGrid, players, allPlayers, newPlayerFrames, playerPortion, grid, var animateGrid, players, allPlayers, playerPortion, grid,
animateTo, offset, user, lagPortion, portionSpeed, zoom, kills, showedDead; animateTo, offset, user, lagPortion, portionSpeed, zoom, kills, showedDead;
grid = new Grid(GRID_SIZE, function(row, col, before, after) { grid = new Grid(GRID_SIZE, function(row, col, before, after) {
@ -63,7 +63,6 @@ function init() {
players = []; players = [];
allPlayers = []; allPlayers = [];
newPlayerFrames = [];
playerPortion = []; playerPortion = [];
animateTo = [0, 0]; animateTo = [0, 0];
@ -91,23 +90,6 @@ function paintGridBorder(ctx)
ctx.fillRect(-BORDER_WIDTH, gridWidth, gridWidth + BORDER_WIDTH * 2, BORDER_WIDTH); ctx.fillRect(-BORDER_WIDTH, gridWidth, gridWidth + BORDER_WIDTH * 2, BORDER_WIDTH);
} }
function paintGridLines(ctx)
{
ctx.fillStyle = 'lightgray';
ctx.beginPath();
for (var x = modRotate(-offset[0], CELL_WIDTH); x < gameWidth; x += CELL_WIDTH)
{
ctx.moveTo(x, 0);
ctx.lineTo(x, gameHeight);
}
for (var y = modRotate(-offset[1], CELL_WIDTH); y < gameHeight; y+= CELL_WIDTH)
{
ctx.moveTo(0, y);
ctx.lineTo(gameWidth, y);
}
ctx.stroke();
}
function paintGrid(ctx) function paintGrid(ctx)
{ {
//Paint background. //Paint background.
@ -208,6 +190,7 @@ function paintGrid(ctx)
} }
} }
function paintUIBar(ctx) function paintUIBar(ctx)
{ {
//UI Bar background //UI Bar background
@ -249,10 +232,37 @@ function paintUIBar(ctx)
if (a.portion === b.portion) return a.player.num - b.player.num; if (a.portion === b.portion) return a.player.num - b.player.num;
else return b.portion - a.portion; else return b.portion - a.portion;
}); });
var rank = sorted.findIndex(function(val) {return val.player === user}); var rank = sorted.findIndex(function(val) {return val.player === user});
ctx.fillText("Rank: " + (rank === -1 ? "--" : rank + 1) + " of " + sorted.length, ctx.fillText("Rank: " + (rank === -1 ? "--" : rank + 1) + " of " + sorted.length,
ctx.measureText(killsText).width + killsOffset + 20, CELL_WIDTH - 5); ctx.measureText(killsText).width + killsOffset + 20, CELL_WIDTH - 5);
//Show leaderboard.
var maxPortion = sorted[0] ? sorted[0].portion : 0;
var leaderboardNum = Math.min(5, sorted.length);
for (var i = 0; i < leaderboardNum; i++)
{
var player = sorted[i].player;
var name = player.name;
var portion = sorted[i].portion / maxPortion;
var nameWidth = ctx.measureText(name).width;
barSize = Math.ceil((BAR_WIDTH - MIN_BAR_WIDTH) * portion + MIN_BAR_WIDTH);
var barX = canvasWidth - barSize;
var barY = BAR_HEIGHT * (i + 1);
ctx.fillStyle = 'rgba(10, 10, 10, .3)';
ctx.fillRect(barX - 10, barY, barSize + 10, CELL_WIDTH + 10);
ctx.fillStyle = player.baseColor.rgbString();
ctx.fillRect(barX, barY, barSize, CELL_WIDTH);
ctx.fillStyle = player.shadowColor.rgbString();
ctx.fillRect(barX, barY + CELL_WIDTH, barSize, SHADOW_OFFSET);
ctx.fillStyle = "black";
ctx.fillText(name, barX - nameWidth - 5, barY + CELL_WIDTH - 10);
}
} }
//TODO: depict leaderboard. //TODO: depict leaderboard.
@ -274,7 +284,7 @@ function paint(ctx)
paintGrid(ctx); paintGrid(ctx);
players.forEach(function (p) { players.forEach(function (p) {
var fr = newPlayerFrames[p.num] || 0; var fr = p.waitLag;
if (fr < ANIMATE_FRAMES) if (fr < ANIMATE_FRAMES)
p.render(ctx, fr / ANIMATE_FRAMES); p.render(ctx, fr / ANIMATE_FRAMES);
else else
@ -343,11 +353,11 @@ function update(frame) {
portionSpeed = Math.abs(userPortion - lagPortion) / ANIMATE_FRAMES; portionSpeed = Math.abs(userPortion - lagPortion) / ANIMATE_FRAMES;
var dead = []; var dead = [];
core.updateFrame(grid, players, newPlayerFrames, dead, function addKill(killer, other) core.updateFrame(grid, players, dead, function addKill(killer, other)
{ {
if (players[killer] === user && killer !== other) if (players[killer] === user && killer !== other)
kills++; kills++;
}, frame); });
dead.forEach(function(val) { dead.forEach(function(val) {
console.log(val.name + " is dead"); console.log(val.name + " is dead");
allPlayers[val.num] = undefined; allPlayers[val.num] = undefined;
@ -359,15 +369,6 @@ function update(frame) {
} }
//Helper methods. //Helper methods.
function modRotate(val, mod)
{
var res = val % mod;
if (res >= 0)
return res;
else
return mod + res;
}
function centerOnPlayer(player, pos) function centerOnPlayer(player, pos)
{ {
var xOff = Math.floor(player.posX - (gameWidth / zoom - CELL_WIDTH) / 2); var xOff = Math.floor(player.posX - (gameWidth / zoom - CELL_WIDTH) / 2);
@ -409,7 +410,6 @@ module.exports = exports = {
if (allPlayers[player.num]) if (allPlayers[player.num])
return; //Already added. return; //Already added.
allPlayers[player.num] = players[players.length] = player; allPlayers[player.num] = players[players.length] = player;
newPlayerFrames[player.num] = 0;
playerPortion[player.num] = 0; playerPortion[player.num] = 0;
return players.length - 1; return players.length - 1;
}, },

View File

@ -2,7 +2,7 @@
var Color = require("./color"); var Color = require("./color");
var Grid = require("./grid"); var Grid = require("./grid");
var Player = require("./player"); var Player = require("./player");
var Gate = require("./gate"); //var Gate = require("./gate");
var core = require("./game-core"); var core = require("./game-core");
var consts = require("./game-consts"); var consts = require("./game-consts");
@ -36,7 +36,7 @@ function Game(id)
var nextInd = 0; var nextInd = 0;
var players = []; var players = [];
var newPlayers = []; var newPlayers = [];
var newPlayerFrames = []; var frameLocs = [];
var frame = 0; var frame = 0;
var filled = 0; var filled = 0;
@ -76,19 +76,9 @@ function Game(id)
p.client = client; p.client = client;
players.push(p); players.push(p);
newPlayers.push(p); newPlayers.push(p);
newPlayerFrames.push(0);
nextInd++; nextInd++;
core.initPlayer(grid, p); core.initPlayer(grid, p);
var splayers = players.map(function(val) {return val.serialData();});
client.emit("game", {
"num": p.num,
"gameid": id,
"frame": frame,
"players": splayers,
"grid": gridSerialData(grid, players),
"colors": colors
});
//playerReady(p, frame); //playerReady(p, frame);
console.log(p.name + " joined."); console.log(p.name + " joined.");
@ -109,6 +99,20 @@ function Game(id)
//playerReady(p, frame); //playerReady(p, frame);
}); });
client.on("verify", function(data, resp) {
if (typeof resp !== "function")
return;
if (!data.frame)
resp(false, "No frame supplied");
else if (!checkInt(data.frame, 0, frame + 1))
resp(false, "Must be a valid frame number");
else
{
verifyPlayerLocations(data.frame, data.locs, resp);
}
});
client.on("frame", function(data, errorHan){ client.on("frame", function(data, errorHan){
if (typeof data === "function") if (typeof data === "function")
{ {
@ -123,8 +127,6 @@ function Game(id)
errorHan(false, "No data supplied."); errorHan(false, "No data supplied.");
else if (!checkInt(data.frame, 0, Infinity)) else if (!checkInt(data.frame, 0, Infinity))
errorHan(false, "Requires a valid non-negative frame integer."); errorHan(false, "Requires a valid non-negative frame integer.");
//else if (data.frame < frame)
// errorHan(false, "Late frame received.");
else if (data.frame > frame) else if (data.frame > frame)
errorHan(false, "Invalid frame received."); errorHan(false, "Invalid frame received.");
else else
@ -149,36 +151,74 @@ function Game(id)
return true; return true;
}; };
/* function pushPlayerLocations()
var ready = 0;
var readyTick = false;
function playerReady(player, waitFrame)
{ {
if (player.frame < waitFrame) var locs = [];
{ for (var p of players)
ready++; locs[p.num] = [p.posX, p.posY, p.waitLag];
player.frame = waitFrame; locs.frame = frame;
}
tick(); if (frameLocs.length >= 100)
frameLocs.shift();
frameLocs.push(locs);
} }
*/
function verifyPlayerLocations(fr, verify, resp)
{
var minFrame = frame - frameLocs.length + 1;
if (fr < minFrame || fr > frame)
{
resp(false, "Frames out of reference");
return;
}
function string(loc)
{
return '(' + loc[0] + ', ' + loc[1] + ') [' + loc[2] + ']';
}
var locs = frameLocs[fr - minFrame];
if (locs.frame !== fr)
{
resp(false, locs.frame + " != " + fr);
return;
}
for (var num in verify)
{
if (locs[num][0] !== verify[num][0] || locs[num][1] !== verify[num][1] || locs[num][2] !== verify[num][2])
{
resp(false, 'P' + num + ' ' + string(locs[num]) + ' !== ' + string(verify[num]));
return;
}
}
resp(true);
}
function tick() { function tick() {
//if (readyTick && ready === players.length)
//{
// ready = 0;
// readyTick = false;
//}else
// return;
//TODO: notify those players that this server automatically drops out. //TODO: notify those players that this server automatically drops out.
var snews = newPlayers.map(function(val) {return val.serialData();}); var splayers = players.map(function(val) {return val.serialData();});
var snews = newPlayers.map(function(val) {
//Emit game stats.
val.client.emit("game", {
"num": val.num,
"gameid": id,
"frame": frame,
"players": splayers,
"grid": gridSerialData(grid, players),
"colors": colors
});
return val.serialData();
});
var moves = players.map(function(val) { var moves = players.map(function(val) {
//Account for race condition (when heading is set after emitting frames, and before updating). //Account for race condition (when heading is set after emitting frames, and before updating).
val.heading = val.tmpHeading; val.heading = val.tmpHeading;
return {num: val.num, left: !!val.disconnected, heading: val.heading}; return {num: val.num, left: !!val.disconnected, heading: val.heading};
}); });
update();
var data = {frame: frame + 1, moves: moves}; var data = {frame: frame + 1, moves: moves};
if (snews.length > 0) if (snews.length > 0)
{ {
@ -186,45 +226,25 @@ function Game(id)
newPlayers = []; newPlayers = [];
} }
//TODO: send a "good-bye" frame to the dead players. Just in case. for (var pl of players)
players.forEach(function(val) { pl.client.emit("notifyFrame", data);
if (val.num === 1) //GHOST PLAYER
{
var splayers = players.map(function(val) {return val.serialData();});
val.client.emit("game", {
"num": val.num,
"gameid": id,
"frame": frame,
"players": splayers,
"grid": gridSerialData(grid, players),
"colors": colors
});
}
else
{
val.client.emit("notifyFrame", data, function() {
//playerReady(val, waitFrame);
});
}
});
frame++; frame++;
setTimeout(update, 1); pushPlayerLocations();
} }
this.tickFrame = function() { this.tickFrame = tick;
//readyTick = true;
tick();
};
function update() function update()
{ {
var dead = []; var dead = [];
core.updateFrame(grid, players, newPlayerFrames, dead, undefined, frame); core.updateFrame(grid, players, dead);
dead.forEach(function(val) { for (var pl of dead)
console.log(val.name + " died."); {
//val.client.disconnect(true); //TODO: send a "good-bye" frame to the dead players. Just in case.
}); console.log(pl.name + " died.");
pl.client.disconnect(true);
}
} }
} }

View File

@ -5,6 +5,7 @@ var consts = require("./game-consts.js");
var GRID_SIZE = consts.GRID_SIZE; var GRID_SIZE = consts.GRID_SIZE;
var CELL_WIDTH = consts.CELL_WIDTH; var CELL_WIDTH = consts.CELL_WIDTH;
var NEW_PLAYER_LAG = 60; //wait for a second at least.
function defineGetter(getter) { function defineGetter(getter) {
return { return {
@ -291,13 +292,17 @@ function fillTail(data)
function floodFill(data, grid, row, col, been) function floodFill(data, grid, row, col, been)
{ {
var coords = [];
var filled = new Stack(40000);
var surrounded = true;
function onTail(c) { return data.tailGrid[c[0]] && data.tailGrid[c[0]][c[1]]; } function onTail(c) { return data.tailGrid[c[0]] && data.tailGrid[c[0]][c[1]]; }
coords.push([row, col]); var start = [row, col];
if (grid.isOutOfBounds(r, c) || been.get(row, col) || onTail(start) || grid.get(row, col) === data.player)
return; //Avoid allocating too many resources.
var coords = [];
var filled = new Stack(GRID_SIZE * GRID_SIZE + 1);
var surrounded = true;
coords.push(start);
while (coords.length > 0) while (coords.length > 0)
{ {
var coord = coords.shift(); var coord = coords.shift();
@ -357,6 +362,7 @@ function Player(isClient, grid, sdata) {
data.posX = sdata.posX; data.posX = sdata.posX;
data.posY = sdata.posY; data.posY = sdata.posY;
this.heading = data.currentHeading = sdata.currentHeading; //0 is up, 1 is right, 2 is down, 3 is left. this.heading = data.currentHeading = sdata.currentHeading; //0 is up, 1 is right, 2 is down, 3 is left.
data.waitLag = sdata.waitLag || 0;
data.dead = false; data.dead = false;
//Only need colors for client side. //Only need colors for client side.
@ -386,7 +392,7 @@ function Player(isClient, grid, sdata) {
//Instance methods. //Instance methods.
this.move = move.bind(this, data); this.move = move.bind(this, data);
this.die = function() { if (data.num !== 1) /* GHOST PLAYER */ data.dead = true;}; this.die = function() { data.dead = true;};
this.serialData = function() { this.serialData = function() {
return { return {
num: data.num, num: data.num,
@ -394,12 +400,13 @@ function Player(isClient, grid, sdata) {
posX: data.posX, posX: data.posX,
posY: data.posY, posY: data.posY,
currentHeading: data.currentHeading, currentHeading: data.currentHeading,
tail: data.tail.serialData() tail: data.tail.serialData(),
waitLag: data.waitLag
}; };
}; };
//Read-only Properties. //Read-only Properties.
defineAccessorProperties(this, data, "currentHeading", "dead", "name", "num", "posX", "posY", "grid", "tail"); defineAccessorProperties(this, data, "currentHeading", "dead", "name", "num", "posX", "posY", "grid", "tail", "waitLag");
Object.defineProperties(this, { Object.defineProperties(this, {
row: defineGetter(function() { return calcRow(data); }), row: defineGetter(function() { return calcRow(data); }),
col: defineGetter(function() { return calcCol(data); }) col: defineGetter(function() { return calcCol(data); })
@ -453,12 +460,15 @@ Player.prototype.render = function(ctx, fade)
}; };
function move(data, frame) function move(data)
{ {
//console.log("P" + this.num + ": " + frame + ": " + this.heading); if (data.waitLag < NEW_PLAYER_LAG)
{
data.waitLag++;
return;
}
//Move to new position. //Move to new position.
var prevX = this.posX, prevY = this.posY;
var heading = this.heading; var heading = this.heading;
if (this.posX % CELL_WIDTH !== 0 || this.posY % CELL_WIDTH !== 0) if (this.posX % CELL_WIDTH !== 0 || this.posY % CELL_WIDTH !== 0)
heading = data.currentHeading; heading = data.currentHeading;
@ -476,12 +486,6 @@ function move(data, frame)
var row = this.row, col = this.col; var row = this.row, col = this.col;
if (data.grid.isOutOfBounds(row, col)) if (data.grid.isOutOfBounds(row, col))
{ {
if (data.num === 1) /* GHOST PLAYER */
{
prevX = this.posX;
prevY = this.posY;
return;
}
data.dead = true; data.dead = true;
return; return;
} }

View File

@ -406,6 +406,7 @@ function connectServer() {
renderer.addPlayer(pl); renderer.addPlayer(pl);
}); });
user = renderer.getPlayerFromNum(data.num); user = renderer.getPlayerFromNum(data.num);
if (!user) throw new Error();
renderer.setUser(user); renderer.setUser(user);
//Load grid. //Load grid.
@ -422,7 +423,7 @@ function connectServer() {
}); });
var waiting = false; var waiting = false;
socket.on('notifyFrame', function(data, fn) { socket.on('notifyFrame', function(data) {
if (timeout != undefined) if (timeout != undefined)
clearTimeout(timeout); clearTimeout(timeout);
@ -435,6 +436,7 @@ function connectServer() {
socket.emit('requestFrame'); //Restore data. socket.emit('requestFrame'); //Restore data.
waiting = true; waiting = true;
return; return;
//TODO: cache frames when this happen.
} }
frame++; frame++;
@ -470,6 +472,19 @@ function connectServer() {
renderer.update(frame); renderer.update(frame);
var locs = {};
for (var i = 0; i < renderer.playerSize(); i++)
{
var p = renderer.getPlayer(i);
locs[p.num] = [p.posX, p.posY, p.waitLag];
}
socket.emit("verify", {
frame: frame,
locs: locs
}, function(frame, success, msg) {
if (!success) console.error(frame + ": " + msg);
}.bind(this, frame));
dirty = true; dirty = true;
requestAnimationFrame(function() { requestAnimationFrame(function() {
paintLoop(); paintLoop();
@ -477,8 +492,7 @@ function connectServer() {
timeout = setTimeout(function() { timeout = setTimeout(function() {
console.warn("Server has timed-out. Disconnecting."); console.warn("Server has timed-out. Disconnecting.");
socket.disconnect(); socket.disconnect();
}, 500); }, 3000);
fn();
}); });
socket.on('disconnect', function(){ socket.on('disconnect', function(){
@ -547,7 +561,6 @@ Object.defineProperties(module.exports, consts);
},{}],6:[function(require,module,exports){ },{}],6:[function(require,module,exports){
var ANIMATE_FRAMES = 24; var ANIMATE_FRAMES = 24;
var CELL_WIDTH = 40; var CELL_WIDTH = 40;
var NEW_PLAYER_LAG = 60; //wait for a second at least.
//TODO: remove constants. //TODO: remove constants.
exports.initPlayer = function(grid, player) exports.initPlayer = function(grid, player)
@ -557,7 +570,7 @@ exports.initPlayer = function(grid, player)
if (!grid.isOutOfBounds(dr + player.row, dc + player.col)) if (!grid.isOutOfBounds(dr + player.row, dc + player.col))
grid.set(dr + player.row, dc + player.col, player); grid.set(dr + player.row, dc + player.col, player);
}; };
exports.updateFrame = function(grid, players, newPlayerFrames, dead, notifyKill, frame) exports.updateFrame = function(grid, players, dead, notifyKill)
{ {
var adead = []; var adead = [];
if (dead instanceof Array) if (dead instanceof Array)
@ -571,15 +584,7 @@ exports.updateFrame = function(grid, players, newPlayerFrames, dead, notifyKill,
//Move players. //Move players.
var tmp = players.filter(function(val) { var tmp = players.filter(function(val) {
if (!newPlayerFrames[val.num]) val.move();
newPlayerFrames[val.num] = 0;
if (newPlayerFrames[val.num] < ANIMATE_FRAMES + NEW_PLAYER_LAG)
newPlayerFrames[val.num]++;
else
//TODO: remove frame
val.move(frame);
if (val.dead) if (val.dead)
adead.push(val); adead.push(val);
return !val.dead; return !val.dead;
@ -649,12 +654,12 @@ exports.updateFrame = function(grid, players, newPlayerFrames, dead, notifyKill,
} }
tmp = tmp.filter(function(val, i) { tmp = tmp.filter(function(val, i) {
if (removing[i] && val.num !== 1) /** GHOST PLAYER **/ if (removing[i])
{ {
adead.push(val); adead.push(val);
val.die(); val.die();
} }
return !removing[i] || val.num === 1; return !removing[i];
}); });
players.length = tmp.length; players.length = tmp.length;
for (var i = 0; i < tmp.length; i++) for (var i = 0; i < tmp.length; i++)
@ -729,7 +734,7 @@ $(function () {
var allowAnimation = true; var allowAnimation = true;
var animateGrid, players, allPlayers, newPlayerFrames, playerPortion, grid, var animateGrid, players, allPlayers, playerPortion, grid,
animateTo, offset, user, lagPortion, portionSpeed, zoom, kills, showedDead; animateTo, offset, user, lagPortion, portionSpeed, zoom, kills, showedDead;
grid = new Grid(GRID_SIZE, function(row, col, before, after) { grid = new Grid(GRID_SIZE, function(row, col, before, after) {
@ -755,7 +760,6 @@ function init() {
players = []; players = [];
allPlayers = []; allPlayers = [];
newPlayerFrames = [];
playerPortion = []; playerPortion = [];
animateTo = [0, 0]; animateTo = [0, 0];
@ -783,23 +787,6 @@ function paintGridBorder(ctx)
ctx.fillRect(-BORDER_WIDTH, gridWidth, gridWidth + BORDER_WIDTH * 2, BORDER_WIDTH); ctx.fillRect(-BORDER_WIDTH, gridWidth, gridWidth + BORDER_WIDTH * 2, BORDER_WIDTH);
} }
function paintGridLines(ctx)
{
ctx.fillStyle = 'lightgray';
ctx.beginPath();
for (var x = modRotate(-offset[0], CELL_WIDTH); x < gameWidth; x += CELL_WIDTH)
{
ctx.moveTo(x, 0);
ctx.lineTo(x, gameHeight);
}
for (var y = modRotate(-offset[1], CELL_WIDTH); y < gameHeight; y+= CELL_WIDTH)
{
ctx.moveTo(0, y);
ctx.lineTo(gameWidth, y);
}
ctx.stroke();
}
function paintGrid(ctx) function paintGrid(ctx)
{ {
//Paint background. //Paint background.
@ -900,6 +887,7 @@ function paintGrid(ctx)
} }
} }
function paintUIBar(ctx) function paintUIBar(ctx)
{ {
//UI Bar background //UI Bar background
@ -941,10 +929,37 @@ function paintUIBar(ctx)
if (a.portion === b.portion) return a.player.num - b.player.num; if (a.portion === b.portion) return a.player.num - b.player.num;
else return b.portion - a.portion; else return b.portion - a.portion;
}); });
var rank = sorted.findIndex(function(val) {return val.player === user}); var rank = sorted.findIndex(function(val) {return val.player === user});
ctx.fillText("Rank: " + (rank === -1 ? "--" : rank + 1) + " of " + sorted.length, ctx.fillText("Rank: " + (rank === -1 ? "--" : rank + 1) + " of " + sorted.length,
ctx.measureText(killsText).width + killsOffset + 20, CELL_WIDTH - 5); ctx.measureText(killsText).width + killsOffset + 20, CELL_WIDTH - 5);
//Show leaderboard.
var maxPortion = sorted[0] ? sorted[0].portion : 0;
var leaderboardNum = Math.min(5, sorted.length);
for (var i = 0; i < leaderboardNum; i++)
{
var player = sorted[i].player;
var name = player.name;
var portion = sorted[i].portion / maxPortion;
var nameWidth = ctx.measureText(name).width;
barSize = Math.ceil((BAR_WIDTH - MIN_BAR_WIDTH) * portion + MIN_BAR_WIDTH);
var barX = canvasWidth - barSize;
var barY = BAR_HEIGHT * (i + 1);
ctx.fillStyle = 'rgba(10, 10, 10, .3)';
ctx.fillRect(barX - 10, barY, barSize + 10, CELL_WIDTH + 10);
ctx.fillStyle = player.baseColor.rgbString();
ctx.fillRect(barX, barY, barSize, CELL_WIDTH);
ctx.fillStyle = player.shadowColor.rgbString();
ctx.fillRect(barX, barY + CELL_WIDTH, barSize, SHADOW_OFFSET);
ctx.fillStyle = "black";
ctx.fillText(name, barX - nameWidth - 5, barY + CELL_WIDTH - 10);
}
} }
//TODO: depict leaderboard. //TODO: depict leaderboard.
@ -966,7 +981,7 @@ function paint(ctx)
paintGrid(ctx); paintGrid(ctx);
players.forEach(function (p) { players.forEach(function (p) {
var fr = newPlayerFrames[p.num] || 0; var fr = p.waitLag;
if (fr < ANIMATE_FRAMES) if (fr < ANIMATE_FRAMES)
p.render(ctx, fr / ANIMATE_FRAMES); p.render(ctx, fr / ANIMATE_FRAMES);
else else
@ -1035,11 +1050,11 @@ function update(frame) {
portionSpeed = Math.abs(userPortion - lagPortion) / ANIMATE_FRAMES; portionSpeed = Math.abs(userPortion - lagPortion) / ANIMATE_FRAMES;
var dead = []; var dead = [];
core.updateFrame(grid, players, newPlayerFrames, dead, function addKill(killer, other) core.updateFrame(grid, players, dead, function addKill(killer, other)
{ {
if (players[killer] === user && killer !== other) if (players[killer] === user && killer !== other)
kills++; kills++;
}, frame); });
dead.forEach(function(val) { dead.forEach(function(val) {
console.log(val.name + " is dead"); console.log(val.name + " is dead");
allPlayers[val.num] = undefined; allPlayers[val.num] = undefined;
@ -1051,15 +1066,6 @@ function update(frame) {
} }
//Helper methods. //Helper methods.
function modRotate(val, mod)
{
var res = val % mod;
if (res >= 0)
return res;
else
return mod + res;
}
function centerOnPlayer(player, pos) function centerOnPlayer(player, pos)
{ {
var xOff = Math.floor(player.posX - (gameWidth / zoom - CELL_WIDTH) / 2); var xOff = Math.floor(player.posX - (gameWidth / zoom - CELL_WIDTH) / 2);
@ -1101,7 +1107,6 @@ module.exports = exports = {
if (allPlayers[player.num]) if (allPlayers[player.num])
return; //Already added. return; //Already added.
allPlayers[player.num] = players[players.length] = player; allPlayers[player.num] = players[players.length] = player;
newPlayerFrames[player.num] = 0;
playerPortion[player.num] = 0; playerPortion[player.num] = 0;
return players.length - 1; return players.length - 1;
}, },
@ -8975,6 +8980,7 @@ var consts = require("./game-consts.js");
var GRID_SIZE = consts.GRID_SIZE; var GRID_SIZE = consts.GRID_SIZE;
var CELL_WIDTH = consts.CELL_WIDTH; var CELL_WIDTH = consts.CELL_WIDTH;
var NEW_PLAYER_LAG = 60; //wait for a second at least.
function defineGetter(getter) { function defineGetter(getter) {
return { return {
@ -9261,13 +9267,17 @@ function fillTail(data)
function floodFill(data, grid, row, col, been) function floodFill(data, grid, row, col, been)
{ {
var coords = [];
var filled = new Stack(40000);
var surrounded = true;
function onTail(c) { return data.tailGrid[c[0]] && data.tailGrid[c[0]][c[1]]; } function onTail(c) { return data.tailGrid[c[0]] && data.tailGrid[c[0]][c[1]]; }
coords.push([row, col]); var start = [row, col];
if (grid.isOutOfBounds(r, c) || been.get(row, col) || onTail(start) || grid.get(row, col) === data.player)
return; //Avoid allocating too many resources.
var coords = [];
var filled = new Stack(GRID_SIZE * GRID_SIZE + 1);
var surrounded = true;
coords.push(start);
while (coords.length > 0) while (coords.length > 0)
{ {
var coord = coords.shift(); var coord = coords.shift();
@ -9327,6 +9337,7 @@ function Player(isClient, grid, sdata) {
data.posX = sdata.posX; data.posX = sdata.posX;
data.posY = sdata.posY; data.posY = sdata.posY;
this.heading = data.currentHeading = sdata.currentHeading; //0 is up, 1 is right, 2 is down, 3 is left. this.heading = data.currentHeading = sdata.currentHeading; //0 is up, 1 is right, 2 is down, 3 is left.
data.waitLag = sdata.waitLag || 0;
data.dead = false; data.dead = false;
//Only need colors for client side. //Only need colors for client side.
@ -9356,7 +9367,7 @@ function Player(isClient, grid, sdata) {
//Instance methods. //Instance methods.
this.move = move.bind(this, data); this.move = move.bind(this, data);
this.die = function() { if (data.num !== 1) /* GHOST PLAYER */ data.dead = true;}; this.die = function() { data.dead = true;};
this.serialData = function() { this.serialData = function() {
return { return {
num: data.num, num: data.num,
@ -9364,12 +9375,13 @@ function Player(isClient, grid, sdata) {
posX: data.posX, posX: data.posX,
posY: data.posY, posY: data.posY,
currentHeading: data.currentHeading, currentHeading: data.currentHeading,
tail: data.tail.serialData() tail: data.tail.serialData(),
waitLag: data.waitLag
}; };
}; };
//Read-only Properties. //Read-only Properties.
defineAccessorProperties(this, data, "currentHeading", "dead", "name", "num", "posX", "posY", "grid", "tail"); defineAccessorProperties(this, data, "currentHeading", "dead", "name", "num", "posX", "posY", "grid", "tail", "waitLag");
Object.defineProperties(this, { Object.defineProperties(this, {
row: defineGetter(function() { return calcRow(data); }), row: defineGetter(function() { return calcRow(data); }),
col: defineGetter(function() { return calcCol(data); }) col: defineGetter(function() { return calcCol(data); })
@ -9423,12 +9435,15 @@ Player.prototype.render = function(ctx, fade)
}; };
function move(data, frame) function move(data)
{ {
//console.log("P" + this.num + ": " + frame + ": " + this.heading); if (data.waitLag < NEW_PLAYER_LAG)
{
data.waitLag++;
return;
}
//Move to new position. //Move to new position.
var prevX = this.posX, prevY = this.posY;
var heading = this.heading; var heading = this.heading;
if (this.posX % CELL_WIDTH !== 0 || this.posY % CELL_WIDTH !== 0) if (this.posX % CELL_WIDTH !== 0 || this.posY % CELL_WIDTH !== 0)
heading = data.currentHeading; heading = data.currentHeading;
@ -9446,12 +9461,6 @@ function move(data, frame)
var row = this.row, col = this.col; var row = this.row, col = this.col;
if (data.grid.isOutOfBounds(row, col)) if (data.grid.isOutOfBounds(row, col))
{ {
if (data.num === 1) /* GHOST PLAYER */
{
prevX = this.posX;
prevY = this.posY;
return;
}
data.dead = true; data.dead = true;
return; return;
} }

View File

@ -3,10 +3,11 @@ var http = require('http');
var serveStatic = require('serve-static'); var serveStatic = require('serve-static');
// Serve up public/ftp folder // Serve up public/ftp folder
var serve = serveStatic('public/', {'cacheControl': false, 'setHeaders': setHeaders}); var serve = serveStatic('public/', {'setHeaders': setHeaders});
function setHeaders(res, path) { function setHeaders(res, path) {
res.setHeader("Access-Control-Allow-Origin", "http://paper-io-thekidofarcrania.c9users.io:8081"); res.setHeader("Access-Control-Allow-Origin", "http://paper-io-thekidofarcrania.c9users.io:8081");
res.setHeader('Cache-Control', 'public, max-age=0');
} }
// Create server // Create server