Adding first version of app battleship

master
capybara1 2020-07-01 00:03:29 +02:00
parent 29ab659f99
commit c73a77ce6c
7 changed files with 492 additions and 0 deletions

View File

@ -2992,5 +2992,26 @@
{"name":"gbmusic.json"}, {"name":"gbmusic.json"},
{"name":"gbmusic.load.json"} {"name":"gbmusic.load.json"}
] ]
},
{
"id": "battleship",
"name":"Battleship",
"icon":"battleship-icon.png",
"version": "0.01",
"readme": "README.md",
"description": "The classic game of battleship",
"tags": "game",
"allow_emulator": true,
"storage": [
{
"name": "battleship.app.js",
"url": "battleship.js"
},
{
"name": "battleship.img",
"url": "battleship-icon.js",
"evaluate": true
}
]
} }
] ]

View File

@ -0,0 +1 @@
0.01: New App!

18
apps/battleship/README.md Normal file
View File

@ -0,0 +1,18 @@
# Battleship
The classic game of battleship.
## Usage
In the beginning, each player is required to place
all ships in his fleet on the field.
Navigation of the cursor is performed using BTN1 and
BTN3 as well as left and right on the touch screen.
To place a ship use BTN2 to initialize a placement
and BTN2 again to complete it.
In the next phase the players take alternating turns
in trying to hit an opposing ship.
After a player succeeds in sinking the entire opposing
fleet the game ends.

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwyBC/AH4A/AH4A/AH4A/AH4AEgeGAIVmAogBHBoRV/LZQBaLf4BK9EMlMMpQBClIJBMf5dM08Utcdh0luABNCIMUpYZBMO5bK1hZPAJYdBMecDswxFhkqktQLrYBEqAlBMI1mXdq5dYpxhqFYsc5pdnAIM16VeuQ1FLs67pAIM9+WP7GHrFN2JhjEYsMlRdtv9Yu34v8YlnNHolmL8GnktQLtkXt3XuwBB/ADBhfIYLq9FimsLtd2+9m21u25hFouQIIq9eLtVu+1eu1myxhIYLi9GtZdps3Wq11u83w94t3WMIZfDmsOL78dhxdo2tOmhZBy/5AIILCYYhfBr0TIopfUswZDLsc+iRRBr2Vp0UL4X2L4teL4JhEAIMD05fYPIfoLstWuk9+dGiZhDu83w+Ys3Wr11MI8UlJfbhkqLsdOqk16U96ZhHAINWqoBBMI8kxZfcpRddmvRLoNGmct2M12RhLqxhKlmsL/ZhDkuRloBBL4JhRL4JhCkhfdlJffAIhhajmLL7cD9BfkuBfCMJlGMJEUhJfcwxflMLFUgenL7sdhxhpnvxp0RnvSMIXzMI89yJFFL7MUpZfnmuxq0xAIbDEMI0k1hdXMJGnL9HRL5BhD+RhBovzHoJfcszBE1hhnovxp0RYoMtyJhG+ck9qhEL7DBIqBhnAIlxMInSNIK9dL5GGhkqLMstyEt2BhJilKXr5hJimsLsdGiABBnvwMIsc5hdjMJXNL7812BfDopfEFoJdnL4VmYc2QXYJdBMoJdC1gxFL8phJhkqktQYr4hBEoJdtMJcD07FdXIWnLuJjGG4xjCpcc9xZPCIMMpZb5YpwBF9EMlMMpQBClIJBC5hd2YpwBWLfZldKf4A/AH4A/AH4A/AH4A/AAo"))

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,130 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="battleship-icon.svg"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
id="svg8"
version="1.1"
viewBox="0 0 12.7 12.7"
height="48"
width="48"
inkscape:export-filename="battleship-icon.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs2">
<linearGradient
id="linearGradient848"
inkscape:collect="always">
<stop
id="stop844"
offset="0"
style="stop-color:#88e7cd;stop-opacity:1;" />
<stop
id="stop846"
offset="1"
style="stop-color:#88e7cd;stop-opacity:0;" />
</linearGradient>
<linearGradient
gradientUnits="userSpaceOnUse"
y2="6.3499999"
x2="6.3499999"
y1="3.0260465"
x1="9.7076998"
id="linearGradient850"
xlink:href="#linearGradient848"
inkscape:collect="always" />
</defs>
<sodipodi:namedview
inkscape:window-maximized="1"
inkscape:window-y="-9"
inkscape:window-x="-9"
inkscape:window-height="1013"
inkscape:window-width="1920"
units="px"
showgrid="false"
inkscape:document-rotation="0"
inkscape:current-layer="layer1"
inkscape:document-units="px"
inkscape:cy="50.37804"
inkscape:cx="120.0705"
inkscape:zoom="3.959798"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
borderopacity="1.0"
bordercolor="#666666"
pagecolor="#ffffff"
id="base" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
inkscape:groupmode="layer"
inkscape:label="Layer 1">
<path
id="path833"
d="M 11.074702,6.3499999 A 4.7247024,4.7247024 0 0 1 6.3499999,11.074702 4.7247024,4.7247024 0 0 1 1.6252975,6.3499999 4.7247024,4.7247024 0 0 1 6.3499999,1.6252975 4.7247024,4.7247024 0 0 1 11.074702,6.3499999 Z"
style="fill:#23ae87;fill-opacity:1;stroke:none;stroke-width:0.765;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
d="M 9.7076995,3.0260466 A 4.7247024,4.7247024 0 0 1 9.8531236,9.5203211 L 6.3499999,6.3499999 Z"
sodipodi:arc-type="slice"
sodipodi:end="0.73556977"
sodipodi:start="5.5028377"
sodipodi:ry="4.7247024"
sodipodi:rx="4.7247024"
sodipodi:cy="6.3499999"
sodipodi:cx="6.3499999"
sodipodi:type="arc"
style="fill:url(#linearGradient850);fill-opacity:1;stroke:none;stroke-width:0.765;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="circle835" />
<circle
style="fill:none;stroke-width:0.765;stroke-linecap:round;stroke-linejoin:round;stroke:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;fill-opacity:1"
id="circle837"
cx="6.3499999"
cy="6.3499999"
r="4.7247024" />
<path
id="path852"
d="M 6.3499999,6.3499999 9.7076994,3.0260467"
style="fill:#00ff00;stroke:#93ffc9;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
style="fill:none;fill-opacity:1;stroke:#007033;stroke-width:0.765;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 11.074702,6.3499999 A 4.7247024,4.7247024 0 0 1 6.3499999,11.074702 4.7247024,4.7247024 0 0 1 1.6252975,6.3499999 4.7247024,4.7247024 0 0 1 6.3499999,1.6252975 4.7247024,4.7247024 0 0 1 11.074702,6.3499999 Z"
id="path842" />
<circle
r="0.46234927"
cy="5.3087621"
cx="8.8007536"
id="path839"
style="fill:#93ffc9;fill-opacity:1;stroke:none;stroke-width:0.38708;stroke-linecap:round;stroke-linejoin:round" />
<circle
style="fill:#57c78f;fill-opacity:1;stroke:none;stroke-width:0.38708;stroke-linecap:round;stroke-linejoin:round"
id="circle841"
cx="8.2715864"
cy="7.9545956"
r="0.46234927" />
<circle
r="0.46234927"
cy="9.3416424"
cx="8.2715864"
id="circle843"
style="fill:#43c082;fill-opacity:1;stroke:none;stroke-width:0.38708;stroke-linecap:round;stroke-linejoin:round" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -0,0 +1,321 @@
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});