papercats/player.js
theKidOfArcrania 8352433fa5 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.
2017-03-01 00:56:56 +00:00

505 lines
12 KiB
JavaScript

var Stack = require("./stack.js");
var Color = require("./color.js");
var Grid = require("./grid.js");
var consts = require("./game-consts.js");
var GRID_SIZE = consts.GRID_SIZE;
var CELL_WIDTH = consts.CELL_WIDTH;
var NEW_PLAYER_LAG = 60; //wait for a second at least.
function defineGetter(getter) {
return {
get: getter,
enumerable: true
};
}
function defineInstanceMethods(thisobj, data /*, methods...*/)
{
for (var i = 2; i < arguments.length; i++)
thisobj[arguments[i].name] = arguments[i].bind(this, data);
}
function defineAccessorProperties(thisobj, data /*, names...*/)
{
var descript = {};
function getAt(name) { return function() {return data[name] } }
for (var i = 2; i < arguments.length; i++)
descript[arguments[i]] = defineGetter(getAt(arguments[i]));
Object.defineProperties(thisobj, descript);
}
function TailMove(orientation)
{
this.move = 1;
Object.defineProperty(this, "orientation", {
value: orientation,
enumerable: true
});
}
function Tail(player, sdata)
{
var data = {
tail: [],
tailGrid: [],
prev: null,
startRow: 0,
startCol: 0,
prevRow: 0,
prevCol: 0,
player: player
};
if (sdata)
{
data.startRow = data.prevRow = sdata.startRow || 0;
data.startCol = data.prevCol = sdata.startCol || 0;
sdata.tail.forEach(function(val) {
addTail(data, val.orientation, val.move);
});
}
data.grid = player.grid;
defineInstanceMethods(this, data, addTail, hitsTail, fillTail, renderTail, reposition, serialData);
Object.defineProperty(this, "moves", {
get: function() {return data.tail.slice(0);},
enumerable: true
});
}
//Instance methods.
function serialData(data) {
return {
tail: data.tail,
startRow: data.startRow,
startCol: data.startCol
};
}
function setTailGrid(data, tailGrid, r, c)
{
if (!tailGrid[r])
tailGrid[r] = [];
tailGrid[r][c] = true;
}
function addTail(data, orientation, count)
{
if (count === undefined)
count = 1;
if (!count || count < 0)
return;
var prev = data.prev;
var r = data.prevRow, c = data.prevCol;
if (data.tail.length === 0)
setTailGrid(data, data.tailGrid, r, c);
if (!prev || prev.orientation !== orientation)
{
prev = data.prev = new TailMove(orientation);
data.tail.push(prev);
prev.move += count - 1;
}
else
prev.move += count;
for (var i = 0; i < count; i++)
{
var pos = walk([data.prevRow, data.prevCol], null, orientation, 1);
data.prevRow = pos[0];
data.prevCol = pos[1];
setTailGrid(data, data.tailGrid, pos[0], pos[1]);
}
}
function reposition(data, row, col)
{
data.prevRow = data.startRow = row;
data.prevCol = data.startCol = col;
data.prev = null;
if (data.tail.length === 0)
return;
else
{
var ret = data.tail;
data.tail = [];
data.tailGrid = [];
return ret;
}
}
/*
function render2(data, ctx)
{
ctx.fillStyle = data.player.tailColor.rgbString();
for (var r = 0; r < data.tailGrid.length; r++)
{
if (!data.tailGrid[r])
continue;
for (var c = 0; c < data.tailGrid[r].length; c++)
if (data.tailGrid[r][c])
ctx.fillRect(c * CELL_WIDTH, r * CELL_WIDTH, CELL_WIDTH, CELL_WIDTH);
}
}
*/
//Helper methods.
function renderTail(data, ctx)
{
if (data.tail.length === 0)
return;
ctx.fillStyle = data.player.tailColor.rgbString();
var prevOrient = -1;
var start = [data.startRow, data.startCol];
//fillTailRect(ctx, start, start);
data.tail.forEach(function(tail) {
var negDir = tail.orientation === 0 || tail.orientation === 3;
var back = start;
if (!negDir)
start = walk(start, null, tail.orientation, 1);
var finish = walk(start, null, tail.orientation, tail.move - 1);
if (tail.move > 1)
fillTailRect(ctx, start, finish);
if (prevOrient !== -1)
//Draw folding triangle.
renderCorner(ctx, back, prevOrient, tail.orientation);
start = finish;
if (negDir)
walk(start, start, tail.orientation, 1);
prevOrient = tail.orientation;
});
var curOrient = data.player.currentHeading;
if (prevOrient === curOrient)
{
fillTailRect(ctx, start, start);
}
else
renderCorner(ctx, start, prevOrient, curOrient);
}
function renderCorner(ctx, cornerStart, dir1, dir2)
{
if (dir1 === 0 || dir2 === 0)
walk(cornerStart, cornerStart, 2, 1);
if (dir1 === 3 || dir2 === 3)
walk(cornerStart, cornerStart, 1, 1);
var a = walk(cornerStart, null, dir2, 1);
var b = walk(a, null, dir1, 1);
var triangle = new Path2D();
triangle.moveTo(cornerStart[1] * CELL_WIDTH, cornerStart[0] * CELL_WIDTH);
triangle.lineTo(a[1] * CELL_WIDTH, a[0] * CELL_WIDTH);
triangle.lineTo(b[1] * CELL_WIDTH, b[0] * CELL_WIDTH);
triangle.closePath();
for (var i = 0; i < 2; i++)
ctx.fill(triangle);
}
function walk(from, ret, orient, dist)
{
ret = ret || [];
ret[0] = from[0];
ret[1] = from[1];
switch (orient)
{
case 0: ret[0] -= dist; break; //UP
case 1: ret[1] += dist; break; //RIGHT
case 2: ret[0] += dist; break; //DOWN
case 3: ret[1] -= dist; break; //LEFT
}
return ret;
}
function fillTailRect(ctx, start, end)
{
var x = start[1] * CELL_WIDTH;
var y = start[0] * CELL_WIDTH;
var width = (end[1] - start[1]) * CELL_WIDTH;
var height = (end[0] - start[0]) * CELL_WIDTH;
if (width === 0)
width += CELL_WIDTH;
if (height === 0)
height += CELL_WIDTH;
if (width < 0)
{
x += width;
width = -width;
}
if (height < 0)
{
y += height;
height = -height;
}
ctx.fillRect(x, y, width, height);
}
function fillTail(data)
{
if (data.tail.length === 0)
return;
function onTail(c) { return data.tailGrid[c[0]] && data.tailGrid[c[0]][c[1]]; }
var grid = data.grid;
var start = [data.startRow, data.startCol];
var been = new Grid(grid.size);
var coords = [];
coords.push(start);
while (coords.length > 0) //BFS for all tail spaces.
{
var coord = coords.shift();
var r = coord[0];
var c = coord[1];
if (grid.isOutOfBounds(r, c))
continue;
if (been.get(r, c))
continue;
if (onTail(coord)) //on the tail.
{
been.set(r, c, true);
grid.set(r, c, data.player);
//Find all spots that this tail encloses.
floodFill(data, grid, r + 1, c, been);
floodFill(data, grid, r - 1, c, been);
floodFill(data, grid, r, c + 1, been);
floodFill(data, grid, r, c - 1, been);
coords.push([r + 1, c]);
coords.push([r - 1, c]);
coords.push([r, c + 1]);
coords.push([r, c - 1]);
}
}
}
function floodFill(data, grid, row, col, been)
{
function onTail(c) { return data.tailGrid[c[0]] && data.tailGrid[c[0]][c[1]]; }
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)
{
var coord = coords.shift();
var r = coord[0];
var c = coord[1];
if (grid.isOutOfBounds(r, c))
{
surrounded = false;
continue;
}
//End this traverse on boundaries (where we been, on the tail, and when we enter our territory)
if (been.get(r, c) || onTail(coord) || grid.get(r, c) === data.player)
continue;
been.set(r, c, true);
if (surrounded)
filled.push(coord);
coords.push([r + 1, c]);
coords.push([r - 1, c]);
coords.push([r, c + 1]);
coords.push([r, c - 1]);
}
if (surrounded)
{
while (!filled.isEmpty())
{
coord = filled.pop();
grid.set(coord[0], coord[1], data.player);
}
}
return surrounded;
}
function hitsTail(data, other)
{
return (data.prevRow !== other.row || data.prevCol !== other.col) &&
(data.startRow !== other.row || data.startCol !== other.col) &&
!!(data.tailGrid[other.row] && data.tailGrid[other.row][other.col]);
}
var SPEED = 5;
var SHADOW_OFFSET = 10;
function Player(isClient, grid, sdata) {
var data = {};
//Parameters
data.num = sdata.num;
data.name = sdata.name || "Player " + (data.num + 1);
data.isCient = isClient;
data.grid = grid;
data.posX = sdata.posX;
data.posY = sdata.posY;
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;
//Only need colors for client side.
if (isClient)
{
var base;
if (sdata.base)
base = this.baseColor = Color.fromData(sdata.base);
else
{
var hue = Math.random();
this.baseColor = base = new Color(hue, .8, .5);
}
this.shadowColor = base.deriveLumination(-.3);
this.tailColor = base.deriveLumination(.2).deriveAlpha(.5);
}
//Tail requires special handling.
this.grid = grid; //Temporary
if (sdata.tail)
data.tail = new Tail(this, sdata.tail);
else
{
data.tail = new Tail(this);
data.tail.reposition(calcRow(data), calcCol(data));
}
//Instance methods.
this.move = move.bind(this, data);
this.die = function() { data.dead = true;};
this.serialData = function() {
return {
num: data.num,
name: data.name,
posX: data.posX,
posY: data.posY,
currentHeading: data.currentHeading,
tail: data.tail.serialData(),
waitLag: data.waitLag
};
};
//Read-only Properties.
defineAccessorProperties(this, data, "currentHeading", "dead", "name", "num", "posX", "posY", "grid", "tail", "waitLag");
Object.defineProperties(this, {
row: defineGetter(function() { return calcRow(data); }),
col: defineGetter(function() { return calcCol(data); })
});
}
//Gets the next integer in positive or negative direction.
function nearestInteger(positive, val)
{
return positive ? Math.ceil(val) : Math.floor(val);
}
function calcRow(data)
{
return nearestInteger(data.currentHeading === 2 /*DOWN*/, data.posY / CELL_WIDTH);
}
function calcCol(data)
{
return nearestInteger(data.currentHeading === 1 /*RIGHT*/, data.posX / CELL_WIDTH);
}
//Instance methods
Player.prototype.render = function(ctx, fade)
{
//Render tail.
this.tail.renderTail(ctx);
//Render player.
fade = fade || 1;
ctx.fillStyle = this.shadowColor.deriveAlpha(fade).rgbString();
ctx.fillRect(this.posX, this.posY, CELL_WIDTH, CELL_WIDTH);
var mid = CELL_WIDTH / 2;
var grd = ctx.createRadialGradient(this.posX + mid, this.posY + mid - SHADOW_OFFSET, 1,
this.posX + mid, this.posY + mid - SHADOW_OFFSET, CELL_WIDTH);
grd.addColorStop(0, this.baseColor.deriveAlpha(fade).rgbString());
grd.addColorStop(1, new Color(0, 0, 1, fade).rgbString());
ctx.fillStyle = grd;
ctx.fillRect(this.posX - 1, this.posY - SHADOW_OFFSET, CELL_WIDTH + 2, CELL_WIDTH);
//Render name
ctx.fillStyle = this.shadowColor.deriveAlpha(fade).rgbString();
ctx.textAlign = "center";
var yoff = -SHADOW_OFFSET * 2;
if (this.row === 0)
yoff = SHADOW_OFFSET * 2 + CELL_WIDTH;
ctx.font = "18px Changa";
ctx.fillText(this.name, this.posX + CELL_WIDTH / 2, this.posY + yoff);
};
function move(data)
{
if (data.waitLag < NEW_PLAYER_LAG)
{
data.waitLag++;
return;
}
//Move to new position.
var heading = this.heading;
if (this.posX % CELL_WIDTH !== 0 || this.posY % CELL_WIDTH !== 0)
heading = data.currentHeading;
else
data.currentHeading = heading;
switch (heading)
{
case 0: data.posY -= SPEED; break; //UP
case 1: data.posX += SPEED; break; //RIGHT
case 2: data.posY += SPEED; break; //DOWN
case 3: data.posX -= SPEED; break; //LEFT
}
//Check for out of bounds.
var row = this.row, col = this.col;
if (data.grid.isOutOfBounds(row, col))
{
data.dead = true;
return;
}
//Update tail position.
if (data.grid.get(row, col) === this)
{
//Safe zone!
this.tail.fillTail();
this.tail.reposition(row, col);
}
//If we are completely in a new cell (not in our safe zone), we add to the tail.
else if (this.posX % CELL_WIDTH === 0 && this.posY % CELL_WIDTH === 0)
this.tail.addTail(heading);
}
module.exports = Player;