From c73a77ce6cac8625e826cac3114f8d51c227944e Mon Sep 17 00:00:00 2001 From: capybara1 <39344016+capybara1@users.noreply.github.com> Date: Wed, 1 Jul 2020 00:03:29 +0200 Subject: [PATCH] Adding first version of app battleship --- apps.json | 21 ++ apps/battleship/ChangeLog | 1 + apps/battleship/README.md | 18 ++ apps/battleship/battleship-icon.js | 1 + apps/battleship/battleship-icon.png | Bin 0 -> 2024 bytes apps/battleship/battleship-icon.svg | 130 +++++++++++ apps/battleship/battleship.js | 321 ++++++++++++++++++++++++++++ 7 files changed, 492 insertions(+) create mode 100644 apps/battleship/ChangeLog create mode 100644 apps/battleship/README.md create mode 100644 apps/battleship/battleship-icon.js create mode 100644 apps/battleship/battleship-icon.png create mode 100644 apps/battleship/battleship-icon.svg create mode 100644 apps/battleship/battleship.js diff --git a/apps.json b/apps.json index 65d30658c..f3db4bd05 100644 --- a/apps.json +++ b/apps.json @@ -2992,5 +2992,26 @@ {"name":"gbmusic.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 + } + ] } ] diff --git a/apps/battleship/ChangeLog b/apps/battleship/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/battleship/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/battleship/README.md b/apps/battleship/README.md new file mode 100644 index 000000000..765692d77 --- /dev/null +++ b/apps/battleship/README.md @@ -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. diff --git a/apps/battleship/battleship-icon.js b/apps/battleship/battleship-icon.js new file mode 100644 index 000000000..0878a4b28 --- /dev/null +++ b/apps/battleship/battleship-icon.js @@ -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")) diff --git a/apps/battleship/battleship-icon.png b/apps/battleship/battleship-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..514492d2f997c4c9590bb612bb548fcd4489a690 GIT binary patch literal 2024 zcmVP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H12X#qA zK~!jg?V0OuTt^wke{;^>y!P6TlZ|V;*h!1hCTY_owsYs!Nb~}&NNFk|@ej}nMWKR7 zP@xrHN`**`kn#^8p^{dJBFd)*B2`gB61%BLaZOq$w&P2DS$lV#y`3{Nd^mf&tG(H4 zFMJ`-ht=%Nyze~Ed(Oh3Xu!Tf<94BdfA5CBOM!sG-(9aKwkjPGcYu~(V^EbAcDct{_lYJ32<hDMBbD;Z*n+(%~HyFYb2x5AC07yHFol0S);wWjC^Y{B4+IbgcgrZ+&Sy_?2x7yhO;gedP;YfUB*<^J3xiP>?al`d?yK}2Z1{$?%n zWbFb@A8tTr-U0DqA@R0ty9l-SAPlpbbqTbRBxddq9UCqd@+VqD-y?D{UCFi(YZGw# za04p)0fKM1RiXB6Z29t27^b%-;>A$5#rzjHNZcFCS8D#wrqJ_i5wUvMA{eCG-$n2& zfDnT4j$Ycf?XEfM}Eh!A>$AS*}cfRL!os0B5!N9bf>2cl7>O$cI~+IQf-c zS^}Lk_?o#jdNbD|zQ&QRzi@eKrjo5u(Q5?5St=coq4sSwKeWBB!~w!BAznVVpTweM zIbpHyiNoyKb--=*AoV$_%+<;Q7W+=P>%8gnv*ppJ>d6~$$ba|PUgl>r#9|q2Wn)Rj zH@|v<#)gp7NpOf$eJ@n5>G>Hgj1(gSCc>ZvRFt*s$mI=Ywfi3AHvDQsKj$SZ7RV=KuMI}g$r z%smAyj&Q2)v9$}(`lMS7wRhKLxhy1q;>cdU7>hEylq4Ju@}-AEc)hHMkV=9O?AozE zZz%LDYZrii+ATGPyXsE(7Zc??51uh5DJ#nx~q-dsWmqMt}9 z-%6Ow4LQ=jK*kJhX=^H^;a;x3m)BiP<<4GclIpX+~}@Af+r2VIvW2 zY7M(?f-LqvTAX8J3H-GVLK|%lo`9KfCzrCzem&KM zc(NcPmNpm_VrB@CrPY~irhuDD6t|apGps2$$FtJ+YhzmlwvxHPmSLHTxE7^PArX%j zaP!#`C7SV~g%+uhDFDH=y9MLfRX{!McBT8V&i`+x$j z11M>eNkmJ!Yy?(rr)~91!VMBb*v_&l6}GmKxtz$7lKHv2*tX@GUZsrs3Olj*n_Id+ zH(6H!03jbd)M(J+hmOWPVJTH^k8tYlj^D}~X)cuWRY8FIgIh|>jiGg257RIT-V_1( zs4Hg!vy{ZLvP_PDmhWhq=gaykJHo)wFq$g>GMgbWeY<|_frXT0HffW{D(=MWeAJ~} zNvX&0d`dQxbWJXC_UgaaE&xFAmRpRD4Wq1V-2`Y2<1w3&g)B>H8_Tu|UL8dylVogU zu+ZeqO1>%!7#MsX=fn(YrI{c7q+a|1@Ov~(K7|kNTpIn^sSD zu1rsJxI=;$+!KZ(Z|z#rPQUjEn$8ax_9z+&1YV+&$apEGxB(3v0(-eX|!^lH9V z7jO~HbC11GDti>cc7SAb8auU2LrWN8)}FCyEl6YxVri39+G2KOkfpmL`3^!{K*nC? zYIN0PZ0$KhhLvX0LY2|{$AH?)JXGzksu+#U+rV?mbCL4uI80t07eq8ffy^%Ci zaH{VG40Q%Aj;zziCA9t}zZv>~|LGM|F<$R|9LY)N=tJ~F?sl>iN#H7)3z+8lO6fr@ zsQ)!92wvaUjTRkf(>WCoav7ng`Sr)+tTWON@Bj}`9{&NngfDB@zYJCY0000B literal 0 HcmV?d00001 diff --git a/apps/battleship/battleship-icon.svg b/apps/battleship/battleship-icon.svg new file mode 100644 index 000000000..bd23abf25 --- /dev/null +++ b/apps/battleship/battleship-icon.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/apps/battleship/battleship.js b/apps/battleship/battleship.js new file mode 100644 index 000000000..3661ef494 --- /dev/null +++ b/apps/battleship/battleship.js @@ -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});