mirror of https://github.com/espruino/BangleApps
322 lines
9.0 KiB
JavaScript
322 lines
9.0 KiB
JavaScript
const FIELD_WIDTH = [11, 11, 15]; // for each phase
|
|
const FIELD_HEIGHT = FIELD_WIDTH;
|
|
//const FIELD_LINE_WIDTH = 2;
|
|
const FIELD_MARGIN = 2;
|
|
const FIELD_COUNT_X = 10;
|
|
const FIELD_COUNT_Y = FIELD_COUNT_X;
|
|
const MARGIN_LEFT = 16;
|
|
const MARGIN_TOP = 42;
|
|
const HEADING_COLOR = ['#FF7070', '#7070FF']; // for each player
|
|
const FIELD_LINE_COLOR = '#FFFFFF';
|
|
const FIELD_BG_COLOR_REGULAR = '#808080';
|
|
const FIELD_BG_COLOR_SELECTED = '#FFFFFF';
|
|
const SHIP_COLOR_PLACED = '#507090';
|
|
const SHIP_COLOR_AVAIL = '#204070';
|
|
const STATE_HIT_COLOR = ['#B00000', '#0000B0']; // for each player
|
|
const STATE_MISS_COLOR = '#404040';
|
|
const SHIP_CAPS = [
|
|
1, // Carrier (type 0, size 5)
|
|
2, // Battleship (type 1, size 4)
|
|
3, // Destroyer (type 2, size 3)
|
|
4 // Patrol Boat (type 3, size 2)
|
|
];
|
|
const FULL_HITS = SHIP_CAPS.reduce((a, c, i) => a + c*(5 -i), 0);
|
|
const INDICATOR_LAYOUT = [
|
|
[0, 1, 1, 3],
|
|
[2, 2, 2, 3, 3, 3]
|
|
];
|
|
const INDICATORS = INDICATOR_LAYOUT.reduce((a, c, i) => {
|
|
let y = FIELD_COUNT_Y + 1 + i;
|
|
let x1 = 0;
|
|
c.forEach(type => {
|
|
let size = 5 - type;
|
|
let x2 = x1 + size - 1;
|
|
a.push({ "type": type, "position": [x1, y, x2, y] });
|
|
x1 += size;
|
|
});
|
|
return a;
|
|
}, []).sort((l, r) => (l.type - r.type)*FIELD_COUNT_X*FIELD_COUNT_Y
|
|
+ (l.position[0] + l.position[1]*FIELD_COUNT_X
|
|
- (r.position[0] + r.position[1]*FIELD_COUNT_X)));
|
|
|
|
let phase = 0;
|
|
let player = 0;
|
|
let selected = [-10, -10];
|
|
let to_add = null;
|
|
let to_rem = null;
|
|
let placements = [[],[]];
|
|
let field_states = [new Array(100).fill(0), new Array(FIELD_COUNT_X*FIELD_COUNT_Y).fill(0)];
|
|
let current = [[0, 0],[0, 0]];
|
|
let behaviours = []; // depending on phase
|
|
|
|
function getLeftOffset(x) {
|
|
return MARGIN_LEFT + x*(FIELD_WIDTH[phase] + FIELD_MARGIN + 1);
|
|
}
|
|
|
|
function getTopOffset(y) {
|
|
return MARGIN_TOP + y*(FIELD_HEIGHT[phase] + FIELD_MARGIN + 1);
|
|
}
|
|
|
|
function getFieldState(x, y) {
|
|
return field_states[player][x + FIELD_COUNT_X*y];
|
|
}
|
|
|
|
function setFieldState(x, y, value) {
|
|
field_states[player][x + FIELD_COUNT_X*y] = value;
|
|
}
|
|
|
|
function updateFieldStates() {
|
|
placements.forEach((ps, i) => {
|
|
ps.forEach(p => {
|
|
let pos = p.position;
|
|
for (let x = pos[0]; x <= pos[2]; x++)
|
|
for (let y = pos[1]; y <= pos[3]; y++) {
|
|
field_states[i][x + FIELD_COUNT_X*y] = 1;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function getHitCount() {
|
|
return field_states[player].reduce(
|
|
(v, state) => state == 3 ? v + 1 : v,
|
|
0);
|
|
}
|
|
|
|
function drawField(x, y, selected) {
|
|
let x1 = getLeftOffset(x);
|
|
let y1 = getTopOffset(y);
|
|
let x2 = x1 + FIELD_WIDTH[phase];
|
|
let y2 = y1 + FIELD_HEIGHT[phase];
|
|
let field_state = getFieldState(x, y);
|
|
g.setColor(selected ? FIELD_BG_COLOR_SELECTED : FIELD_BG_COLOR_REGULAR);
|
|
g.fillRect(x1, y1, x2, y2);
|
|
g.setColor(FIELD_LINE_COLOR);
|
|
g.drawRect(x1, y1, x2, y2);
|
|
switch (field_state) {
|
|
case 2:
|
|
g.setColor(STATE_MISS_COLOR);
|
|
g.fillCircle(x1 + FIELD_WIDTH[phase]/2 + 1, y1 + FIELD_HEIGHT[phase]/2 + 1, FIELD_WIDTH[phase]/2 - 3);
|
|
break;
|
|
case 3:
|
|
g.setColor(STATE_HIT_COLOR[player]);
|
|
g.fillCircle(x1 + FIELD_WIDTH[phase]/2 + 1, y1 + FIELD_HEIGHT[phase]/2 + 1, FIELD_WIDTH[phase]/2 - 1);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
function drawFields(x1, y1, x2, y2) {
|
|
let l = getLeftOffset(x1);
|
|
let t = getTopOffset(y1);
|
|
let r = getLeftOffset(x2) + FIELD_WIDTH[phase] + FIELD_MARGIN;
|
|
let b = getTopOffset(y2) + FIELD_HEIGHT[phase] + FIELD_MARGIN;
|
|
g.clearRect(l, t, r, b);
|
|
for (let x = x1; x <= x2; x++)
|
|
for (let y = y1; y <= y2; y++) {
|
|
drawField(x, y, x == current[player][0] && y == current[player][1]);
|
|
}
|
|
}
|
|
|
|
function drawShip(x1, y1, x2, y2, color) {
|
|
g.setColor(color);
|
|
let diam = Math.min(FIELD_HEIGHT[phase], FIELD_WIDTH[phase]) - 3;
|
|
let rad = diam/2;
|
|
let cx1 = getLeftOffset(x1) + FIELD_WIDTH[phase]/2 + 1;
|
|
let cy1 = getTopOffset(y1) + FIELD_HEIGHT[phase]/2 + 1;
|
|
let cx2 = getLeftOffset(x2) + FIELD_WIDTH[phase]/2 + 1;
|
|
let cy2 = getTopOffset(y2) + FIELD_HEIGHT[phase]/2 + 1;
|
|
if (x1 == x2) {
|
|
g.fillRect(cx1 - rad, cy1, cx1 + rad, cy2);
|
|
} else {
|
|
g.fillRect(cx1, cy1 - rad, cx2, cy1 + rad);
|
|
}
|
|
g.fillCircle(cx1, cy1, rad);
|
|
g.fillCircle(cx2, cy2, rad);
|
|
}
|
|
|
|
function hasCollision(pos) {
|
|
return placements[player].some(
|
|
p => pos[0] <= p.position[2]
|
|
&& pos[2] >= p.position[0]
|
|
&& pos[1] <= p.position[3]
|
|
&& pos[3] >= p.position[1]);
|
|
}
|
|
|
|
function isAvailable(type) {
|
|
let count = placements[player].reduce(
|
|
(v, p) => p.type == type ? v + 1 : v,
|
|
0);
|
|
return count < SHIP_CAPS[type];
|
|
}
|
|
|
|
function determineChanges() {
|
|
to_rem = to_add;
|
|
to_add = null;
|
|
if (selected[0] == current[player][0] && selected[1] == current[player][1]) return;
|
|
if (selected[0] == current[player][0]) {
|
|
let size = Math.abs(selected[1] - current[player][1]) + 1;
|
|
if (size < 2 || size > 5 ) return;
|
|
let y1 = Math.min(selected[1], current[player][1]);
|
|
let y2 = Math.max(selected[1], current[player][1]);
|
|
let pos = [current[player][0], y1, current[player][0], y2];
|
|
let type = 5 - size;
|
|
if (!hasCollision(pos) && isAvailable(type)) {
|
|
to_add = { "type": type, "position": pos };
|
|
}
|
|
}
|
|
if (selected[1] == current[player][1]) {
|
|
let size = Math.abs(selected[0] - current[player][0]) + 1;
|
|
if (size < 2 || size > 5 ) return;
|
|
let x1 = Math.min(selected[0], current[player][0]);
|
|
let x2 = Math.max(selected[0], current[player][0]);
|
|
let pos = [x1, current[player][1], x2, current[player][1]];
|
|
let type = 5 - size;
|
|
if (!hasCollision(pos) && isAvailable(type)) {
|
|
to_add = { "type": type, "position": pos };
|
|
}
|
|
}
|
|
}
|
|
|
|
function addPlacement(descriptor) {
|
|
placements[player].push(descriptor);
|
|
placements[player].sort((l, r) => l.type - r.type);
|
|
}
|
|
|
|
function drawShipPlacements() {
|
|
if (to_rem) {
|
|
drawFields.apply(null, to_rem.position);
|
|
}
|
|
placements[player].forEach(
|
|
p => drawShip.apply(null, p.position.concat([SHIP_COLOR_PLACED])));
|
|
if (to_add) {
|
|
drawShip.apply(null, to_add.position.concat([SHIP_COLOR_PLACED]));
|
|
}
|
|
}
|
|
|
|
function drawShipIndicator() {
|
|
let p = to_add
|
|
? placements[player].concat(to_add).sort((l, r) => l.type - r.type)
|
|
: placements[player];
|
|
let pi = 0;
|
|
INDICATORS.forEach(indicator => {
|
|
let color = SHIP_COLOR_AVAIL;
|
|
if (pi < p.length && p[pi].type == indicator.type) {
|
|
pi += 1;
|
|
color = SHIP_COLOR_PLACED;
|
|
}
|
|
drawShip.apply(null, indicator.position.concat(color));
|
|
});
|
|
}
|
|
|
|
function drawHeading(text) {
|
|
g.clearRect(0, 20, 100, 32);
|
|
g.setColor(HEADING_COLOR[player]);
|
|
g.setFont('4x6', 2.8);
|
|
g.drawString(text, MARGIN_LEFT, 20);
|
|
}
|
|
|
|
function reset() {
|
|
g.clear();
|
|
drawHeading('Player ' + (player + 1));
|
|
drawFields(0, 0, 9, 9);
|
|
}
|
|
|
|
function showResults() {
|
|
let text1 = 'Player ' + (player + 1) + ' won!';
|
|
let text2 = 'Congratulations!';
|
|
g.clear();
|
|
g.clearRect(0, 20, 100, 32);
|
|
g.setColor(HEADING_COLOR[player]);
|
|
g.setFont('Vector', 20);
|
|
g.drawString(text1, MARGIN_LEFT, 80);
|
|
g.drawString(text2, MARGIN_LEFT, 120);
|
|
}
|
|
|
|
function moveSelection(dx, dy) {
|
|
let x = current[player][0];
|
|
let y = current[player][1];
|
|
drawField(x, y, false);
|
|
current[player][0] = x = (x + dx + FIELD_COUNT_X)%FIELD_COUNT_X;
|
|
current[player][1] = y = (y + dy + FIELD_COUNT_Y)%FIELD_COUNT_Y;
|
|
drawField(x, y, true);
|
|
}
|
|
|
|
behaviours.push({
|
|
"move": (dx, dy) => {
|
|
moveSelection(dx, dy);
|
|
determineChanges();
|
|
drawShipPlacements();
|
|
drawShipIndicator();
|
|
},
|
|
"action": _ => {
|
|
if (to_add) {
|
|
addPlacement(to_add);
|
|
to_add = null;
|
|
selected = [-10, -10];
|
|
if (placements[player].length == 10) {
|
|
behaviours[phase].transition();
|
|
}
|
|
} else {
|
|
selected = [current[player][0], current[player][1]];
|
|
}
|
|
},
|
|
"transition": _ => {
|
|
current[0] = [0, 0];
|
|
player = 1;
|
|
phase = 1;
|
|
reset();
|
|
drawShipIndicator();
|
|
}
|
|
});
|
|
|
|
behaviours.push({
|
|
"move": behaviours[0].move,
|
|
"action": behaviours[0].action,
|
|
"transition": _ => {
|
|
current[1] = [0, 0];
|
|
player = 0;
|
|
phase = 2;
|
|
updateFieldStates();
|
|
reset();
|
|
}
|
|
});
|
|
|
|
behaviours.push({
|
|
"move": (dx, dy) => moveSelection(dx, dy),
|
|
"action": _ => {
|
|
let x = current[player][0];
|
|
let y = current[player][1];
|
|
let field_state = getFieldState(x, y);
|
|
if (field_state > 1) return;
|
|
setFieldState(x, y, field_state + 2);
|
|
drawField(x, y, true);
|
|
Bangle.buzz(200 + field_state*800, 0.5 + field_state*0.5);
|
|
if (getHitCount() < FULL_HITS) {
|
|
player = (player + 1)%2;
|
|
setTimeout(reset, 1000);
|
|
} else {
|
|
setTimeout(behaviours[phase].transition, 1000);
|
|
}
|
|
},
|
|
"transition": _ => {
|
|
phase = 3;
|
|
showResults();
|
|
}
|
|
});
|
|
|
|
behaviours.push({
|
|
"move": _ => {},
|
|
"action": _ => {}
|
|
});
|
|
|
|
reset();
|
|
drawShipIndicator();
|
|
|
|
setWatch(_ => behaviours[phase].move(0, -1), BTN1, {repeat: true, debounce: 100});
|
|
setWatch(_ => behaviours[phase].move(0, 1), BTN3, {repeat: true, debounce: 100});
|
|
setWatch(_ => behaviours[phase].move(-1, 0), BTN4, {repeat: true, debounce: 100});
|
|
setWatch(_ => behaviours[phase].move(1, 0), BTN5, {repeat: true, debounce: 100});
|
|
setWatch(_ => behaviours[phase].action(), BTN2, {repeat: true, debounce: 100});
|