battleships-1.0.0: static/js/map.js
// Creates a mouse handler that calculates client coordinates before calling the real handler.
function mkClientMouseEvent(handler, elem) {
return function(evt) {
var parentOffset = $(elem).offset();
evt.elemX = evt.pageX - parentOffset.left;
evt.elemY = evt.pageY - parentOffset.top;
handler(evt);
};
}
///////////////////////////////////////////////////////////////////////////////
// class Map
///////////////////////////////////////////////////////////////////////////////
function Map(canvas, numX, numY, cellSize, shipDef) {
var drag = new MouseDrag(canvas, 4);
var render = new Render(canvas, numX, numY, cellSize);
var that = this; // this-reference for callbacks
// convert between grid and canvas coordinates
var toC = function(gridCoord) { return gridCoord * cellSize; };
var toG = function(canvasCoord) { return Math.floor(canvasCoord / cellSize); };
// PUBLIC PROPERTIES
this.ships = []
this.safetyMargin = 1;
this.shipDef = shipDef;
this.width = numX * cellSize;
this.height = numY * cellSize;
// update canvas
$(canvas).attr('width', this.width);
$(canvas).attr('height', this.height);
// DRAG EVENTS
// drag.onDragStart.add(function (evt) {
// });
drag.onDragging.add(function (evt) {
shipOX = toG(evt.origin.x);
shipOY = toG(evt.origin.y);
curShipX = toG(evt.pos.x);
curShipY = toG(evt.pos.y);
var ship = Ship.fromLine(shipOX, shipOY, curShipX, curShipY);
that.redraw();
style = that.shipAdmissible(ship) ? render.validShipStyle : render.invalidShipStyle;
render.drawShip(ship, style);
});
drag.onDragged.add(function (evt) {
shipOX = toG(evt.origin.x);
shipOY = toG(evt.origin.y);
curShipX = toG(evt.pos.x);
curShipY = toG(evt.pos.y);
var ship = Ship.fromLine(shipOX, shipOY, curShipX, curShipY);
if(that.shipAdmissible(ship)) {
that.ships.push(ship);
that.shipDef.takeShip(ship.Size);
} else if(s = that.shipSelected(ship)) {
that.ships.splice(that.ships.indexOf(s),1);
that.shipDef.returnShip(s.Size);
}
that.redraw();
});
drag.onCancelDrag.add(function () {
that.redraw();
});
drag.onClick.add(function (evt) {
shipOX = toG(evt.pos.x);
shipOY = toG(evt.pos.y);
for (var i = that.ships.length - 1; i >= 0; i--) {
if(that.ships[i].contains(shipOX, shipOY, 0)) {
that.shipDef.returnShip(that.ships[i].Size);
that.ships.splice(i, 1);
}
};
that.redraw();
});
this.shipAdmissible = function (ship) {
for (var i = this.ships.length - 1; i >= 0; i--) {
if(ship.intersects(this.ships[i], this.safetyMargin)) {
return false;
}
};
return shipDef.hasShip(ship.Size);
};
this.redraw = function() {
render.clear();
for (var i = this.ships.length - 1; i >= 0; i--) {
render.drawShipMargin(this.ships[i], this.safetyMargin);
render.drawShip(this.ships[i], render.placedShipStyle);
};
};
this.reset = function() {
this.ships.splice(0, this.ships.length);
this.shipDef.reset();
this.redraw();
};
this.shipSelected = function (ship) {
for (var i = this.ships.length - 1; i >= 0; i--) {
if (ship.equals(this.ships[i])) {
return this.ships[i];
}
}
return null;
}
// initial redraw
this.redraw();
}
///////////////////////////////////////////////////////////////////////////////
// class MouseDrag
///////////////////////////////////////////////////////////////////////////////
function MouseDrag(elem, threshold) {
var startDragging = false;
var dragging = false;
// define events
this.onClick = new CustomEvent();
this.onDragStart = new CustomEvent();
this.onDragging = new CustomEvent();
this.onDragged = new CustomEvent();
this.onCancelDrag = new CustomEvent();
var dragOrigin;
var that = this;
var onMouseDown = function(evt) {
dragOrigin = {x: evt.elemX, y:evt.elemY};
startDragging = true;
evt.preventDefault();
};
var onMouseMove = function(evt) {
if(startDragging) {
var dx = evt.elemX - dragOrigin.x;
var dy = evt.elemY - dragOrigin.y;
if(dx * dx + dy * dy >= threshold * threshold) {
startDragging = false;
dragging = true;
that.onDragStart.raise({origin: dragOrigin});
}
} else if(dragging) {
that.onDragging.raise({origin: dragOrigin, pos: {x:evt.elemX, y: evt.elemY}});
}
evt.preventDefault();
};
var onMouseUp = function(evt) {
if(dragging) {
that.onDragged.raise({origin: dragOrigin, pos: {x:evt.elemX, y: evt.elemY}});
} else if(startDragging) {
that.onClick.raise({pos: {x:evt.elemX, y: evt.elemY}});
}
dragging = false;
startDragging = false;
evt.preventDefault();
};
var onMouseLeave = function () {
if(dragging) {
that.onCancelDrag.raise();
}
startDragging = false;
dragging = false;
}
// register events
$(elem).mousedown(mkClientMouseEvent(onMouseDown, elem));
$(elem).mouseup(mkClientMouseEvent(onMouseUp, elem));
$(elem).mousemove(mkClientMouseEvent(onMouseMove, elem));
$(elem).mouseleave(onMouseLeave);
}
///////////////////////////////////////////////////////////////////////////////
// class CustomEvent
///////////////////////////////////////////////////////////////////////////////
function CustomEvent() {
this.callbacks = [];
}
CustomEvent.prototype.add = function (handler) {
this.callbacks.push(handler);
};
CustomEvent.prototype.remove = function (handler) {
var i = this.callbacks.indexOf(handler);
if(i >= 0) {
this.callbacks.splice(i, 1);
}
};
CustomEvent.prototype.raise = function (arg) {
for (var i = this.callbacks.length - 1; i >= 0; i--) {
this.callbacks[i](arg);
};
}
///////////////////////////////////////////////////////////////////////////////
// class Render
///////////////////////////////////////////////////////////////////////////////
function Render(canvas, numX, numY, cellSize) {
var c2d = canvas.getContext("2d");
// convert between grid and canvas coordinates
var toC = function(gridCoord) { return gridCoord * cellSize; };
var toG = function(canvasCoord) { return Math.floor(canvasCoord); };
// calculate pixel size of map
var width = toC(numX);
var height = toC(numY);
// resize canvas element
$(canvas).attr('width', width);
$(canvas).attr('height', height);
// ship styles
this.validShipStyle = { thickness: 1, border: 'rgb(0, 200, 0)', fill : '#AEF100' };
this.invalidShipStyle = { thickness: 1, border: 'rgb(255, 0, 0)', fill : '#FD0006' };
this.placedShipStyle = { thickness: 1, border: 'rgb(0, 0, 200)', fill : '#AEF100' };
this.blockedCellColor = 'rgba(200, 200, 200, 0.5)';
// grid style
this.gridStyle = { thickness: 2, color: '#D2F870'};
// rendering functions
this.clear = function () {
c2d.clearRect(0, 0, width, height);
};
this.drawGrid = function () {
c2d.save();
// draw map grid
c2d.beginPath();
c2d.strokeStyle = this.gridStyle.color;
c2d.lineWidth = this.gridStyle.thickness;
for(var x = 0; x <= width; x += cellSize) {
c2d.moveTo(x, 0);
c2d.lineTo(x, height);
}
for(var y = 0; y <= height; y += cellSize) {
c2d.moveTo(0, y);
c2d.lineTo(width, y);
}
c2d.stroke();
c2d.restore();
}
this.drawShip = function (ship, style) {
c2d.save();
c2d.strokeStyle = style.border;
c2d.lineWidth = style.thickness;
c2d.fillStyle = style.fill;
var rx = toC(ship.X);
var ry = toC(ship.Y);
var rw = toC(ship.Width);
var rh = toC(ship.Height);
c2d.fillRect(rx, ry, rw, rh);
c2d.strokeRect(rx, ry, rw, rh);
c2d.restore();
};
this.drawShipMargin = function (ship, margin) {
c2d.save();
c2d.fillStyle = this.blockedCellColor;
var rx = toC(ship.X - margin);
var ry = toC(ship.Y - margin);
var rw = toC(ship.Width + 2 * margin);
var rh = toC(ship.Height + 2 * margin);
c2d.fillRect(rx, ry, rw, rh);
c2d.restore();
};
}
///////////////////////////////////////////////////////////////////////////////
// class ShipDef
///////////////////////////////////////////////////////////////////////////////
function ShipDef(shipDefOrig) {
var shipDef = {};
this.shipTypes = [];
for(var len in shipDefOrig) {
shipDef[len] = shipDefOrig[len];
this.shipTypes.push(parseInt(len));
}
this.onChanged = new CustomEvent();
this.hasShip = function (length) {
return shipDef[length] > 0;
};
this.shipCount = function(length) {
return shipDef[length];
};
this.total = function() {
var t = 0;
for(var len in shipDef) {
t += shipDef[len];
}
return t;
};
this.takeShip = function (length) {
if(this.hasShip(length)) {
shipDef[length] -= 1;
}
this.onChanged.raise(this);
};
this.returnShip = function (length) {
if(shipDef[length] < shipDefOrig[length]) {
shipDef[length] += 1;
}
this.onChanged.raise(this);
};
this.reset = function () {
for(var len in shipDefOrig) {
shipDef[len] = shipDefOrig[len];
}
this.onChanged.raise(this);
};
}
ShipDef.fromList = function(shipList) {
var shipDefOrig = {};
for (var i = shipList.length - 1; i >= 0; i--) {
var len = shipList[i];
if(len in shipDefOrig)
shipDefOrig[len] += 1;
else
shipDefOrig[len] = 1;
};
return new ShipDef(shipDefOrig);
}
///////////////////////////////////////////////////////////////////////////////
// class Ship
///////////////////////////////////////////////////////////////////////////////
function Ship(x, y, size, orientation) {
this.X = x;
this.Y = y;
this.Size = size;
this.Orientation = orientation;
if(this.Orientation == 0) {
this.Width = this.Size;
this.Height = 1;
} else {
this.Width = 1;
this.Height = this.Size;
}
this.contains = function(x,y,margin) {
return (x >= this.X - margin) && (y >= this.Y - margin) && (x < this.X + this.Width + margin) && (y < this.Y + this.Height + margin);
};
this.intersects = function(other, margin) {
var tr = this.X + this.Width - 1;
var tb = this.Y + this.Height - 1;
var or = other.X + other.Width + margin - 1;
var ob = other.Y + other.Height + margin - 1;
return (this.X <= or &&
other.X - margin <= tr &&
this.Y <= ob &&
other.Y - margin <= tb);
};
this.equals = function(other) {
return (this.X == other.X &&
this.Y == other.Y &&
this.Orientation == other.Orientation);
}
}
Ship.fromLine = function(x1, y1, x2, y2) {
if(Math.abs(x2 - x1) >= Math.abs(y2 - y1)) {
y2 = y1;
if(x2 < x1) {
var t = x1;
x1 = x2;
x2 = t;
}
var shipSize = Math.abs(x2 - x1) + 1;
var orientation = 0;
} else {
x2 = x1;
if(y2 < y1) {
var t = y1;
y1 = y2;
y2 = t;
}
var shipSize = Math.abs(y2 - y1) + 1;
var orientation = 1;
}
return new Ship(x1, y1, shipSize, orientation);
}