diff --git a/client-modes/bot-mode.js b/client-modes/bot-mode.js index feba1f1..04da316 100644 --- a/client-modes/bot-mode.js +++ b/client-modes/bot-mode.js @@ -5,20 +5,158 @@ if (process.argv.length < 3) { var core = require("../game-core"); var client = require("../client"); -var user; var GRID_SIZE = core.GRID_SIZE; var CELL_WIDTH = core.CELL_WIDTH; +var MOVES = [[-1, 0], [0, 1], [1, 0], [0, -1]] + +var AGGRESSIVE = Math.random(); +var THRESHOLD = 10; + +var startFrame = -1; +var endFrame = -1; +var coeffs = [1,3,1,1,3]; +var grid, others, user; +var DIST_TYPES = { + land: { + check: function(loc) { return grid.get(loc.row, loc.col) === user; }, + coeff: function() {return coeffs[0];} + }, tail: { + check: function(loc) {return tail(user, loc)}, + coeff: function() {return -coeffs[1];} + }, oTail: { + check: foundProto(tail), + coeff: function() {return AGGRESSIVE * coeffs[2];} + }, other: { + check: foundProto(function(other, loc) { return other.row === this.row && other.col === this.col; }), + coeff: function() {return -(1 - AGGRESSIVE) * coeffs[3];} + }, edge: { + check: function(loc) {return loc.row <= 1 || loc.col <= 1 || loc.row >= GRID_SIZE - 1 || loc.col >= GRID_SIZE - 1}, + coeff: function() {return -coeffs[4];} + } +}; + +function foundProto(func) { + return function(loc) { + return others.some(function(other) { + return func(other, loc); + }); + } +} function connect() { client.connectGame(process.argv[2], process.argv[3] || '[BOT]', function(success, msg) { }); } +function Loc(row, col, step) { + if (this.constructor != Loc) { + return new Loc(row, col, step); + } + + this.row = row; + this.col = col; + this.step = step; +} + +//Projects vector b onto vector a +function project(a, b) { + var factor = (b[0] * a[0] + b[1] * a[1]) / (a[0] * a[0] + a[1] * a[1]); + return [factor * a[0], factor * a[1]]; +} + +function tail(player, loc) { + return player.tail.hitsTail(loc); +} + +function traverseGrid(dir) { + steps = new Array(GRID_SIZE * GRID_SIZE); + for (var i in steps) { + steps[i] = -1; + } + + distWeights = {}; + for (var type in DIST_TYPES) { + distWeights[type] = 0; + } + + var row = user.row, col = user.col; + var minRow = Math.max(0, row - 10), maxRow = Math.min(GRID_SIZE, row + 10); + var minCol = Math.max(0, col - 10), maxCol = Math.min(GRID_SIZE, col + 10); + + var proj = 0; + for (var i = 1; i >= -1; i-=2) { + proj = (1 + THRESHOLD) * i; + while (proj != 0) { + proj -= i; + var normRange = Math.abs(proj); + for (var norm = -normRange; norm <= normRange; norm++) { + for (var distType in distWeights) { + var move = MOVES[dir]; + var delta = THRESHOLD - Math.abs(proj); + var dist = Math.sign(proj) * delta * delta / (Math.abs(norm) + 1) + var loc = {row: proj * move[0] + norm * move[1], col: proj * move[1] + norm * move[0]}; + + loc.row += user.row; + loc.col += user.col; + + if (loc.row < 0 || loc.row >= GRID_SIZE || loc.col < 0 || loc.col >= GRID_SIZE) { + continue; + } + if (DIST_TYPES[distType].check(loc)) { + distWeights[distType] += dist; + } + } + } + } + } + + return distWeights; +} + +function printGrid() { + var chars = new core.Grid(GRID_SIZE); + for (var r = 0; r < GRID_SIZE; r++) { + for (var c = 0; c < GRID_SIZE; c++) { + if (tail(user, {row: r, col: c})) { + chars.set(r, c, 't'); + } else { + var owner = grid.get(r, c); + chars.set(r, c, owner ? '' + owner.num % 10 : '.'); + } + } + } + + for (var p of others) { + chars.set(p.row, p.col, 'x'); + } + chars.set(user.row, user.col, '^>V<'[user.currentHeading]); + + var str = ''; + for (var r = 0; r < GRID_SIZE; r++) { + str += '\n'; + for (var c = 0; c < GRID_SIZE; c++) { + str += chars.get(r, c); + } + } + console.log(str); +} + client.allowAnimation = false; client.renderer = { disconnect: function() { - console.log("I died..."); + var dt = (endFrame - startFrame); + startFrame = -1; + + console.log("I died... (survived for " + dt + " frames.)"); + console.log("I killed " + client.kills + " player(s)."); + console.log("Coefficients: " + coeffs) + + var mutation = Math.min(10, Math.pow(2, -dt / 150)); + for (var i = 0; i < coeffs.length; i++) { + coeffs[i] += Math.random() * mutation * 2 - mutation; + } + connect(); }, @@ -27,10 +165,61 @@ client.renderer = { }, update: function(frame) { - if (frame % 6 == 1) - { - //TODO: decide move. - client.changeHeading(Math.floor(Math.random() * 4)); + if (startFrame == -1) { + startFrame = frame; + } + endFrame = frame; + + if (frame % 6 == 1) { + grid = client.grid; + others = client.getOthers(); + + //printGrid(); + + var weights = [0, 0, 0, 0]; + for (var d of [3, 0, 1]) { + var weight = 0; + + d = (d + user.currentHeading) % 4; + distWeights = traverseGrid(d); + + var str = d + ": " + for (var distType in DIST_TYPES) { + var point = distWeights[distType] * DIST_TYPES[distType].coeff(); + weight += point; + str += distType + ": " + point + ", "; + } + //console.log(str); + weights[d] = weight; + } + + var low = Math.min(0, Math.min.apply(this, weights)); + var total = 0; + + weights[(user.currentHeading + 2) % 4] = low; + for (var i = 0; i < weights.length; i++) { + weights[i] -= low * (1 + Math.random()); + total += weights[i]; + } + + if (total == 0) { + for (var d of [-1, 0, 1]) { + d = (d + user.currentHeading) % 4; + while (d < 0) d += 4; + weights[d] = 1; + total++; + } + } + + //console.log(weights) + + //Choose a random direction from the weighted list + var choice = Math.random() * total; + var d = 0; + while (choice > weights[d]) { + choice -= weights[d++]; + } + client.changeHeading(d); } } }; diff --git a/client.js b/client.js index 6d45072..bbb0df1 100644 --- a/client.js +++ b/client.js @@ -178,6 +178,20 @@ function changeHeading(newHeading) { } } +function getUser() { + return user; +} + +function getOthers() { + var ret = []; + for (var p of players) { + if (p !== user) { + ret.push(p); + } + } + return ret; +} + function getPlayers() { return players.slice(); } @@ -345,7 +359,7 @@ function update() { } //Export stuff -var funcs = [connectGame, changeHeading, getPlayers]; +var funcs = [connectGame, changeHeading, getOthers, getPlayers, getUser]; funcs.forEach(function (f) { exports[f.name] = f; }); diff --git a/public/bundle.js b/public/bundle.js index 173ec5e..845c7b6 100644 --- a/public/bundle.js +++ b/public/bundle.js @@ -1,854 +1,868 @@ (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= minRow && r < maxRow && c >= minCol && c < maxCol; - if (animateSpec.after && viewable) - { - //Bouncing the squares. - var offsetBounce = getBounceOffset(animateSpec.frame); - y -= offsetBounce; - - shadowColor = animateSpec.after.shadowColor; - baseColor = animateSpec.after.lightBaseColor.deriveLumination(-(offsetBounce / DROP_HEIGHT) * .1); - - ctx.fillStyle = shadowColor.rgbString(); - ctx.fillRect(x, y + CELL_WIDTH, CELL_WIDTH, SHADOW_OFFSET); - ctx.fillStyle = baseColor.rgbString(); - ctx.fillRect(x, y, CELL_WIDTH + 1, CELL_WIDTH + 1); - } - - animateSpec.frame++; - if (animateSpec.frame >= ANIMATE_FRAMES) - animateGrid.set(r, c, null); - } - } - } -} - - -function paintUIBar(ctx) -{ - - //UI Bar background - ctx.fillStyle = "#24422c"; - ctx.fillRect(0, 0, canvasWidth, BAR_HEIGHT); - - var barOffset; - ctx.fillStyle = "white"; - ctx.font = "24px Changa"; - barOffset = (user && user.name) ? (ctx.measureText(user.name).width + 20) : 0; - ctx.fillText(user ? user.name : "", 5, CELL_WIDTH - 5); - - //Draw filled bar. - ctx.fillStyle = "rgba(180, 180, 180, .3)"; - ctx.fillRect(barOffset, 0, BAR_WIDTH, BAR_HEIGHT); - - var userPortions = portionsRolling[user.num] ? portionsRolling[user.num].lag : 0; - var barSize = Math.ceil((BAR_WIDTH - MIN_BAR_WIDTH) * userPortions + MIN_BAR_WIDTH); - ctx.fillStyle = user ? user.baseColor.rgbString() : ""; - ctx.fillRect(barOffset, 0, barSize, CELL_WIDTH); - ctx.fillStyle = user ? user.shadowColor.rgbString() : ""; - ctx.fillRect(barOffset, CELL_WIDTH, barSize, SHADOW_OFFSET); - - //TODO: dont reset kill count and zoom when we request frames. - //Percentage - ctx.fillStyle = "white"; - ctx.font = "18px Changa"; - ctx.fillText((userPortions * 100).toFixed(3) + "%", 5 + barOffset, CELL_WIDTH - 5); - - //Number of kills - var killsText = "Kills: " + client.kills; - var killsOffset = 20 + BAR_WIDTH + barOffset; - ctx.fillText(killsText, killsOffset, CELL_WIDTH - 5); - - //Calcuate rank - var sorted = []; - client.getPlayers().forEach(function(val) { - sorted.push({player: val, portion: playerPortion[val.num]}); - }); - sorted.sort(function(a, b) { - if (a.portion === b.portion) return a.player.num - b.player.num; - else return b.portion - a.portion; - }); - - var rank = sorted.findIndex(function(val) {return val.player === user}); - ctx.fillText("Rank: " + (rank === -1 ? "--" : rank + 1) + " of " + sorted.length, - ctx.measureText(killsText).width + killsOffset + 20, CELL_WIDTH - 5); - - //Rolling the leaderboard bars. - if (sorted.length > 0) - { - var maxPortion = sorted[0].portion; - client.getPlayers().forEach(function(player) { - var rolling = barProportionRolling[player.num]; - rolling.value = playerPortion[player.num] / maxPortion; - rolling.update(); - }); - } - - //Show leaderboard. - var leaderboardNum = Math.min(5, sorted.length); - for (var i = 0; i < leaderboardNum; i++) - { - var player = sorted[i].player; - var name = player.name || "Unnamed"; - var portion = barProportionRolling[player.num].lag; - - 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); - var offset = i == 0 ? 10 : 0; - - ctx.fillStyle = 'rgba(10, 10, 10, .3)'; - ctx.fillRect(barX - 10, barY + 10 - offset, barSize + 10, BAR_HEIGHT + offset); - 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 - 15, barY + 27); - - var percentage = (portionsRolling[player.num].lag * 100).toFixed(3) + "%"; - ctx.fillStyle = "white"; - ctx.fillText(percentage, barX + 5, barY + CELL_WIDTH - 5); - } - -} - -function paint(ctx) -{ - - ctx.fillStyle = '#e2ebf3'; //'whitesmoke'; - ctx.fillRect(0, 0, canvasWidth, canvasHeight); - - //Move grid to viewport as said with the offsets, below the stats - ctx.save(); - ctx.translate(0, BAR_HEIGHT); - ctx.beginPath(); - ctx.rect(0, 0, gameWidth, gameHeight); - ctx.clip(); - - //Zoom in/out based on player stats. - ctx.scale(zoom, zoom); - ctx.translate(-offset[0] + BORDER_WIDTH, -offset[1] + BORDER_WIDTH); - - paintGrid(ctx); - client.getPlayers().forEach(function (p) { - var fr = p.waitLag; - if (fr < ANIMATE_FRAMES) - p.render(ctx, fr / ANIMATE_FRAMES); - else - p.render(ctx); - }); - - //Reset transform to paint fixed UI elements - ctx.restore(); - paintUIBar(ctx); - - if ((!user || user.dead) && !showedDead) - { - showedDead = true; - console.log("You died!"); - //return; - } -} - -function paintDoubleBuff() -{ - paint(offctx); - ctx.drawImage(offscreenCanvas, 0, 0); -} - -function update() { - updateSize(); - - //Change grid offsets. - for (var i = 0; i <= 1; i++) - { - if (animateTo[i] !== offset[i]) - { - if (client.allowAnimation) - { - var delta = animateTo[i] - offset[i]; - var dir = Math.sign(delta); - var mag = Math.min(SPEED, Math.abs(delta)); - offset[i] += dir * mag; - } - else - offset[i] = animateTo[i]; - } - } - - //Calculate player portions. - client.getPlayers().forEach(function(player) { - var roll = portionsRolling[player.num]; - roll.value = playerPortion[player.num] / GRID_SIZE / GRID_SIZE; - roll.update(); - }); - - //Zoom goes from 1 to .5, decreasing as portion goes up. TODO: maybe can modify this? - if (portionsRolling[user.num]) - zoom = 1 / (portionsRolling[user.num].lag + 1); - - //TODO: animate player is dead. (maybe explosion?), and tail rewinds itself. - if (user) centerOnPlayer(user, animateTo); -} - -//Helper methods. -function centerOnPlayer(player, pos) -{ - var xOff = Math.floor(player.posX - (gameWidth / zoom - CELL_WIDTH) / 2); - var yOff = Math.floor(player.posY - (gameHeight / zoom - CELL_WIDTH) / 2); - var gridWidth = grid.size * CELL_WIDTH + BORDER_WIDTH * 2; - pos[0] = xOff; //Math.max(Math.min(xOff, gridWidth + (BAR_WIDTH + 100) / zoom - gameWidth / zoom), 0); - pos[1] = yOff; //Math.max(Math.min(yOff, gridWidth - gameHeight / zoom), 0); -} - -function getBounceOffset(frame) -{ - var offsetBounce = ANIMATE_FRAMES; - var bounceNum = BOUNCE_FRAMES.length - 1; - while (bounceNum >= 0 && frame < offsetBounce - BOUNCE_FRAMES[bounceNum]) - { - offsetBounce -= BOUNCE_FRAMES[bounceNum]; - bounceNum--; - } - - if (bounceNum === -1) - { - return (offsetBounce - frame) * DROP_SPEED; - } - else - { - offsetBounce -= BOUNCE_FRAMES[bounceNum]; - frame = frame - offsetBounce; - var midFrame = BOUNCE_FRAMES[bounceNum] / 2; - if (frame >= midFrame) - return (BOUNCE_FRAMES[bounceNum] - frame) * DROP_SPEED; - else - return frame * DROP_SPEED; - } -} - -function showStats() { - //TODO: Show score stats. - $("#begin").removeClass("hidden"); - $("#begin").animate({ - opacity: .9999 - }, 1000, function() { - $("#stats").addClass("hidden").css("opacity", 0); - }); -} - -module.exports = exports = { - addPlayer: function(player) { - playerPortion[player.num] = 0; - portionsRolling[player.num] = new Rolling(9 / GRID_SIZE / GRID_SIZE, ANIMATE_FRAMES); - barProportionRolling[player.num] = new Rolling(0, ANIMATE_FRAMES); - }, - disconnect: function() { - //Show score stats. - $("#stats").removeClass("hidden"); - $("#stats").animate({ - opacity: .9999 - }, 3000, function() { - showStats(); - //Then fade back into the login screen. - }); - }, - removePlayer: function(player) { - delete playerPortion[player.num]; - delete portionsRolling[player.num]; - delete barProportionRolling[player.num]; - }, - - setUser: function(player) { - user = player; - centerOnPlayer(user, offset); - }, - reset: reset, - updateGrid: function(row, col, before, after) { - //Keep track of areas. - if (before) - playerPortion[before.num]--; - if (after) - playerPortion[after.num]++; - - //Queue animation - if (before === after || !client.allowAnimation) - return; - animateGrid.set(row, col, { - before: before, - after: after, - frame: 0 - }); - }, - paint: paintDoubleBuff, - update: update -}; +/* global $ */ + +var core = require("../game-core"); +var client = require("../client"); +var Rolling = require("./rolling"); + +var GRID_SIZE = core.GRID_SIZE; +var CELL_WIDTH = core.CELL_WIDTH; +var SPEED = core.SPEED; +var BORDER_WIDTH = core.BORDER_WIDTH; +var SHADOW_OFFSET = 5; +var ANIMATE_FRAMES = 24; +var BOUNCE_FRAMES = [8, 4]; +var DROP_HEIGHT = 24; +var DROP_SPEED = 2; +var MIN_BAR_WIDTH = 65; +var BAR_HEIGHT = SHADOW_OFFSET + CELL_WIDTH; +var BAR_WIDTH = 400; + + +var canvas, canvasWidth, canvasHeight, gameWidth, gameHeight, ctx, offctx, offscreenCanvas; + +$(function () { + canvas = $("#main-ui")[0]; + ctx = canvas.getContext('2d'); + + offscreenCanvas = document.createElement("canvas"); + offctx = offscreenCanvas.getContext('2d'); + + canvas.style.marginTop = 10; + updateSize(); +}); + + +var animateGrid, playerPortion, portionsRolling, + barProportionRolling, animateTo, offset, user, zoom, showedDead; +var grid = client.grid; + +function updateSize() +{ + var changed = false; + if (canvasWidth != window.innerWidth) + { + gameWidth = canvasWidth = offscreenCanvas.width = canvas.width = window.innerWidth; + changed = true; + } + + if (canvasHeight != window.innerHeight - 20) + { + canvasHeight = offscreenCanvas.height = canvas.height = window.innerHeight - 20; + gameHeight = canvasHeight - BAR_HEIGHT; + changed = true; + } + + if (changed && user) + centerOnPlayer(user, offset); +} + +function reset() { + animateGrid = new core.Grid(GRID_SIZE); + + playerPortion = []; + portionsRolling = []; + barProportionRolling = []; + + animateTo = [0, 0]; + offset = [0, 0]; + + user = null; + zoom = 1; + + showedDead = false; +} + +reset(); + +//Paint methods. +function paintGridBorder(ctx) +{ + ctx.fillStyle = 'lightgray'; + var gridWidth = CELL_WIDTH * GRID_SIZE; + + ctx.fillRect(-BORDER_WIDTH, 0, BORDER_WIDTH, gridWidth); + ctx.fillRect(-BORDER_WIDTH, -BORDER_WIDTH, gridWidth + BORDER_WIDTH * 2, BORDER_WIDTH); + ctx.fillRect(gridWidth, 0, BORDER_WIDTH, gridWidth); + ctx.fillRect(-BORDER_WIDTH, gridWidth, gridWidth + BORDER_WIDTH * 2, BORDER_WIDTH); +} + +function paintGrid(ctx) +{ + //Paint background. + ctx.fillStyle = "rgb(211, 225, 237)"; + ctx.fillRect(0, 0, CELL_WIDTH * GRID_SIZE, CELL_WIDTH * GRID_SIZE); + + paintGridBorder(ctx); + //paintGridLines(ctx); + + //Get viewing limits + var offsetX = (offset[0] - BORDER_WIDTH); + var offsetY = (offset[1] - BORDER_WIDTH); + + var minRow = Math.max(Math.floor(offsetY / CELL_WIDTH), 0); + var minCol = Math.max(Math.floor(offsetX / CELL_WIDTH), 0); + var maxRow = Math.min(Math.ceil((offsetY + gameHeight / zoom) / CELL_WIDTH), grid.size); + var maxCol = Math.min(Math.ceil((offsetX + gameWidth / zoom) / CELL_WIDTH), grid.size); + + //Paint occupied areas. (and fading ones). + for (var r = minRow; r < maxRow; r++) + { + for (var c = minCol; c < maxCol; c++) + { + var p = grid.get(r, c); + var x = c * CELL_WIDTH, y = r * CELL_WIDTH, baseColor, shadowColor; + + var animateSpec = animateGrid.get(r, c); + if (client.allowAnimation && animateSpec) + { + if (animateSpec.before) //fading animation + { + var frac = (animateSpec.frame / ANIMATE_FRAMES); + var back = new core.Color(.58, .41, .92, 1); + baseColor = animateSpec.before.lightBaseColor.interpolateToString(back, frac); + shadowColor = animateSpec.before.shadowColor.interpolateToString(back, frac); + } + else + continue; + } + else if (p) + { + baseColor = p.lightBaseColor; + shadowColor = p.shadowColor; + } + else //No animation nor is this player owned. + continue; + + var hasBottom = !grid.isOutOfBounds(r + 1, c); + var bottomAnimate = hasBottom && animateGrid.get(r + 1, c); + var totalStatic = !bottomAnimate && !animateSpec; + var bottomEmpty = totalStatic ? (hasBottom && !grid.get(r + 1, c)) : + (!bottomAnimate || (bottomAnimate.after && bottomAnimate.before)); + if (hasBottom && ((!!bottomAnimate ^ !!animateSpec) || bottomEmpty)) + { + + ctx.fillStyle = shadowColor.rgbString(); + ctx.fillRect(x, y + CELL_WIDTH, CELL_WIDTH + 1, SHADOW_OFFSET); + } + ctx.fillStyle = baseColor.rgbString(); + ctx.fillRect(x, y, CELL_WIDTH + 1, CELL_WIDTH + 1); + } + } + + if (!client.allowAnimation) + return; + + //Paint squares with drop in animation. + for (var r = 0; r < grid.size; r++) + { + for (var c = 0; c < grid.size; c++) + { + animateSpec = animateGrid.get(r, c); + x = c * CELL_WIDTH, y = r * CELL_WIDTH; + + if (animateSpec && client.allowAnimation) + { + var viewable = r >= minRow && r < maxRow && c >= minCol && c < maxCol; + if (animateSpec.after && viewable) + { + //Bouncing the squares. + var offsetBounce = getBounceOffset(animateSpec.frame); + y -= offsetBounce; + + shadowColor = animateSpec.after.shadowColor; + baseColor = animateSpec.after.lightBaseColor.deriveLumination(-(offsetBounce / DROP_HEIGHT) * .1); + + ctx.fillStyle = shadowColor.rgbString(); + ctx.fillRect(x, y + CELL_WIDTH, CELL_WIDTH, SHADOW_OFFSET); + ctx.fillStyle = baseColor.rgbString(); + ctx.fillRect(x, y, CELL_WIDTH + 1, CELL_WIDTH + 1); + } + + animateSpec.frame++; + if (animateSpec.frame >= ANIMATE_FRAMES) + animateGrid.set(r, c, null); + } + } + } +} + + +function paintUIBar(ctx) +{ + + //UI Bar background + ctx.fillStyle = "#24422c"; + ctx.fillRect(0, 0, canvasWidth, BAR_HEIGHT); + + var barOffset; + ctx.fillStyle = "white"; + ctx.font = "24px Changa"; + barOffset = (user && user.name) ? (ctx.measureText(user.name).width + 20) : 0; + ctx.fillText(user ? user.name : "", 5, CELL_WIDTH - 5); + + //Draw filled bar. + ctx.fillStyle = "rgba(180, 180, 180, .3)"; + ctx.fillRect(barOffset, 0, BAR_WIDTH, BAR_HEIGHT); + + var userPortions = portionsRolling[user.num] ? portionsRolling[user.num].lag : 0; + var barSize = Math.ceil((BAR_WIDTH - MIN_BAR_WIDTH) * userPortions + MIN_BAR_WIDTH); + ctx.fillStyle = user ? user.baseColor.rgbString() : ""; + ctx.fillRect(barOffset, 0, barSize, CELL_WIDTH); + ctx.fillStyle = user ? user.shadowColor.rgbString() : ""; + ctx.fillRect(barOffset, CELL_WIDTH, barSize, SHADOW_OFFSET); + + //TODO: dont reset kill count and zoom when we request frames. + //Percentage + ctx.fillStyle = "white"; + ctx.font = "18px Changa"; + ctx.fillText((userPortions * 100).toFixed(3) + "%", 5 + barOffset, CELL_WIDTH - 5); + + //Number of kills + var killsText = "Kills: " + client.kills; + var killsOffset = 20 + BAR_WIDTH + barOffset; + ctx.fillText(killsText, killsOffset, CELL_WIDTH - 5); + + //Calcuate rank + var sorted = []; + client.getPlayers().forEach(function(val) { + sorted.push({player: val, portion: playerPortion[val.num]}); + }); + sorted.sort(function(a, b) { + if (a.portion === b.portion) return a.player.num - b.player.num; + else return b.portion - a.portion; + }); + + var rank = sorted.findIndex(function(val) {return val.player === user}); + ctx.fillText("Rank: " + (rank === -1 ? "--" : rank + 1) + " of " + sorted.length, + ctx.measureText(killsText).width + killsOffset + 20, CELL_WIDTH - 5); + + //Rolling the leaderboard bars. + if (sorted.length > 0) + { + var maxPortion = sorted[0].portion; + client.getPlayers().forEach(function(player) { + var rolling = barProportionRolling[player.num]; + rolling.value = playerPortion[player.num] / maxPortion; + rolling.update(); + }); + } + + //Show leaderboard. + var leaderboardNum = Math.min(5, sorted.length); + for (var i = 0; i < leaderboardNum; i++) + { + var player = sorted[i].player; + var name = player.name || "Unnamed"; + var portion = barProportionRolling[player.num].lag; + + 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); + var offset = i == 0 ? 10 : 0; + + ctx.fillStyle = 'rgba(10, 10, 10, .3)'; + ctx.fillRect(barX - 10, barY + 10 - offset, barSize + 10, BAR_HEIGHT + offset); + 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 - 15, barY + 27); + + var percentage = (portionsRolling[player.num].lag * 100).toFixed(3) + "%"; + ctx.fillStyle = "white"; + ctx.fillText(percentage, barX + 5, barY + CELL_WIDTH - 5); + } + +} + +function paint(ctx) +{ + + ctx.fillStyle = '#e2ebf3'; //'whitesmoke'; + ctx.fillRect(0, 0, canvasWidth, canvasHeight); + + //Move grid to viewport as said with the offsets, below the stats + ctx.save(); + ctx.translate(0, BAR_HEIGHT); + ctx.beginPath(); + ctx.rect(0, 0, gameWidth, gameHeight); + ctx.clip(); + + //Zoom in/out based on player stats. + ctx.scale(zoom, zoom); + ctx.translate(-offset[0] + BORDER_WIDTH, -offset[1] + BORDER_WIDTH); + + paintGrid(ctx); + client.getPlayers().forEach(function (p) { + var fr = p.waitLag; + if (fr < ANIMATE_FRAMES) + p.render(ctx, fr / ANIMATE_FRAMES); + else + p.render(ctx); + }); + + //Reset transform to paint fixed UI elements + ctx.restore(); + paintUIBar(ctx); + + if ((!user || user.dead) && !showedDead) + { + showedDead = true; + console.log("You died!"); + //return; + } +} + +function paintDoubleBuff() +{ + paint(offctx); + ctx.drawImage(offscreenCanvas, 0, 0); +} + +function update() { + updateSize(); + + //Change grid offsets. + for (var i = 0; i <= 1; i++) + { + if (animateTo[i] !== offset[i]) + { + if (client.allowAnimation) + { + var delta = animateTo[i] - offset[i]; + var dir = Math.sign(delta); + var mag = Math.min(SPEED, Math.abs(delta)); + offset[i] += dir * mag; + } + else + offset[i] = animateTo[i]; + } + } + + //Calculate player portions. + client.getPlayers().forEach(function(player) { + var roll = portionsRolling[player.num]; + roll.value = playerPortion[player.num] / GRID_SIZE / GRID_SIZE; + roll.update(); + }); + + //Zoom goes from 1 to .5, decreasing as portion goes up. TODO: maybe can modify this? + if (portionsRolling[user.num]) + zoom = 1 / (portionsRolling[user.num].lag + 1); + + //TODO: animate player is dead. (maybe explosion?), and tail rewinds itself. + if (user) centerOnPlayer(user, animateTo); +} + +//Helper methods. +function centerOnPlayer(player, pos) +{ + var xOff = Math.floor(player.posX - (gameWidth / zoom - CELL_WIDTH) / 2); + var yOff = Math.floor(player.posY - (gameHeight / zoom - CELL_WIDTH) / 2); + var gridWidth = grid.size * CELL_WIDTH + BORDER_WIDTH * 2; + pos[0] = xOff; //Math.max(Math.min(xOff, gridWidth + (BAR_WIDTH + 100) / zoom - gameWidth / zoom), 0); + pos[1] = yOff; //Math.max(Math.min(yOff, gridWidth - gameHeight / zoom), 0); +} + +function getBounceOffset(frame) +{ + var offsetBounce = ANIMATE_FRAMES; + var bounceNum = BOUNCE_FRAMES.length - 1; + while (bounceNum >= 0 && frame < offsetBounce - BOUNCE_FRAMES[bounceNum]) + { + offsetBounce -= BOUNCE_FRAMES[bounceNum]; + bounceNum--; + } + + if (bounceNum === -1) + { + return (offsetBounce - frame) * DROP_SPEED; + } + else + { + offsetBounce -= BOUNCE_FRAMES[bounceNum]; + frame = frame - offsetBounce; + var midFrame = BOUNCE_FRAMES[bounceNum] / 2; + if (frame >= midFrame) + return (BOUNCE_FRAMES[bounceNum] - frame) * DROP_SPEED; + else + return frame * DROP_SPEED; + } +} + +function showStats() { + //TODO: Show score stats. + $("#begin").removeClass("hidden"); + $("#begin").animate({ + opacity: .9999 + }, 1000, function() { + $("#stats").addClass("hidden").css("opacity", 0); + }); +} + +module.exports = exports = { + addPlayer: function(player) { + playerPortion[player.num] = 0; + portionsRolling[player.num] = new Rolling(9 / GRID_SIZE / GRID_SIZE, ANIMATE_FRAMES); + barProportionRolling[player.num] = new Rolling(0, ANIMATE_FRAMES); + }, + disconnect: function() { + //Show score stats. + $("#stats").removeClass("hidden"); + $("#stats").animate({ + opacity: .9999 + }, 3000, function() { + showStats(); + //Then fade back into the login screen. + }); + }, + removePlayer: function(player) { + delete playerPortion[player.num]; + delete portionsRolling[player.num]; + delete barProportionRolling[player.num]; + }, + + setUser: function(player) { + user = player; + centerOnPlayer(user, offset); + }, + reset: reset, + updateGrid: function(row, col, before, after) { + //Keep track of areas. + if (before) + playerPortion[before.num]--; + if (after) + playerPortion[after.num]++; + + //Queue animation + if (before === after || !client.allowAnimation) + return; + animateGrid.set(row, col, { + before: before, + after: after, + frame: 0 + }); + }, + paint: paintDoubleBuff, + update: update +}; },{"../client":3,"../game-core":9,"./rolling":1}],3:[function(require,module,exports){ -var core = require('./game-core'); -var Player = core.Player; - -var io = require('socket.io-client'); - -var GRID_SIZE = core.GRID_SIZE; -var CELL_WIDTH = core.CELL_WIDTH; - -var running = false; -var user, socket, frame; -var players, allPlayers; - -var kills; - -var timeout = undefined; -var dirty = false; -var deadFrames = 0; -var requesting = -1; //frame that we are requesting at. -var frameCache = []; //Frames after our request. - -var allowAnimation = true; - -var grid = new core.Grid(core.GRID_SIZE, function(row, col, before, after) { - invokeRenderer('updateGrid', [row, col, before, after]); -}); - -/** - * Provides requestAnimationFrame in a cross browser way. (edited so that this is also compatible with node.) - * @author paulirish / http://paulirish.com/ - */ -// window.requestAnimationFrame = function( /* function FrameRequestCallback */ callback, /* DOMElement Element */ element ) { -// window.setTimeout( callback, 1000 / 60 ); -// }; - -var hasWindow; -try { - window.document; - hasWindow = true; -} catch (e) { - hasWindow = false; -} - -var requestAnimationFrame; -if ( !requestAnimationFrame ) { - requestAnimationFrame = ( function() { - if (hasWindow) { - return window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - window.oRequestAnimationFrame || - window.msRequestAnimationFrame || - function( /* function FrameRequestCallback */ callback, /* DOMElement Element */ element ) { - setTimeout( callback, 1000 / 60 ); - }; - } else { - return function( /* function FrameRequestCallback */ callback, /* DOMElement Element */ element ) { - setTimeout( callback, 1000 / 60 ); - }; - } - })(); -} - -//Public API -function connectGame(url, name, callback) { - if (running) - return; //Prevent multiple runs. - running = true; - - user = null; - deadFrames = 0; - - //Socket connection. - io.j = []; - io.sockets = []; - socket = io(url, { - 'forceNew': true, - upgrade: false, - transports: ['websocket'] - }); - socket.on('connect', function(){ - console.info('Connected to server.'); - }); - socket.on('game', function(data) { - if (timeout != undefined) - clearTimeout(timeout); - - //Initialize game. - //TODO: display data.gameid --- game id # - frame = data.frame; - - reset(); - - //Load players. - data.players.forEach(function(p) { - var pl = new Player(grid, p); - addPlayer(pl); - }); - user = allPlayers[data.num]; - if (!user) - throw new Error(); - setUser(user); - - //Load grid. - var gridData = new Uint8Array(data.grid); - for (var r = 0; r < grid.size; r++) - for (var c = 0; c < grid.size; c++) - { - var ind = gridData[r * grid.size + c] - 1; - grid.set(r, c, ind === -1 ? null : players[ind]); - } - - invokeRenderer('paint', []); - frame = data.frame; - - if (requesting !== -1) - { - //Update those cache frames after we updated game. - var minFrame = requesting; - requesting = -1; - while (frameCache.length > frame - minFrame) - processFrame(frameCache[frame - minFrame]); - frameCache = []; - } - }); - - socket.on('notifyFrame', processFrame); - - socket.on('dead', function() { - socket.disconnect(); //In case we didn't get the disconnect call. - }); - - socket.on('disconnect', function(){ - if (!user) - return; - console.info('Server has disconnected. Creating new game.'); - socket.disconnect(); - user.die(); - dirty = true; - paintLoop(); - running = false; - invokeRenderer('disconnect', []); - }); - - socket.emit('hello', { - name: name, - type: 0, //Free-for-all - gameid: -1 //Requested game-id, or -1 for anyone. - }, function(success, msg) { - if (success) - console.info('Connected to game!'); - else { - console.error('Unable to connect to game: ' + msg); - running = false; - } - if (callback) - callback(success, msg); - }); - - -} - -function changeHeading(newHeading) { - if (!user || user.dead) - return; - if (newHeading === user.currentHeading || ((newHeading % 2 === 0) ^ - (user.currentHeading % 2 === 0))) - { - //user.heading = newHeading; - if (socket) { - socket.emit('frame', { - frame: frame, - heading: newHeading - }, function(success, msg) { - if (!success) - console.error(msg); - }); - } - } -} - -function getPlayers() { - return players.slice(); -} - -//Private API -function addPlayer(player) { - if (allPlayers[player.num]) - return; //Already added. - allPlayers[player.num] = players[players.length] = player; - invokeRenderer('addPlayer', [player]); - return players.length - 1; -} - -function invokeRenderer(name, args) { - var renderer = exports.renderer; - if (renderer && typeof renderer[name] === 'function') - renderer[name].apply(exports, args); -} - - -function processFrame(data) -{ - if (timeout != undefined) - clearTimeout(timeout); - - if (requesting !== -1 && requesting < data.frame) - { - frameCache.push(data); - return; - } - - if (data.frame - 1 !== frame) - { - console.error('Frames don\'t match up!'); - socket.emit('requestFrame'); //Restore data. - requesting = data.frame; - frameCache.push(data); - return; - } - - frame++; - if (data.newPlayers) - { - data.newPlayers.forEach(function(p) { - if (p.num === user.num) - return; - var pl = new Player(grid, p); - addPlayer(pl); - core.initPlayer(grid, pl); - }); - } - - var found = new Array(players.length); - data.moves.forEach(function(val, i) { - var player = allPlayers[val.num]; - if (!player) return; - if (val.left) player.die(); - found[i] = true; - player.heading = val.heading; - }); - for (var i = 0; i < players.length; i++) - { - //Implicitly leaving game. - if (!found[i]) - { - var player = players[i]; - player && player.die(); - } - } - - update(); - - var locs = {}; - for (var i = 0; i < players.length; i++) - { - var p = players[i]; - locs[p.num] = [p.posX, p.posY, p.waitLag]; - } - - /* - socket.emit('verify', { - frame: frame, - locs: locs - }, function(frame, success, adviceFix, msg) { - if (!success && requesting === -1) - { - console.error(frame + ': ' + msg); - if (adviceFix) - socket.emit('requestFrame'); - } - }.bind(this, frame)); - */ - - dirty = true; - requestAnimationFrame(function() { - paintLoop(); - }); - timeout = setTimeout(function() { - console.warn('Server has timed-out. Disconnecting.'); - socket.disconnect(); - }, 3000); -} - -function paintLoop() -{ - if (!dirty) - return; - invokeRenderer('paint', []); - dirty = false; - - if (user && user.dead) - { - if (timeout) - clearTimeout(timeout); - if (deadFrames === 60) //One second of frame - { - var before =allowAnimation; - allowAnimation = false; - update(); - invokeRenderer('paint', []); - allowAnimation = before; - user = null; - deadFrames = 0; - return; - } - - socket.disconnect(); - deadFrames++; - dirty = true; - update(); - requestAnimationFrame(paintLoop); - } -} - -function reset() { - user = null; - - grid.reset(); - players = []; - allPlayers = []; - kills = 0; - - invokeRenderer('reset'); -} - -function setUser(player) { - user = player; - invokeRenderer('setUser', [player]); -} - -function update() { - var dead = []; - core.updateFrame(grid, players, dead, function addKill(killer, other) - { - if (players[killer] === user && killer !== other) - kills++; - }); - dead.forEach(function(val) { - console.log((val.name || 'Unnamed') + ' is dead'); - delete allPlayers[val.num]; - invokeRenderer('removePlayer', [val]); - }); - - invokeRenderer('update', [frame]); -} - -//Export stuff -var funcs = [connectGame, changeHeading, getPlayers]; -funcs.forEach(function (f) { - exports[f.name] = f; -}); - -exports.renderer = null; -Object.defineProperties(exports, { - allowAnimation: { - get: function() { return allowAnimation; }, - set: function(val) { allowAnimation = !!val; }, - enumerable: true - }, - grid: { - get: function() { return grid; }, - enumerable: true - }, - kills: { - get: function() { return kills; }, - enumerable: true - } -}); +var core = require('./game-core'); +var Player = core.Player; + +var io = require('socket.io-client'); + +var GRID_SIZE = core.GRID_SIZE; +var CELL_WIDTH = core.CELL_WIDTH; + +var running = false; +var user, socket, frame; +var players, allPlayers; + +var kills; + +var timeout = undefined; +var dirty = false; +var deadFrames = 0; +var requesting = -1; //frame that we are requesting at. +var frameCache = []; //Frames after our request. + +var allowAnimation = true; + +var grid = new core.Grid(core.GRID_SIZE, function(row, col, before, after) { + invokeRenderer('updateGrid', [row, col, before, after]); +}); + +/** + * Provides requestAnimationFrame in a cross browser way. (edited so that this is also compatible with node.) + * @author paulirish / http://paulirish.com/ + */ +// window.requestAnimationFrame = function( /* function FrameRequestCallback */ callback, /* DOMElement Element */ element ) { +// window.setTimeout( callback, 1000 / 60 ); +// }; + +var hasWindow; +try { + window.document; + hasWindow = true; +} catch (e) { + hasWindow = false; +} + +var requestAnimationFrame; +if ( !requestAnimationFrame ) { + requestAnimationFrame = ( function() { + if (hasWindow) { + return window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function( /* function FrameRequestCallback */ callback, /* DOMElement Element */ element ) { + setTimeout( callback, 1000 / 60 ); + }; + } else { + return function( /* function FrameRequestCallback */ callback, /* DOMElement Element */ element ) { + setTimeout( callback, 1000 / 60 ); + }; + } + })(); +} + +//Public API +function connectGame(url, name, callback) { + if (running) + return; //Prevent multiple runs. + running = true; + + user = null; + deadFrames = 0; + + //Socket connection. + io.j = []; + io.sockets = []; + socket = io(url, { + 'forceNew': true, + upgrade: false, + transports: ['websocket'] + }); + socket.on('connect', function(){ + console.info('Connected to server.'); + }); + socket.on('game', function(data) { + if (timeout != undefined) + clearTimeout(timeout); + + //Initialize game. + //TODO: display data.gameid --- game id # + frame = data.frame; + + reset(); + + //Load players. + data.players.forEach(function(p) { + var pl = new Player(grid, p); + addPlayer(pl); + }); + user = allPlayers[data.num]; + if (!user) + throw new Error(); + setUser(user); + + //Load grid. + var gridData = new Uint8Array(data.grid); + for (var r = 0; r < grid.size; r++) + for (var c = 0; c < grid.size; c++) + { + var ind = gridData[r * grid.size + c] - 1; + grid.set(r, c, ind === -1 ? null : players[ind]); + } + + invokeRenderer('paint', []); + frame = data.frame; + + if (requesting !== -1) + { + //Update those cache frames after we updated game. + var minFrame = requesting; + requesting = -1; + while (frameCache.length > frame - minFrame) + processFrame(frameCache[frame - minFrame]); + frameCache = []; + } + }); + + socket.on('notifyFrame', processFrame); + + socket.on('dead', function() { + socket.disconnect(); //In case we didn't get the disconnect call. + }); + + socket.on('disconnect', function(){ + if (!user) + return; + console.info('Server has disconnected. Creating new game.'); + socket.disconnect(); + user.die(); + dirty = true; + paintLoop(); + running = false; + invokeRenderer('disconnect', []); + }); + + socket.emit('hello', { + name: name, + type: 0, //Free-for-all + gameid: -1 //Requested game-id, or -1 for anyone. + }, function(success, msg) { + if (success) + console.info('Connected to game!'); + else { + console.error('Unable to connect to game: ' + msg); + running = false; + } + if (callback) + callback(success, msg); + }); + + +} + +function changeHeading(newHeading) { + if (!user || user.dead) + return; + if (newHeading === user.currentHeading || ((newHeading % 2 === 0) ^ + (user.currentHeading % 2 === 0))) + { + //user.heading = newHeading; + if (socket) { + socket.emit('frame', { + frame: frame, + heading: newHeading + }, function(success, msg) { + if (!success) + console.error(msg); + }); + } + } +} + +function getUser() { + return user; +} + +function getOthers() { + var ret = []; + for (var p of players) { + if (p !== user) { + ret.push(p); + } + } + return ret; +} + +function getPlayers() { + return players.slice(); +} + +//Private API +function addPlayer(player) { + if (allPlayers[player.num]) + return; //Already added. + allPlayers[player.num] = players[players.length] = player; + invokeRenderer('addPlayer', [player]); + return players.length - 1; +} + +function invokeRenderer(name, args) { + var renderer = exports.renderer; + if (renderer && typeof renderer[name] === 'function') + renderer[name].apply(exports, args); +} + + +function processFrame(data) +{ + if (timeout != undefined) + clearTimeout(timeout); + + if (requesting !== -1 && requesting < data.frame) + { + frameCache.push(data); + return; + } + + if (data.frame - 1 !== frame) + { + console.error('Frames don\'t match up!'); + socket.emit('requestFrame'); //Restore data. + requesting = data.frame; + frameCache.push(data); + return; + } + + frame++; + if (data.newPlayers) + { + data.newPlayers.forEach(function(p) { + if (p.num === user.num) + return; + var pl = new Player(grid, p); + addPlayer(pl); + core.initPlayer(grid, pl); + }); + } + + var found = new Array(players.length); + data.moves.forEach(function(val, i) { + var player = allPlayers[val.num]; + if (!player) return; + if (val.left) player.die(); + found[i] = true; + player.heading = val.heading; + }); + for (var i = 0; i < players.length; i++) + { + //Implicitly leaving game. + if (!found[i]) + { + var player = players[i]; + player && player.die(); + } + } + + update(); + + var locs = {}; + for (var i = 0; i < players.length; i++) + { + var p = players[i]; + locs[p.num] = [p.posX, p.posY, p.waitLag]; + } + + /* + socket.emit('verify', { + frame: frame, + locs: locs + }, function(frame, success, adviceFix, msg) { + if (!success && requesting === -1) + { + console.error(frame + ': ' + msg); + if (adviceFix) + socket.emit('requestFrame'); + } + }.bind(this, frame)); + */ + + dirty = true; + requestAnimationFrame(function() { + paintLoop(); + }); + timeout = setTimeout(function() { + console.warn('Server has timed-out. Disconnecting.'); + socket.disconnect(); + }, 3000); +} + +function paintLoop() +{ + if (!dirty) + return; + invokeRenderer('paint', []); + dirty = false; + + if (user && user.dead) + { + if (timeout) + clearTimeout(timeout); + if (deadFrames === 60) //One second of frame + { + var before =allowAnimation; + allowAnimation = false; + update(); + invokeRenderer('paint', []); + allowAnimation = before; + user = null; + deadFrames = 0; + return; + } + + socket.disconnect(); + deadFrames++; + dirty = true; + update(); + requestAnimationFrame(paintLoop); + } +} + +function reset() { + user = null; + + grid.reset(); + players = []; + allPlayers = []; + kills = 0; + + invokeRenderer('reset'); +} + +function setUser(player) { + user = player; + invokeRenderer('setUser', [player]); +} + +function update() { + var dead = []; + core.updateFrame(grid, players, dead, function addKill(killer, other) + { + if (players[killer] === user && killer !== other) + kills++; + }); + dead.forEach(function(val) { + console.log((val.name || 'Unnamed') + ' is dead'); + delete allPlayers[val.num]; + invokeRenderer('removePlayer', [val]); + }); + + invokeRenderer('update', [frame]); +} + +//Export stuff +var funcs = [connectGame, changeHeading, getOthers, getPlayers, getUser]; +funcs.forEach(function (f) { + exports[f.name] = f; +}); + +exports.renderer = null; +Object.defineProperties(exports, { + allowAnimation: { + get: function() { return allowAnimation; }, + set: function(val) { allowAnimation = !!val; }, + enumerable: true + }, + grid: { + get: function() { return grid; }, + enumerable: true + }, + kills: { + get: function() { return kills; }, + enumerable: true + } +}); },{"./game-core":9,"socket.io-client":44}],4:[function(require,module,exports){ /* global $ */ @@ -974,123 +988,123 @@ $(document).keydown(function(e) { }); },{"./client":3,"./client-modes/user-mode":2,"./game-core":9,"socket.io-client":44}],5:[function(require,module,exports){ - - -function Color(h, s, l, a) -{ - verifyRange(h, s, l); - if (a === undefined) a = 1; - else verifyRange(a); - - Object.defineProperties(this, { - "hue": {value: h, enumerable: true}, - "sat": {value: s, enumerable: true}, - "lum": {value: l, enumerable: true}, - "alpha": {value: a, enumerable: true}, - }); -} - -Color.fromData = function(data) { - return new Color(data.hue, data.sat, data.lum, data.alpha); -}; - -function verifyRange() -{ - for (var i = 0; i < arguments.length; i++) - { - if (arguments[i] < 0 || arguments[i] > 1) - throw new RangeError("H, S, L, and A parameters must be between the range [0, 1]"); - } -} - -Color.prototype.interpolateToString = function(color, amount) -{ - var rgbThis = hslToRgb(this.hue, this.sat, this.lum); - var rgbThat = hslToRgb(color.hue, color.sat, color.lum); - var rgb = []; - - for (var i = 0; i < 3; i++) - rgb[i] = Math.floor((rgbThat[i] - rgbThis[i]) * amount + rgbThis[i]); - return {rgbString: function() {return 'rgb(' + rgb[0] + ', ' + rgb[1] + ', ' + rgb[2] + ')'}}; -} - -Color.prototype.deriveLumination = function(amount) -{ - var lum = this.lum + amount; - lum = Math.min(Math.max(lum, 0), 1); - return new Color(this.hue, this.sat, lum, this.alpha); -}; - -Color.prototype.deriveHue = function(amount) -{ - var hue = this.hue - amount; - return new Color(hue - Math.floor(hue), this.sat, this.lum, this.alpha); -}; - -Color.prototype.deriveSaturation = function(amount) -{ - var sat = this.sat + amount; - sat = Math.min(Math.max(sat, 0), 1); - return new Color(this.hue, sat, this.lum, this.alpha); -}; - -Color.prototype.deriveAlpha = function(newAlpha) -{ - verifyRange(newAlpha); - return new Color(this.hue, this.sat, this.lum, newAlpha); -}; - -Color.prototype.rgbString = function() { - var rgb = hslToRgb(this.hue, this.sat, this.lum); - rgb[3] = this.a; - return 'rgba(' + rgb[0] + ', ' + rgb[1] + ', ' + rgb[2] + ', ' + this.alpha + ')'; -}; - - -//http://stackoverflow.com/a/9493060/7344257 -function hslToRgb(h, s, l){ - var r, g, b; - - if(s == 0){ - r = g = b = l; // achromatic - }else{ - var hue2rgb = function hue2rgb(p, q, t){ - if(t < 0) t += 1; - if(t > 1) t -= 1; - if(t < 1/6) return p + (q - p) * 6 * t; - if(t < 1/2) return q; - if(t < 2/3) return p + (q - p) * (2/3 - t) * 6; - return p; - }; - - var q = l < 0.5 ? l * (1 + s) : l + s - l * s; - var p = 2 * l - q; - r = hue2rgb(p, q, h + 1/3); - g = hue2rgb(p, q, h); - b = hue2rgb(p, q, h - 1/3); - } - - return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; -} - + + +function Color(h, s, l, a) +{ + verifyRange(h, s, l); + if (a === undefined) a = 1; + else verifyRange(a); + + Object.defineProperties(this, { + "hue": {value: h, enumerable: true}, + "sat": {value: s, enumerable: true}, + "lum": {value: l, enumerable: true}, + "alpha": {value: a, enumerable: true}, + }); +} + +Color.fromData = function(data) { + return new Color(data.hue, data.sat, data.lum, data.alpha); +}; + +function verifyRange() +{ + for (var i = 0; i < arguments.length; i++) + { + if (arguments[i] < 0 || arguments[i] > 1) + throw new RangeError("H, S, L, and A parameters must be between the range [0, 1]"); + } +} + +Color.prototype.interpolateToString = function(color, amount) +{ + var rgbThis = hslToRgb(this.hue, this.sat, this.lum); + var rgbThat = hslToRgb(color.hue, color.sat, color.lum); + var rgb = []; + + for (var i = 0; i < 3; i++) + rgb[i] = Math.floor((rgbThat[i] - rgbThis[i]) * amount + rgbThis[i]); + return {rgbString: function() {return 'rgb(' + rgb[0] + ', ' + rgb[1] + ', ' + rgb[2] + ')'}}; +} + +Color.prototype.deriveLumination = function(amount) +{ + var lum = this.lum + amount; + lum = Math.min(Math.max(lum, 0), 1); + return new Color(this.hue, this.sat, lum, this.alpha); +}; + +Color.prototype.deriveHue = function(amount) +{ + var hue = this.hue - amount; + return new Color(hue - Math.floor(hue), this.sat, this.lum, this.alpha); +}; + +Color.prototype.deriveSaturation = function(amount) +{ + var sat = this.sat + amount; + sat = Math.min(Math.max(sat, 0), 1); + return new Color(this.hue, sat, this.lum, this.alpha); +}; + +Color.prototype.deriveAlpha = function(newAlpha) +{ + verifyRange(newAlpha); + return new Color(this.hue, this.sat, this.lum, newAlpha); +}; + +Color.prototype.rgbString = function() { + var rgb = hslToRgb(this.hue, this.sat, this.lum); + rgb[3] = this.a; + return 'rgba(' + rgb[0] + ', ' + rgb[1] + ', ' + rgb[2] + ', ' + this.alpha + ')'; +}; + + +//http://stackoverflow.com/a/9493060/7344257 +function hslToRgb(h, s, l){ + var r, g, b; + + if(s == 0){ + r = g = b = l; // achromatic + }else{ + var hue2rgb = function hue2rgb(p, q, t){ + if(t < 0) t += 1; + if(t > 1) t -= 1; + if(t < 1/6) return p + (q - p) * 6 * t; + if(t < 1/2) return q; + if(t < 2/3) return p + (q - p) * (2/3 - t) * 6; + return p; + }; + + var q = l < 0.5 ? l * (1 + s) : l + s - l * s; + var p = 2 * l - q; + r = hue2rgb(p, q, h + 1/3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1/3); + } + + return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; +} + module.exports = Color; },{}],6:[function(require,module,exports){ -function constant(val) { - return { - value: val, - enumerable: true - }; -} - -var consts = { - GRID_SIZE: constant(80), - CELL_WIDTH: constant(40), - SPEED: constant(5), - BORDER_WIDTH: constant(20), - MAX_PLAYERS: constant(81) -}; - -Object.defineProperties(module.exports, consts); +function constant(val) { + return { + value: val, + enumerable: true + }; +} + +var consts = { + GRID_SIZE: constant(80), + CELL_WIDTH: constant(40), + SPEED: constant(5), + BORDER_WIDTH: constant(20), + MAX_PLAYERS: constant(81) +}; + +Object.defineProperties(module.exports, consts); },{}],7:[function(require,module,exports){ var ANIMATE_FRAMES = 24; @@ -1229,627 +1243,627 @@ function area(player) return Math.abs(player.posX - xDest); } },{}],8:[function(require,module,exports){ -function Grid(size, changeCallback) -{ - var grid = new Array(size); - var modified = false; - - var data = { - grid: grid, - size: size - }; - - this.get = function(row, col) - { - if (isOutOfBounds(data, row, col)) - throw new RangeError("Row or Column value out of bounds"); - return grid[row] && grid[row][col]; - } - this.set = function(row, col, value) - { - if (isOutOfBounds(data, row, col)) - throw new RangeError("Row or Column value out of bounds"); - - if (!grid[row]) - grid[row] = new Array(size); - var before = grid[row][col]; - grid[row][col] = value; - - if (typeof changeCallback === "function") - changeCallback(row, col, before, value); - - modified = true; - - return before; - } - this.reset = function() { - if (modified) - { - grid = new Array(size); - modified = false; - } - } - - this.isOutOfBounds = isOutOfBounds.bind(this, data); - - Object.defineProperty(this, "size", { - get: function() {return size; }, - enumerable: true - }); - -} - -function isOutOfBounds(data, row, col) -{ - return row < 0 || row >= data.size || col < 0 || col >= data.size; -} - +function Grid(size, changeCallback) +{ + var grid = new Array(size); + var modified = false; + + var data = { + grid: grid, + size: size + }; + + this.get = function(row, col) + { + if (isOutOfBounds(data, row, col)) + throw new RangeError("Row or Column value out of bounds"); + return grid[row] && grid[row][col]; + } + this.set = function(row, col, value) + { + if (isOutOfBounds(data, row, col)) + throw new RangeError("Row or Column value out of bounds"); + + if (!grid[row]) + grid[row] = new Array(size); + var before = grid[row][col]; + grid[row][col] = value; + + if (typeof changeCallback === "function") + changeCallback(row, col, before, value); + + modified = true; + + return before; + } + this.reset = function() { + if (modified) + { + grid = new Array(size); + modified = false; + } + } + + this.isOutOfBounds = isOutOfBounds.bind(this, data); + + Object.defineProperty(this, "size", { + get: function() {return size; }, + enumerable: true + }); + +} + +function isOutOfBounds(data, row, col) +{ + return row < 0 || row >= data.size || col < 0 || col >= data.size; +} + module.exports = Grid; },{}],9:[function(require,module,exports){ -var core = require('./game-core'); -var consts = require('./game-consts'); - -exports.Color = require('./color'); -exports.Grid = require('./grid'); -exports.Player = require('./player'); - -exports.initPlayer = core.initPlayer; -exports.updateFrame = core.updateFrame; - -for (var prop in consts) { - Object.defineProperty(exports, prop, { - enumerable: true, - value: consts[prop] - }); -} - +var core = require('./game-core'); +var consts = require('./game-consts'); + +exports.Color = require('./color'); +exports.Grid = require('./grid'); +exports.Player = require('./player'); + +exports.initPlayer = core.initPlayer; +exports.updateFrame = core.updateFrame; + +for (var prop in consts) { + Object.defineProperty(exports, prop, { + enumerable: true, + value: consts[prop] + }); +} + },{"./color":5,"./game-consts":6,"./game-core":7,"./grid":8,"./player":10}],10:[function(require,module,exports){ -var Stack = require("./stack"); -var Color = require("./color"); -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(row, col) || 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(grid, sdata) { - var data = {}; - - //Parameters - data.num = sdata.num; - data.name = sdata.name || ""; //|| "Player " + (data.num + 1); - 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. - var base; - if (sdata.base) - base = this.baseColor = sdata.base instanceof Color ? sdata.base : Color.fromData(sdata.base); - else - { - var hue = Math.random(); - this.baseColor = base = new Color(hue, .8, .5); - } - this.lightBaseColor = base.deriveLumination(.1); - this.shadowColor = base.deriveLumination(-.3); - this.tailColor = base.deriveLumination(.3).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 { - base: this.baseColor, - 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; +var Stack = require("./stack"); +var Color = require("./color"); +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(row, col) || 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(grid, sdata) { + var data = {}; + + //Parameters + data.num = sdata.num; + data.name = sdata.name || ""; //|| "Player " + (data.num + 1); + 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. + var base; + if (sdata.base) + base = this.baseColor = sdata.base instanceof Color ? sdata.base : Color.fromData(sdata.base); + else + { + var hue = Math.random(); + this.baseColor = base = new Color(hue, .8, .5); + } + this.lightBaseColor = base.deriveLumination(.1); + this.shadowColor = base.deriveLumination(-.3); + this.tailColor = base.deriveLumination(.3).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 { + base: this.baseColor, + 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; },{"./color":5,"./game-consts.js":6,"./grid.js":8,"./stack":11}],11:[function(require,module,exports){ - - -function Stack(initSize) -{ - var len = 0; - var arr = []; - - this.ensureCapacity = function(size) - { - arr.length = Math.max(arr.length, size || 0); - }; - - this.push = function(ele) - { - this[len] = ele; - len++; - }; - - this.pop = function() - { - if (len === 0) - return; - len--; - var tmp = this[len]; - this[len] = undefined; - return tmp; - }; - - this.isEmpty = function() { - return len === 0; - } - - this.ensureCapacity(initSize); - - - Object.defineProperty(this, "length", { - get: function() {return len;} - }); -} - + + +function Stack(initSize) +{ + var len = 0; + var arr = []; + + this.ensureCapacity = function(size) + { + arr.length = Math.max(arr.length, size || 0); + }; + + this.push = function(ele) + { + this[len] = ele; + len++; + }; + + this.pop = function() + { + if (len === 0) + return; + len--; + var tmp = this[len]; + this[len] = undefined; + return tmp; + }; + + this.isEmpty = function() { + return len === 0; + } + + this.ensureCapacity(initSize); + + + Object.defineProperty(this, "length", { + get: function() {return len;} + }); +} + module.exports = Stack; },{}],12:[function(require,module,exports){ module.exports = after