From f1c923d4b52187856bf2dce4fe6de640ae990246 Mon Sep 17 00:00:00 2001 From: "Dirk Hillbrecht (home)" Date: Wed, 29 Dec 2021 07:30:47 +0100 Subject: [PATCH 1/6] Puzzle15: First playable version of the 15-puzzle --- apps/puzzle15/ChangeLog | 1 + apps/puzzle15/README.md | 18 ++ apps/puzzle15/puzzle15.app-icon.js | 1 + apps/puzzle15/puzzle15.app.js | 361 +++++++++++++++++++++++++++++ apps/puzzle15/puzzle15.app.png | Bin 0 -> 2313 bytes apps/puzzle15/screenshot.png | Bin 0 -> 1161 bytes 6 files changed, 381 insertions(+) create mode 100644 apps/puzzle15/ChangeLog create mode 100644 apps/puzzle15/README.md create mode 100644 apps/puzzle15/puzzle15.app-icon.js create mode 100644 apps/puzzle15/puzzle15.app.js create mode 100644 apps/puzzle15/puzzle15.app.png create mode 100644 apps/puzzle15/screenshot.png diff --git a/apps/puzzle15/ChangeLog b/apps/puzzle15/ChangeLog new file mode 100644 index 000000000..2f5d93cad --- /dev/null +++ b/apps/puzzle15/ChangeLog @@ -0,0 +1 @@ +0.01: Initial version, UI mechanics ready, no real game play so far diff --git a/apps/puzzle15/README.md b/apps/puzzle15/README.md new file mode 100644 index 000000000..540646f97 --- /dev/null +++ b/apps/puzzle15/README.md @@ -0,0 +1,18 @@ +# Puzzle15 - A 15-puzzle for the Bangle.js 2 + +Puzzle15 implements a 15-puzzle on the screen of the Bangle.js 2 smart watch. +A "15-puzzle" is a single-player game. 15 "stones" are numbered from 1 to 15 and placed randomly on a 4 by 4 playing field. +So, one place on the field is free. +The target of the game is to move the stones around (using the "gap" on the playing field) to bring them in natural order. + +_This is work in progress!_ + +## How to play + +After starting the game, the playing field is shown with the stones in the corrct order. +Push the stones around with drag gestures (left to right, right to left, top to bottom, bottom to top) to shuffle them. +Then, use the same gestures to restore the order. + +So far, there are no other capabililties or options. + +Have fun! diff --git a/apps/puzzle15/puzzle15.app-icon.js b/apps/puzzle15/puzzle15.app-icon.js new file mode 100644 index 000000000..04fb4a665 --- /dev/null +++ b/apps/puzzle15/puzzle15.app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgn/AC3+7oAD7e7AAW8BQndBQe79/9DomgHocH74KD/RJE34Xax4XDtvoC4fJ54XDluAC4f2z4XDzm/C4ett4XD34OBF4e/I4m+C4f8r4XChHuC5U98oXEF4cP7/AC5O9mYXC/2/F4cGtwvE/SsBC4Ws7gvD7YCBL4ULO4i/u1QAD7QED1e6AoetCAnf/YeE1wpD/lgBQcKIAgXG14LD/twC5kL3Z+BC4P+LgIXBg272wXD7wXEh7eCC4PWzIXChHtOoIXB/WX54XDh3KmAXC1oLBI4UD+AXC+/rdIIvD5wvD3O4C4cJ4AXC/dUI4kJhgMBC4Ov+AXDh9QC4X2/gvEhvvoAXC81dC4duR4f8wSncC6v8u4AD3ndAAXcy4KDtYKD7vf/oGE2wRDvPNBQfLFAnP/o2EVIIACg7yBAATZBAAe/C7P9g4XCx+wn/6C4Op//AC4MK+cI/+QC4X2/fPC4PM2HKh8H7vpewIXBhvThV5+AXC+/5C4UL2HHC4Pf/P/AIJHB6cAj2wC4X+3AXPhADBF4fX94XB1va1vOC4PXAIX6hfrxvb0CPD7p3C1e6hW2C4LOBAIIXB3eJ3YXEX78GM4IAC9QXG1QAD7QEDJYIFD14oE//7DwgME/twBQcPC70G6EG5dQ1/8VYPtC4ObgfM5IXHr/whvO4Gvy6LBtX9vfugnr3AXHkXggGOC4P97/43X9ukOgnv6BfIC4Oe2AXC6+nI4MOgfI9QXJhssF4f91AXCgnA9IXHr3u1HusGv3Ob//s/t693l3xHJX9v+3YAD7oAE5YKD34XFAC4=")) diff --git a/apps/puzzle15/puzzle15.app.js b/apps/puzzle15/puzzle15.app.js new file mode 100644 index 000000000..9a5864b9a --- /dev/null +++ b/apps/puzzle15/puzzle15.app.js @@ -0,0 +1,361 @@ +// A 15-puzzle game for the Bangle.js 2 clock +// (C) Dirk Hillbrecht 2022 +// Released unter the terms of the MIT license + +// *** Global settings +// Note: These could be changed by settings later... + +// Minimum number of pixels to interpret it as drag gesture +const dragThreshold = 10; + +// Number of steps in button move animation +const animationSteps = 5; + +// Milliseconds to wait between move animation steps +const animationWaitMillis = 70; + +// Size of the playing field +const buttonsPerLine = 4; + +// *** Global settings derived by device characteristics + +// Total width of the playing field (full screen width) +const fieldw = g.getWidth(); + +// Total height of the playing field (screen height minus widget zones) +const fieldh = g.getHeight() - 48; + +// Size of one button +const buttonsize = Math.floor(Math.min(fieldw / (buttonsPerLine + 1), fieldh / buttonsPerLine)) - 2; + +// Actual left start of the playing field (so that it is centered) +const leftstart = (fieldw - ((buttonsPerLine + 1) * buttonsize + 8)) / 2; + +// Actual top start of the playing field (so that it is centered) +const topstart = 24 + ((fieldh - (buttonsPerLine * buttonsize + 6)) / 2); + +// *** Low level helper classes + +// One node of a first-in-first-out storage +class FifoNode { + constructor(payload) { + this.payload = payload; + this.next = null; + } +} + +// Simple first-in-first-out (fifo) storage +// Needed to keep the stone movements in order +class Fifo { + // Initialize an empty Fifo + constructor() { + this.first = null; + this.last = null; + } + // Add an element to the end of the internal fifo queue + add(payload) { + if (this.last === null) { // queue is empty + this.first = new FifoNode(payload); + this.last = this.first; + } else { + let newlast = new FifoNode(payload); + this.last.next = newlast; + this.last = newlast; + } + } + // Returns the first element in the queue, null if it is empty + remove() { + if (this.first === null) + return null; + oldfirst = this.first; + this.first = this.first.next; + if (this.first === null) + this.last = null; + return oldfirst.payload; + } + // Returns if the fifo is empty, i.e. it does not hold any elements + isEmpty() { + return (this.first === null); + } +} + +// Helper class to keep track of tasks +// Executes tasks given by addTask. +// Tasks must call Worker.endTask() when they are finished, for this they get the worker passed as parameter. +// If a task is given with addTask() while another task is still running, +// it is queued and executed once the currently running task and all +// previously scheduled tasks have finished. +// Tasks must be functions with the Worker as first and only parameter. +class Worker { + // Create an empty worker + constructor() { + this.tasks = new Fifo(); + this.busy = false; + } + // Add a task to the worker + addTask(task) { + if (this.busy) // other task is running: Queue this task + this.tasks.add(task); + else { // No other task is running: Execute directly + this.busy = true; + task(this); + } + } + // Called by the task once it finished + endTask() { + if (this.tasks.isEmpty()) // No more tasks queued: Become idle + this.busy = false; + else // Call the next task immediately + this.tasks.remove()(this); + } +} + +// *** Mid-level game mechanics + +// Representation of a position where a stone is set. +// Stones can be moved from field to field. +// The playing field consists of a fixed set of fields forming a square. +// During an animation, a series of interim field instances is generated +// which represents the locations of a stone during the animation. +class Field { + // Generate a field with a left and a top coordinate. + // Note that these coordinates are "cooked", i.e. they contain all offsets + // needed place the elements globally correct on the screen + constructor(left, top) { + this.left = left; + this.top = top; + this.centerx = (left + buttonsize / 2) + 1; + this.centery = (top + buttonsize / 2) + 2; + } + // Generate a field for the given playing field index. + // Playing field indexes start at top left with "0" + // and go from left to right line by line from top to bottom. + static forIndex(index) { + return new Field(leftstart + (index % buttonsPerLine) * (buttonsize + 2), + topstart + (Math.floor(index / buttonsPerLine)) * (buttonsize + 2)); + } +} + +// Representation of a moveable stone of the game. +// Stones are moved from field to field to solve the puzzle +// Stones are numbered from 0 to the maximum number ot stones. +// Stone "0" represents the gap on the playing field. +// The main knowledge of a Stone instance is how to draw itself. +class Stone { + // Create stone with the given number + // The constructor creates the "draw()" function which is used to draw the stone + constructor(number, targetindex) { + this.number = number; + this.targetindex = targetindex; + // gap: Does not draw anything + if (number === 0) + this.draw = function(field) {}; + else if ((number + (buttonsPerLine % 2 == 0 ? (Math.floor((number - 1) / buttonsPerLine)) : 0)) % 2 == 0) { + // Black stone + this.draw = function(field) { + g.setFont("Vector", 20).setFontAlign(0, 0).setColor(0, 0, 0); + g.fillRect(field.left, field.top, field.left + buttonsize, field.top + buttonsize); + g.setColor(1, 1, 1).drawString(number, field.centerx, field.centery); + }; + } else { + // White stone + this.draw = function(field) { + g.setFont("Vector", 20).setFontAlign(0, 0).setColor(0, 0, 0); + g.drawRect(field.left, field.top, field.left + buttonsize, field.top + buttonsize); + g.drawString(number, field.centerx, field.centery); + }; + } + } + // Returns whether this stone is on its target index + isOnTarget(index) { + return index === this.targetindex; + } +} + +// Helper class which knows how to clear the rectangle opened up by the two given fields +class Clearer { + // Create a clearer for the area between the two given fields + constructor(startfield, endfield) { + this.minleft = Math.min(startfield.left, endfield.left); + this.mintop = Math.min(startfield.top, endfield.top); + this.maxleft = Math.max(startfield.left, endfield.left); + this.maxtop = Math.max(startfield.top, endfield.top); + } + // Clear the area defined by this clearer + clearArea() { + g.setColor(1, 1, 1); + g.fillRect(this.minleft, this.mintop, + this.maxleft + buttonsize, this.maxtop + buttonsize); + } +} + +// Helper class which moves a stone between two fields +class Mover extends Clearer { + // Create a mover which moves the given stone from startfield to endfield + // and animate the move in the given number of steps + constructor(stone, startfield, endfield, steps) { + super(startfield, endfield); + this.stone = stone; + this.startfield = startfield; + this.endfield = endfield; + this.steps = steps; + } + // Create the coordinate between start and end for the given step + // Computation uses sinus for a smooth movement + stepCoo(start, end, step) { + return start + ((end - start) * ((1 + Math.sin((step / this.steps) * Math.PI - (Math.PI / 2))) / 2)); + } + // Compute the interim field for the stone to place during the animation + stepField(step) { + return new Field( + (this.minleft === this.maxleft ? this.minleft : + this.stepCoo(this.startfield.left, this.endfield.left, step)), + (this.mintop === this.maxtop ? this.mintop : + this.stepCoo(this.startfield.top, this.endfield.top, step))); + } + // Perform one animation step + animateStep(step, worker) { + this.clearArea(); + this.stone.draw(this.stepField(step)); + if (step < this.steps) // still steps left: Issue next step + setTimeout(function(t) { + t.animateStep(step + 1, worker); + }, animationWaitMillis, this); + else // all steps done: Inform the worker + worker.endTask(); + } + // Start the animation, this method is called by the worker + animate(worker) { + this.animateStep(1, worker); + } +} + +// Representation of the playing field +// Knows to draw the field and to move a stone into a gap +// TODO: More game mechanics (shuffling, solving,...) +class Board { + // Generates the actual playing field with all fields and buttons + constructor() { + this.fields = []; + this.buttons = []; + for (i = 0; i < (buttonsPerLine * buttonsPerLine); i++) { + this.fields[i] = Field.forIndex(i); + this.buttons[i] = new Stone((i + 1) % (buttonsPerLine * buttonsPerLine),i); + } + } + // Draws the complete playing field + draw() { + new Clearer(this.fields[0], this.fields[this.fields.length - 1]).clearArea(); + for (i = 0; i < this.fields.length; i++) + this.buttons[i].draw(this.fields[i]); + } + // returns the index of the field left of the field with the given index, + // -1 if there is none (index indicates already a leftmost field on the board) + leftOf(index) { + return (index % buttonsPerLine === 0 ? -1 : index - 1); + } + rightOf(index) { + return (index % buttonsPerLine === (buttonsPerLine - 1) ? -1 : index + 1); + } + topOf(index) { + return (index >= buttonsPerLine ? index - buttonsPerLine : -1); + } + bottomOf(index) { + return (index < (buttonsPerLine - 1) * buttonsPerLine ? index + buttonsPerLine : -1); + } + // Return the index of the gap in the field, -1 if there is none (should never happel) + indexOf0() { + for (i = 0; i < this.buttons.length; i++) + if (this.buttons[i].number === 0) + return i; + return -1; + } + // Moves the stone at the field with the index found by the startfunc operation + // into the gap field. + moveTo0(startfunc, animator) { + let endidx = this.indexOf0(); // Target field (the gap) + if (endidx === -1) { + animator.endTask(); + return; + } + let startidx = startfunc(endidx); // Start field (relative to the gap) + if (startidx === -1) { + animator.endTask(); + return; + } + let moved = this.buttons[startidx]; + this.buttons[startidx] = this.buttons[endidx]; + this.buttons[endidx] = moved; + new Mover(moved, this.fields[startidx], this.fields[endidx], animationSteps).animate(animator); + } + // Move the stone right fro the gap into the gap + moveRight(animator) { + this.moveTo0(this.leftOf, animator); + } + moveLeft(animator) { + this.moveTo0(this.rightOf, animator); + } + moveUp(animator) { + this.moveTo0(this.bottomOf, animator); + } + moveDown(animator) { + this.moveTo0(this.topOf, animator); + } + // Check if the board is solved (all stones at the right position) + isSolved() { + for (i = 0; i < this.buttons.length; i++) + if (!this.buttons[i].isOnTarget(i)) + return false; + return true; + } +} + +// *** Main program + +// We need a worker... +var worker = new Worker(); +// ...and the board +var board = new Board(); + +// UI: Accumulation of current drag operation +var currentdrag = { + x: 0, + y: 0 +}; + +// Handle a drag event +function handledrag(e) { + if (e.b === 0) { // Drag event ended: Evaluate drag and start move operation + if (Math.abs(currentdrag.x) > Math.abs(currentdrag.y)) { // Horizontal drag + if (currentdrag.x > dragThreshold) + worker.addTask(e => board.moveRight(e)); + else if (currentdrag.x < -dragThreshold) + worker.addTask(e => board.moveLeft(e)); + } else { // Vertical drag + if (currentdrag.y > dragThreshold) + worker.addTask(e => board.moveDown(e)); + else if (currentdrag.y < -dragThreshold) + worker.addTask(e => board.moveUp(e)); + } + currentdrag.x = 0; // Clear the drag accumulator + currentdrag.y = 0; + } else { // Drag still running: Accumulate drag shifts + currentdrag.x += e.dx; + currentdrag.y += e.dy; + } +} + +// Clear the screen once, at startup +g.clear(); +// Drop mode as this is a game +Bangle.setUI(undefined); +// Load widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +// Draw the board initially +board.draw(); +// Start the interaction +Bangle.on("drag", handledrag); + +// end of file \ No newline at end of file diff --git a/apps/puzzle15/puzzle15.app.png b/apps/puzzle15/puzzle15.app.png new file mode 100644 index 0000000000000000000000000000000000000000..f95366a4616cb5f6fde4f00694a5aaa1e1c7194c GIT binary patch literal 2313 zcmV+k3HJ7hP)-&vx#;&$;)U&*z-;j6(<^_z#O20QC3wL#b3! z4IqTz-Me?-a5${GHa0e}wY7zrnHj6DTCE0`%SC^GKh<-rT)7g~SHLp^sBqEWBdhXiVT0#gR+`q*Ye%6!q8o)3NE?l?(jYh*d!NG$E zAr_0#)6;|W^mJDJ#>PhE<>leQg9lU(=0uC6X*XJ^wJaO~JIaJgKnUGeep0mCrp z?(Rl%a!l;XCG#aF(rD1e*6!!M^aB*=l45RuWO{39hp^eKmY-e_M zHmt0y5EmE63a#(ozejU(Gq!EphMAceRznjL6T#tdkdcvL(B1SJP+3_COG``C*4AS6 z>eX1WVuis)1_uY>;NXDk*RO*Rf`EVkgSsv*E^v2uM@>x)PMkP_MT-_0H9#a10RY0n z!XTAO=?#dEj;3~@0|yS6+{|1mMz+uNxr zyrZLoDwS|_bOevb!}$0(tM2UVECd39L08dtlHT6l-%^R1ni^^fFDWS@0|NtuVHnn{ zxw)AnCMHtPee>o`qEe|?=T$0|q`keJ5JHF<0IJ>89vO!NYiny*TU)d0^7(v|T!YKy z!qU>xB$v}WdqqWs7Nez7DKauLj7~Nl$019 z)78}ldwY90J3G^(P*_-)Hm#kSnlie8L?S^+NeMhXJ#qj3eH=Y{)F_Pl`uc(p0(WaF05LH!U>Jr~|MlzFh>D6rNJt0<2M6h4)Y;h?DwPUBK|x4KNin*Jxw$#S zVlifBW{{VcM-@Zy@$u;F>|~9-z`#HxCnqB-D+?PpZlpIrBoblEmM!S(>w|}fhtWl* zr>BvWl!Tz5Af%?IQsw=^HYU~8)u^tnMs98{9zT9e9|IdUY{1*MZ{h0diqOzd zql*Lu1!2#gJ(!u9q2iK+gak}YO`)=~l2tc0HWsO=si>%^z@tZx4DJZ2t*teP&0H>5 z=UyZd5r$!?%EY#|HX;-Xsd=ERtc<#}ohyU6Gz|IIcO`6WY#W++zz{A6XYHqb!4LdtKbar-9ZQ2|Fa0nr!qoaeWeB^Sukjv$$s;Z)5SPu^m`1<<7 z(b17rSEW*6cz76h@7|@_$lbele?z9frLDp+%-_>i`PtO?XU6L_;Khp<;BYuNdGaKt zr>FG}Ja_IKlu9Ma%gX@(0RaIR8X5v21jWV0I(5M0$GJn!!S^(RLIH6(HW=LfXkOJV|I2Hn>KAiV`C!>aYb8On>OGwGc!?JTdQT- z?*sA8n>Pp!4%U9BR;w{EFo3I9uj-xK&(9AnEiKx(bar+|dU`qv3JQ>rkf0Npug6ZjLNF)--WHPk2 zwnCv$%&SM1ELnonr%xj_H5IY3vFPvbhmDO5ii?ZcbD>kGPNA=_4?>|(=jeZwmoHyx zKkwhaA4Nq)_~)O0aO>7By^X4bgan<~h>MFeNLo*xJkfrB`0ybd931GAIa^y>tXZ?h z=usynCFyPS>Juj?CpbAd%`3{km+#+CL`FuMmP)AAYJC3unU#&r%*;&6jnry2R4NtK z%M+ziX;O=QPM1RnAyTOn5{ZPm7uwU)L)GuGv$KOnqru3?2-O3Hhlhj5<54{apU=m_ zg$vo!=^rnVNJLaB6;*oB(a~X2Iz2KnLQIvPN+r~4HI?33}TSS;45o*Eh& zP+3`t9XodDecSRqnavgX`YN;B+*~a>tzEko&CShdYHC7LQxh&;y!h(|L`6lRu&_`E z_m3Po0v?YCUteDwK73e*E%Wp9ap%q*9n7Dep2qp}=kfgcbDc%1s;Uqk9u3UkIg#}7WOaDJH@MA90+S-booE*sIa=j%akH^!(-qNK@AruN> zWn~2`D=VYeZEkK3jYfmi)Ku{Kd`wPG{#s2u*G{}~;|9Kd{R){(hWh$?)Ya9Yxw#p$ zv$M#`%F>B}^73-+%ePmrUSZd+U2t@Cgt@u-ygp`eaWOV;-VBXK1G!v|#Kc4}3z jaPYVEz*Gb9AC`XspcQ$f%Ox5-00000NkvXXu0mjfxkpY+ literal 0 HcmV?d00001 diff --git a/apps/puzzle15/screenshot.png b/apps/puzzle15/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..3a79a3ae64687fcd1afdbc160b4e0a9f3a912169 GIT binary patch literal 1161 zcmV;41a|w0P)?f=M;sC?}hyxG@AP&exlP<$N%E0TG)@9*!JkQfr=dow!p-E4SrzozkYaiRU z?|;8;EfVSRT1HVoqOjtz?b~yo@Be8YS1**l|F>?DlQrteEPp^6w(eDhv6|Ni6;`58RP}1g!b3Jhp|@C8)k<%_XJ$TjgaPFf zQm-v@uRQ0Qy^`T3t>^HnN`5Vp_j#rep5xbf1!b ziaACW5lGGpo&yjEAPztrfH(kgK%!E1^>Y-QqxnA%w0(lxo`e1v^ylb?6>sPd62pEH zQlMG^!~uu{5C7~eJ{Bcnv`Z7K*q{39^Jx7ss@YFi0X@&*wX^8mAERftg~zh6 zzufJRk-~|^4@cA1rg4BYY_qjpYs{wk%k{|Aqa>u-w%@s_ii8x@JXq8Cl~^=vyA#$d z8@5F&oh58f-)b`AUWRoz)ynw@E6GKwc@z0mH$g5xjq6cEc#f+;tovqE?vLSZ|5VO# zPCPx_X_WpLRqfv;Rcq~DqxHw|Z1Ekkl%>sd%qK@stw8LA6c7g>4gkafhyz?p*=nUB z_MeV}{v2@IbI>1y{v6$~LVt|4?I$4x!~uu{5Cs;+jC@z`Ijc`aGWr?meG3o!!{23}?QvnAq6m|7TwUh*Pivw^UFo)M zfBt4CeDrhD3u|qE%b;wFFpymo8g_@TRnpgoHXhUX00000NkvXXu0mjfBDE|A literal 0 HcmV?d00001 From d5a4e7b93e6b5344e732f4a1de1f34a1dd853b99 Mon Sep 17 00:00:00 2001 From: "Dirk Hillbrecht (home)" Date: Tue, 4 Jan 2022 23:12:45 +0100 Subject: [PATCH 2/6] Puzzle15: v0.01, initial integration into the app loader --- apps.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/apps.json b/apps.json index 5cc311413..7aeae8749 100644 --- a/apps.json +++ b/apps.json @@ -5389,5 +5389,22 @@ "storage": [ {"name":"touchmenu.boot.js","url":"touchmenu.boot.js"} ] + }, + { + "id": "puzzle15", + "name": "15 puzzle", + "version": "0.01", + "description": "A 15 puzzle game with drag gesture interface", + "readme":"README.md", + "icon": "puzzle15.app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "app", + "tags": "game", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"puzzle15.app.js","url":"puzzle15.app.js"}, + {"name":"puzzle15.img","url":"puzzle15.app-icon.js","evaluate":true} + ] } ] From 43e87247d7803efafd3216cb77eb9a9f2cf0062b Mon Sep 17 00:00:00 2001 From: "Dirk Hillbrecht (home)" Date: Sat, 1 Jan 2022 19:03:35 +0100 Subject: [PATCH 3/6] Puzzle15: v0.02 with shuffled board init, menu-based control flow etc. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now it actually starts making sense… --- apps.json | 2 +- apps/puzzle15/ChangeLog | 1 + apps/puzzle15/puzzle15.app.js | 326 ++++++++++++++++++++++++++++------ 3 files changed, 274 insertions(+), 55 deletions(-) diff --git a/apps.json b/apps.json index 7aeae8749..4a3d8c7c8 100644 --- a/apps.json +++ b/apps.json @@ -5393,7 +5393,7 @@ { "id": "puzzle15", "name": "15 puzzle", - "version": "0.01", + "version": "0.02", "description": "A 15 puzzle game with drag gesture interface", "readme":"README.md", "icon": "puzzle15.app.png", diff --git a/apps/puzzle15/ChangeLog b/apps/puzzle15/ChangeLog index 2f5d93cad..bc5a4422b 100644 --- a/apps/puzzle15/ChangeLog +++ b/apps/puzzle15/ChangeLog @@ -1 +1,2 @@ 0.01: Initial version, UI mechanics ready, no real game play so far +0.02: Lots of enhancements, menu system not yet functional, but packaging should be now... diff --git a/apps/puzzle15/puzzle15.app.js b/apps/puzzle15/puzzle15.app.js index 9a5864b9a..801cdf759 100644 --- a/apps/puzzle15/puzzle15.app.js +++ b/apps/puzzle15/puzzle15.app.js @@ -8,14 +8,14 @@ // Minimum number of pixels to interpret it as drag gesture const dragThreshold = 10; +// Maximum number of pixels to interpret a click from a drag event series +const clickThreshold = 3; + // Number of steps in button move animation -const animationSteps = 5; +const animationSteps = 6; // Milliseconds to wait between move animation steps -const animationWaitMillis = 70; - -// Size of the playing field -const buttonsPerLine = 4; +const animationWaitMillis = 30; // *** Global settings derived by device characteristics @@ -25,14 +25,29 @@ const fieldw = g.getWidth(); // Total height of the playing field (screen height minus widget zones) const fieldh = g.getHeight() - 48; +// Size of the playing field +var buttonsPerLine; + // Size of one button -const buttonsize = Math.floor(Math.min(fieldw / (buttonsPerLine + 1), fieldh / buttonsPerLine)) - 2; +var buttonsize; // Actual left start of the playing field (so that it is centered) -const leftstart = (fieldw - ((buttonsPerLine + 1) * buttonsize + 8)) / 2; +var leftstart; // Actual top start of the playing field (so that it is centered) -const topstart = 24 + ((fieldh - (buttonsPerLine * buttonsize + 6)) / 2); +var topstart; + +// Number of buttons on the board (needed at several occasions) +var buttonsPerBoard; + +// Set the buttons per line globally and all derived values, too +function setButtonsPerLine(bPL) { + buttonsPerLine = bPL; + buttonsize = Math.floor(Math.min(fieldw / (buttonsPerLine + 1), fieldh / buttonsPerLine)) - 2; + leftstart = (fieldw - ((buttonsPerLine + 1) * buttonsize + 8)) / 2; + topstart = 24 + ((fieldh - (buttonsPerLine * buttonsize + 6)) / 2); + buttonsPerBoard = (buttonsPerLine * buttonsPerLine); +} // *** Low level helper classes @@ -67,7 +82,7 @@ class Fifo { remove() { if (this.first === null) return null; - oldfirst = this.first; + let oldfirst = this.first; this.first = this.first.next; if (this.first === null) this.last = null; @@ -110,6 +125,56 @@ class Worker { } } +// Evaluate "drag" events from the UI and call handlers for drags or clicks +// The UI sends a drag as a series of events indicating partial movements +// of the finger. +// This class combines such parts to a long drag from start to end +// If the drag is short, it is interpreted as click, +// otherwise as drag. +// The approprate method is called with the data of the drag. +class Dragger { + constructor(clickHandler, dragHandler, clickThreshold, dragThreshold) { + this.clickHandler = clickHandler; + this.dragHandler = dragHandler; + this.clickThreshold = (clickThreshold === undefined ? 3 : clickThreshold); + this.dragThreshold = (dragThreshold === undefined ? 10 : dragThreshold); + this.dx = 0; + this.dy = 0; + this.enabled = true; + } + // Enable or disable the Dragger + setEnabled(b) { + this.enabled = b; + } + // Handle a raw drag event from the UI + handleRawDrag(e) { + if (!this.enabled) + return; + this.dx += e.dx; // Always accumulate + this.dy += e.dy; + if (e.b === 0) { // Drag event ended: Evaluate full drag + if (Math.abs(this.dx) < this.clickThreshold && Math.abs(this.dy) < this.clickThreshold) + this.clickHandler({ + x: e.x - this.dx, + y: e.y - this.dy + }); // take x and y from the drag start + else if (Math.abs(this.dx) > this.dragThreshold || Math.abs(this.dy) > this.dragThreshold) + this.dragHandler({ + x: e.x - this.dx, + y: e.y - this.dy, + dx: this.dx, + dy: this.dy + }); + this.dx = 0; // Clear the drag accumulator + this.dy = 0; + } + } + // Attach the drag evaluator to the UI + attach() { + Bangle.on("drag", e => this.handleRawDrag(e)); + } +} + // *** Mid-level game mechanics // Representation of a position where a stone is set. @@ -127,6 +192,11 @@ class Field { this.centerx = (left + buttonsize / 2) + 1; this.centery = (top + buttonsize / 2) + 2; } + // Returns whether this field contains the given coordinate + contains(x, y) { + return (this.left < x && this.left + buttonsize > x && + this.top < y && this.top + buttonsize > y); + } // Generate a field for the given playing field index. // Playing field indexes start at top left with "0" // and go from left to right line by line from top to bottom. @@ -134,6 +204,16 @@ class Field { return new Field(leftstart + (index % buttonsPerLine) * (buttonsize + 2), topstart + (Math.floor(index / buttonsPerLine)) * (buttonsize + 2)); } + // Special field for the result "stone" + static forResult() { + return new Field(leftstart + (buttonsPerLine * (buttonsize + 2)), + topstart + ((buttonsPerLine - 1) * (buttonsize + 2))); + } + // Special field for the menu + static forMenu() { + return new Field(leftstart + (buttonsPerLine * (buttonsize + 2)), + topstart); + } } // Representation of a moveable stone of the game. @@ -217,11 +297,11 @@ class Mover extends Clearer { animateStep(step, worker) { this.clearArea(); this.stone.draw(this.stepField(step)); - if (step < this.steps) // still steps left: Issue next step + if (step < this.steps) // still steps left: Issue next step setTimeout(function(t) { t.animateStep(step + 1, worker); }, animationWaitMillis, this); - else // all steps done: Inform the worker + else // all steps done: Inform the worker worker.endTask(); } // Start the animation, this method is called by the worker @@ -232,22 +312,54 @@ class Mover extends Clearer { // Representation of the playing field // Knows to draw the field and to move a stone into a gap -// TODO: More game mechanics (shuffling, solving,...) +// TODO: More game mechanics (solving,...) class Board { // Generates the actual playing field with all fields and buttons constructor() { this.fields = []; - this.buttons = []; - for (i = 0; i < (buttonsPerLine * buttonsPerLine); i++) { + this.resultField = Field.forResult(); + this.menuField = Field.forMenu(); + for (i = 0; i < buttonsPerBoard; i++) this.fields[i] = Field.forIndex(i); - this.buttons[i] = new Stone((i + 1) % (buttonsPerLine * buttonsPerLine),i); + this.setShuffled(); + } + // Set the board into the "solved" position + setSolved() { + this.buttons = []; + for (i = 0; i < buttonsPerBoard; i++) + this.buttons[i] = new Stone((i + 1) % buttonsPerBoard, i); + this.moveCount = 0; + } + setShuffled() { + let nrs = []; + for (i = 0; i < buttonsPerBoard; i++) + nrs[i] = i; + this.buttons = []; + let count = buttonsPerBoard; + for (i = 0; i < buttonsPerBoard; i++) { + let curridx = Math.floor(Math.random() * count); + let currnr = nrs[curridx]; + this.buttons[i] = new Stone(currnr, (currnr + (buttonsPerBoard - 1)) % buttonsPerBoard); + for (j = curridx + 1; j < count; j++) + nrs[j - 1] = nrs[j]; + count -= 1; } + if (!this.isSolvable()) { + let a = (this.buttons[0].number === 0 ? 2 : 0); + let b = (this.buttons[1].number === 0 ? 2 : 1); + let bx = this.buttons[a]; + this.buttons[a] = this.buttons[b]; + this.buttons[b] = bx; + } + this.moveCount = 0; } // Draws the complete playing field draw() { new Clearer(this.fields[0], this.fields[this.fields.length - 1]).clearArea(); for (i = 0; i < this.fields.length; i++) this.buttons[i].draw(this.fields[i]); + this.drawResult(null); + this.drawMenu(); } // returns the index of the field left of the field with the given index, // -1 if there is none (index indicates already a leftmost field on the board) @@ -270,36 +382,44 @@ class Board { return i; return -1; } + // Returns the row in which the gap is, 0 is upmost + rowOf0() { + let idx = this.indexOf0(); + if (idx < 0) + return -1; + return Math.floor(idx / buttonsPerLine); + } // Moves the stone at the field with the index found by the startfunc operation // into the gap field. - moveTo0(startfunc, animator) { + moveTo0(startfunc, worker) { let endidx = this.indexOf0(); // Target field (the gap) if (endidx === -1) { - animator.endTask(); + worker.endTask(); return; } - let startidx = startfunc(endidx); // Start field (relative to the gap) + let startidx = startfunc(endidx); // Start field (relative to the gap) if (startidx === -1) { - animator.endTask(); + worker.endTask(); return; } let moved = this.buttons[startidx]; this.buttons[startidx] = this.buttons[endidx]; this.buttons[endidx] = moved; - new Mover(moved, this.fields[startidx], this.fields[endidx], animationSteps).animate(animator); + this.moveCount += 1; + new Mover(moved, this.fields[startidx], this.fields[endidx], animationSteps).animate(worker); } // Move the stone right fro the gap into the gap - moveRight(animator) { - this.moveTo0(this.leftOf, animator); + moveRight(worker) { + this.moveTo0(this.leftOf, worker); } - moveLeft(animator) { - this.moveTo0(this.rightOf, animator); + moveLeft(worker) { + this.moveTo0(this.rightOf, worker); } - moveUp(animator) { - this.moveTo0(this.bottomOf, animator); + moveUp(worker) { + this.moveTo0(this.bottomOf, worker); } - moveDown(animator) { - this.moveTo0(this.topOf, animator); + moveDown(worker) { + this.moveTo0(this.topOf, worker); } // Check if the board is solved (all stones at the right position) isSolved() { @@ -308,54 +428,152 @@ class Board { return false; return true; } + // counts the inversions on the board + // see https://www.geeksforgeeks.org/check-instance-15-puzzle-solvable/ + getInversionCount() { + let inversions = 0; + for (outer = 0; outer < buttonsPerBoard - 1; outer++) { + let outernr = this.buttons[outer].number; + if (outernr === 0) + continue; + for (inner = outer + 1; inner < buttonsPerBoard; inner++) { + let innernr = this.buttons[inner].number; + if (innernr > 0 && outernr > innernr) + inversions++; + } + } + return inversions; + } + // return whether the puzzle is solvable + // see https://www.geeksforgeeks.org/check-instance-15-puzzle-solvable/ + isSolvable() { + let invs = this.getInversionCount(); + if (buttonsPerLine % 2 !== 0) // odd number of rows/columns + return (invs % 2 === 0); + else { + return ((invs + this.rowOf0()) % 2 !== 0); + } + } + // draw the result field, pass null as argument if not called from worker + drawResult(worker) { + let field = this.resultField; + if (this.isSolved()) + g.setColor(0, 1, 0); + else + g.setColor(1, 0, 0); + g.fillRect(field.left, field.top, field.left + buttonsize, field.top + buttonsize); + g.setColor(0, 0, 0); + g.drawRect(field.left, field.top, field.left + buttonsize, field.top + buttonsize); + g.setFont("Vector", 14).setFontAlign(0, 0).drawString(this.moveCount, field.centerx, field.centery); + if (worker !== null) + worker.endTask(); + } + // draws the menu button + drawMenu() { + let field = this.menuField; + g.setColor(0.5, 0.5, 0.5); + g.fillRect(field.left, field.top, field.left + buttonsize, field.top + buttonsize); + g.setColor(0, 0, 0); + g.drawRect(field.left, field.top, field.left + buttonsize, field.top + buttonsize); + let l = field.left + 8; + let r = field.left + buttonsize - 8; + let t = field.top + 5; + for (i = 0; i < 3; i++) + g.fillRect(l, t + (i * 7), r, t + (i * 7) + 3); + } } +/* +// Main class, containing the complete game logic +class Puzzle15 { + constructor() { + this.worker=new Worker(); + this.board=new Board(); + } +} +*/ + // *** Main program // We need a worker... var worker = new Worker(); + +setButtonsPerLine(3); // ...and the board var board = new Board(); -// UI: Accumulation of current drag operation -var currentdrag = { - x: 0, - y: 0 -}; +var dragger; + +function initGame(bpl) { + setButtonsPerLine(bpl); + board = new Board(); + board.draw(); + dragger.setEnabled(true); +} + +function showMenu() { + var mainmenu = { + "": { + "title": "15 Puzzle" + }, + "< Back": () => { + E.showMenu(); + dragger.setEnabled(true); + board.draw(); + }, // remove the menu + "Start 3x3": function() { + E.showMenu(); + initGame(3); + }, + "Start 4x4": function() { + E.showMenu(); + initGame(4); + }, + "Start 5x5": function() { + E.showMenu(); + initGame(5); + } + }; + dragger.setEnabled(false); + + E.showMenu(mainmenu); +} + +function handleclick(e) { + if (board.menuField.contains(e.x, e.y)) { + console.log("GGG - handleclick, dragger: " + dragger); + g.reset(); + showMenu(); + console.log("showing menu ended"); + } +} // Handle a drag event function handledrag(e) { - if (e.b === 0) { // Drag event ended: Evaluate drag and start move operation - if (Math.abs(currentdrag.x) > Math.abs(currentdrag.y)) { // Horizontal drag - if (currentdrag.x > dragThreshold) - worker.addTask(e => board.moveRight(e)); - else if (currentdrag.x < -dragThreshold) - worker.addTask(e => board.moveLeft(e)); - } else { // Vertical drag - if (currentdrag.y > dragThreshold) - worker.addTask(e => board.moveDown(e)); - else if (currentdrag.y < -dragThreshold) - worker.addTask(e => board.moveUp(e)); - } - currentdrag.x = 0; // Clear the drag accumulator - currentdrag.y = 0; - } else { // Drag still running: Accumulate drag shifts - currentdrag.x += e.dx; - currentdrag.y += e.dy; - } + worker.addTask(Math.abs(e.dx) > Math.abs(e.dy) ? + (e.dx > 0 ? e => board.moveRight(e) : e => board.moveLeft(e)) : + (e.dy > 0 ? e => board.moveDown(e) : e => board.moveUp(e))); + worker.addTask(e => board.drawResult(e)); } // Clear the screen once, at startup g.clear(); -// Drop mode as this is a game -Bangle.setUI(undefined); + +// Clock mode allows short-press on button to exit +Bangle.setUI("clock"); // Load widgets Bangle.loadWidgets(); Bangle.drawWidgets(); // Draw the board initially board.draw(); + +dragger = new Dragger(handleclick, handledrag, clickThreshold, dragThreshold); + +showMenu(); // Start the interaction -Bangle.on("drag", handledrag); +dragger.attach(); + +console.log("GGG - main program, dragger: " + dragger); // end of file \ No newline at end of file From 46286e13b52127774ffe0f48ee7230e8b19bfdc7 Mon Sep 17 00:00:00 2001 From: "Dirk Hillbrecht (home)" Date: Mon, 3 Jan 2022 18:12:04 +0100 Subject: [PATCH 4/6] Puzzle15: v0.03 with splash screen and working menu system --- apps.json | 2 +- apps/puzzle15/ChangeLog | 1 + apps/puzzle15/README.md | 27 ++++--- apps/puzzle15/introscreen.png | Bin 0 -> 3717 bytes apps/puzzle15/puzzle15.app.js | 147 ++++++++++++++++++++++------------ 5 files changed, 116 insertions(+), 61 deletions(-) create mode 100644 apps/puzzle15/introscreen.png diff --git a/apps.json b/apps.json index 4a3d8c7c8..f6ef51546 100644 --- a/apps.json +++ b/apps.json @@ -5393,7 +5393,7 @@ { "id": "puzzle15", "name": "15 puzzle", - "version": "0.02", + "version": "0.03", "description": "A 15 puzzle game with drag gesture interface", "readme":"README.md", "icon": "puzzle15.app.png", diff --git a/apps/puzzle15/ChangeLog b/apps/puzzle15/ChangeLog index bc5a4422b..d7458c968 100644 --- a/apps/puzzle15/ChangeLog +++ b/apps/puzzle15/ChangeLog @@ -1,2 +1,3 @@ 0.01: Initial version, UI mechanics ready, no real game play so far 0.02: Lots of enhancements, menu system not yet functional, but packaging should be now... +0.03: Menu logic now generally functioning, splash screen added. The first really playable version! diff --git a/apps/puzzle15/README.md b/apps/puzzle15/README.md index 540646f97..eddeb3b94 100644 --- a/apps/puzzle15/README.md +++ b/apps/puzzle15/README.md @@ -1,18 +1,27 @@ # Puzzle15 - A 15-puzzle for the Bangle.js 2 -Puzzle15 implements a 15-puzzle on the screen of the Bangle.js 2 smart watch. -A "15-puzzle" is a single-player game. 15 "stones" are numbered from 1 to 15 and placed randomly on a 4 by 4 playing field. -So, one place on the field is free. -The target of the game is to move the stones around (using the "gap" on the playing field) to bring them in natural order. +This is a Bangle.js 2 adoption of the famous 15 puzzle. -_This is work in progress!_ +## The game + +A board of n x n fields is filled with n²-1 numbered stones. +Bring them in the correct order so that the gap is finally at the bottom right of the playing field. +The less moves you need, the better you are. ## How to play -After starting the game, the playing field is shown with the stones in the corrct order. -Push the stones around with drag gestures (left to right, right to left, top to bottom, bottom to top) to shuffle them. -Then, use the same gestures to restore the order. +Select whether you want to play on a board with 3 x 3, 4 x 4, or 5 x 5 fields. +Move the stones with drag gestures on the screen. +If you want to move the stone below the gap upward, drag from the bottom of the screen upward. +The drag gestures can be performed anywhere on the screen, there is no need to start or end them on the stone to be moved. -So far, there are no other capabililties or options. +If you managed to order the stones correctly, a success message appears. +You can continue with another game, go to the game's main menu, or quit the game entirely. + +There is a menu button right of the board. It opens the game's main menu. + +## Implemenation notes + +The game engine always generates 15 puzzles which can be solved. Have fun! diff --git a/apps/puzzle15/introscreen.png b/apps/puzzle15/introscreen.png new file mode 100644 index 0000000000000000000000000000000000000000..766cd61fadbcf8afb58f5bbea090796c89df5eb1 GIT binary patch literal 3717 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 { + gameEnd(this.moveCount); + }, 500); } // draws the menu button drawMenu() { @@ -479,22 +500,39 @@ class Board { let r = field.left + buttonsize - 8; let t = field.top + 5; for (i = 0; i < 3; i++) - g.fillRect(l, t + (i * 7), r, t + (i * 7) + 3); + g.fillRect(l, t + (i * 6), r, t + (i * 6) + 2); } } -/* -// Main class, containing the complete game logic -class Puzzle15 { - constructor() { - this.worker=new Worker(); - this.board=new Board(); - } +function framedText(text, x, y, textcol, framecol) { + g.setColor(framecol); + for (i = -2; i < 3; i++) + for (j = -2; j < 3; j++) { + if (i === 0 && j === 0) + continue; + g.drawString(text, x + i, y + j); + } + g.setColor(textcol).drawString(text, x, y); +} + +function showSplash(afterSplash) { + g.reset(); + g.drawImage(introscreen, 0, 0); + setTimeout(() => { + g.setFont("Vector", 40).setFontAlign(0, 0); + framedText("15", g.getWidth() / 2, g.getHeight() / 2 - g.getFontHeight() * 0.66, "#f00", "#fff"); + setTimeout(() => { + g.setFont("Vector", 40).setFontAlign(0, 0); + framedText("Puzzle", g.getWidth() / 2, g.getHeight() / 2 + g.getFontHeight() * 0.66, "#f00", "#fff"); + setTimeout(afterSplash, 2000); + }, 1000); + }, 2000); } -*/ // *** Main program +g.reset(); + // We need a worker... var worker = new Worker(); @@ -504,48 +542,61 @@ var board = new Board(); var dragger; +// Initialize the game with an explicit number of stones per line function initGame(bpl) { setButtonsPerLine(bpl); + newGame(); +} + +// Start a new game with the same number of stones per line as before +function newGame() { board = new Board(); + continueGame(); +} + +// Continue the currently running game +function continueGame() { + E.showMenu(); board.draw(); dragger.setEnabled(true); } -function showMenu() { +// Show message on game end, allows to restart new game +function gameEnd(moveCount) { + dragger.setEnabled(false); + E.showPrompt("You solved the\n" + buttonsPerLine + "x" + buttonsPerLine + " puzzle in\n" + moveCount + " move" + (moveCount === 1 ? "" : "s") + ".", { + title: "Puzzle solved", + buttons: { + "Again": newGame, + "Menu": () => showMenu(false), + "Exit": load + } + }).then(v => { + E.showPrompt(); + setTimeout(v, 10); + }); +} + +function showMenu(withContinue) { var mainmenu = { "": { "title": "15 Puzzle" - }, - "< Back": () => { - E.showMenu(); - dragger.setEnabled(true); - board.draw(); - }, // remove the menu - "Start 3x3": function() { - E.showMenu(); - initGame(3); - }, - "Start 4x4": function() { - E.showMenu(); - initGame(4); - }, - "Start 5x5": function() { - E.showMenu(); - initGame(5); } }; + if (withContinue) + mainmenu.Continue = continueGame; + mainmenu["Start 3x3"] = () => initGame(3); + mainmenu["Start 4x4"] = () => initGame(4); + mainmenu["Start 5x5"] = () => initGame(5); + mainmenu.Exit = () => load(); dragger.setEnabled(false); - + g.clear(true); E.showMenu(mainmenu); } function handleclick(e) { - if (board.menuField.contains(e.x, e.y)) { - console.log("GGG - handleclick, dragger: " + dragger); - g.reset(); - showMenu(); - console.log("showing menu ended"); - } + if (board.menuField.contains(e.x, e.y)) + setTimeout(() => showMenu(true), 10); } // Handle a drag event @@ -556,24 +607,18 @@ function handledrag(e) { worker.addTask(e => board.drawResult(e)); } -// Clear the screen once, at startup -g.clear(); - -// Clock mode allows short-press on button to exit -Bangle.setUI("clock"); -// Load widgets -Bangle.loadWidgets(); -Bangle.drawWidgets(); - -// Draw the board initially -board.draw(); - dragger = new Dragger(handleclick, handledrag, clickThreshold, dragThreshold); -showMenu(); // Start the interaction dragger.attach(); -console.log("GGG - main program, dragger: " + dragger); +showSplash(() => { + // Clock mode allows short-press on button to exit + Bangle.setUI("clock"); + // Load widgets + Bangle.loadWidgets(); + Bangle.drawWidgets(); + showMenu(false); +}, 5000); // end of file \ No newline at end of file From 3633078a46a9526a15c023e58aeec3730692e8df Mon Sep 17 00:00:00 2001 From: "Dirk Hillbrecht (home)" Date: Tue, 4 Jan 2022 17:35:52 +0100 Subject: [PATCH 5/6] Puzzle15: v0.04 with settings, formatting changes, about screen --- apps.json | 6 +- apps/puzzle15/ChangeLog | 1 + apps/puzzle15/README.md | 40 +++- apps/puzzle15/puzzle15.app.js | 357 ++++++++++++++++++++--------- apps/puzzle15/puzzle15.settings.js | 50 ++++ 5 files changed, 339 insertions(+), 115 deletions(-) create mode 100644 apps/puzzle15/puzzle15.settings.js diff --git a/apps.json b/apps.json index f6ef51546..3a390938d 100644 --- a/apps.json +++ b/apps.json @@ -5393,7 +5393,7 @@ { "id": "puzzle15", "name": "15 puzzle", - "version": "0.03", + "version": "0.04", "description": "A 15 puzzle game with drag gesture interface", "readme":"README.md", "icon": "puzzle15.app.png", @@ -5404,7 +5404,9 @@ "allow_emulator": true, "storage": [ {"name":"puzzle15.app.js","url":"puzzle15.app.js"}, + {"name":"puzzle15.settings.js","url":"puzzle15.settings.js"}, {"name":"puzzle15.img","url":"puzzle15.app-icon.js","evaluate":true} - ] + ], + "data": [{"name":"puzzle15.json"}] } ] diff --git a/apps/puzzle15/ChangeLog b/apps/puzzle15/ChangeLog index d7458c968..bd6af53d8 100644 --- a/apps/puzzle15/ChangeLog +++ b/apps/puzzle15/ChangeLog @@ -1,3 +1,4 @@ 0.01: Initial version, UI mechanics ready, no real game play so far 0.02: Lots of enhancements, menu system not yet functional, but packaging should be now... 0.03: Menu logic now generally functioning, splash screen added. The first really playable version! +0.04: Settings dialog, about screen diff --git a/apps/puzzle15/README.md b/apps/puzzle15/README.md index eddeb3b94..16c0c4593 100644 --- a/apps/puzzle15/README.md +++ b/apps/puzzle15/README.md @@ -4,13 +4,16 @@ This is a Bangle.js 2 adoption of the famous 15 puzzle. ## The game -A board of n x n fields is filled with n²-1 numbered stones. +A board of _n_ by _n_ fields is filled with _n^2-1_ numbered stones. So, one field, the "gap", is left free. + Bring them in the correct order so that the gap is finally at the bottom right of the playing field. The less moves you need, the better you are. +If _n_ is 4, the number of stones is _16-1=15_. Hence the name of the game. + ## How to play -Select whether you want to play on a board with 3 x 3, 4 x 4, or 5 x 5 fields. +If you start the game, it shows a splash screen and then generates a shuffled 4x4 board with a 15 puzzle. Move the stones with drag gestures on the screen. If you want to move the stone below the gap upward, drag from the bottom of the screen upward. The drag gestures can be performed anywhere on the screen, there is no need to start or end them on the stone to be moved. @@ -18,10 +21,37 @@ The drag gestures can be performed anywhere on the screen, there is no need to s If you managed to order the stones correctly, a success message appears. You can continue with another game, go to the game's main menu, or quit the game entirely. -There is a menu button right of the board. It opens the game's main menu. +There is a grey menu button right of the board containing the well-known three-bar menu symbol ("Hamburger menu"). +It opens the game's main menu directly from within the game. -## Implemenation notes +## The main menu -The game engine always generates 15 puzzles which can be solved. +Puzzle15 has a main menu which can be reached from the in-game menu button or the end-of-game message window. +It features the following options: + +* **Continue** - Continue the currently running game. _This option is only shown if the main menu is opened during an open game._ +* **Start 3x3**, **Start 4x4**, **Start 5x5** - Start a new game on a board with the respective dimension. Any currently open game is dropped. +* **About** Show a small "About" info box. +* **Exit** Exit Puzzle15 and return to the default watch face. + +## Game settings + +The game has some global settings which can be accessed on the usual way through the Bangle.js' app settings user interface. +Currently it has the following options: + +* **Splash** - Define whether the game should open with a splash screen. **long** shows the splash screen for five seconds, **short** shows it for two seconds. **off** starts the app _without_ a splash screen, it directly comes up with whatever the "Start with" option says. +* **Start with** - What should happen after the splash screen (or, if it is disabled, directly at app start): **3x3**, **4x4** and **5x5** start the game with a board of the respective dimension, **menu** shows the main menu which allows to select the board size. + +## Implementation notes + +The game engine always generates puzzles which can be solved. + +Solvability is detected by counting inversions, +i.e. pairs of stones where the stone at the earlier field (row-wise, left to right, top to bottom) has a number _greater than_ the stone on the later field, with all pairs of stones compared. +The algorithm is described at https://www.geeksforgeeks.org/check-instance-15-puzzle-solvable/ . + +## The splash screen + +The Splash screen shows a part of the illustration "The 14-15-puzzle in puzzleland" from Sam Loyd. Other than Puzzle15, it depicts a 15 puzzle with the stones "14" and "15" swapped. This puzzle is indeed *not* solvable. Have fun! diff --git a/apps/puzzle15/puzzle15.app.js b/apps/puzzle15/puzzle15.app.js index c5c60bb0f..e66f9b9bb 100644 --- a/apps/puzzle15/puzzle15.app.js +++ b/apps/puzzle15/puzzle15.app.js @@ -2,36 +2,37 @@ // (C) Dirk Hillbrecht 2022 // Released unter the terms of the MIT license -// *** Global settings -// Note: These could be changed by settings later... - +// The intro screen as large base-64-encoded binary data const introscreen = E.toArrayBuffer(atob("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")); +// *** Global constants from which several other settings are derived + // Minimum number of pixels to interpret it as drag gesture const dragThreshold = 10; // Maximum number of pixels to interpret a click from a drag event series const clickThreshold = 3; -// Number of steps in button move animation +// Number of steps in stone move animation const animationSteps = 6; // Milliseconds to wait between move animation steps const animationWaitMillis = 30; -// *** Global settings derived by device characteristics - // Total width of the playing field (full screen width) const fieldw = g.getWidth(); // Total height of the playing field (screen height minus widget zones) const fieldh = g.getHeight() - 48; -// Size of the playing field -var buttonsPerLine; -// Size of one button -var buttonsize; +// *** Global game characteristics + +// Size of the playing field +var stonesPerLine; + +// Size of one field +var stonesize; // Actual left start of the playing field (so that it is centered) var leftstart; @@ -39,18 +40,51 @@ var leftstart; // Actual top start of the playing field (so that it is centered) var topstart; -// Number of buttons on the board (needed at several occasions) -var buttonsPerBoard; +// Number of stones on the board (needed at several occasions) +var stonesPerBoard; -// Set the buttons per line globally and all derived values, too -function setButtonsPerLine(bPL) { - buttonsPerLine = bPL; - buttonsize = Math.floor(Math.min(fieldw / (buttonsPerLine + 1), fieldh / buttonsPerLine)) - 2; - leftstart = (fieldw - ((buttonsPerLine + 1) * buttonsize + 8)) / 2; - topstart = 24 + ((fieldh - (buttonsPerLine * buttonsize + 6)) / 2); - buttonsPerBoard = (buttonsPerLine * buttonsPerLine); +// Set the stones per line globally and all derived values, too +function setStonesPreLine(bPL) { + stonesPerLine = bPL; + stonesize = Math.floor(Math.min(fieldw / (stonesPerLine + 1), fieldh / stonesPerLine)) - 2; + leftstart = (fieldw - ((stonesPerLine + 1) * stonesize + 8)) / 2; + topstart = 24 + ((fieldh - (stonesPerLine * stonesize + 6)) / 2); + stonesPerBoard = (stonesPerLine * stonesPerLine); } + +// *** Global app settings + +var SETTINGSFILE = "puzzle15.json"; + +// variables defined from settings +var splashMode; +var startWith; + +/* For development purposes +require('Storage').writeJSON(SETTINGSFILE, { + splashMode: "off", + startWith: "5x5", +}); +/* */ + +/* OR (also for development purposes) +require('Storage').erase(SETTINGSFILE); +/* */ + +// Helper method for loading the settings +function def(value, def) { + return (value !== undefined ? value : def); +} + +// Load settings +function loadSettings() { + var settings = require('Storage').readJSON(SETTINGSFILE, true) || {}; + splashMode = def(settings.splashMode, "long"); + startWith = def(settings.startWith, "4x4"); +} + + // *** Low level helper classes // One node of a first-in-first-out storage @@ -64,11 +98,13 @@ class FifoNode { // Simple first-in-first-out (fifo) storage // Needed to keep the stone movements in order class Fifo { + // Initialize an empty Fifo constructor() { this.first = null; this.last = null; } + // Add an element to the end of the internal fifo queue add(payload) { if (this.last === null) { // queue is empty @@ -80,6 +116,7 @@ class Fifo { this.last = newlast; } } + // Returns the first element in the queue, null if it is empty remove() { if (this.first === null) @@ -90,10 +127,12 @@ class Fifo { this.last = null; return oldfirst.payload; } + // Returns if the fifo is empty, i.e. it does not hold any elements isEmpty() { return (this.first === null); } + } // Helper class to keep track of tasks @@ -104,11 +143,13 @@ class Fifo { // previously scheduled tasks have finished. // Tasks must be functions with the Worker as first and only parameter. class Worker { + // Create an empty worker constructor() { this.tasks = new Fifo(); this.busy = false; } + // Add a task to the worker addTask(task) { if (this.busy) // other task is running: Queue this task @@ -118,6 +159,7 @@ class Worker { task(this); } } + // Called by the task once it finished endTask() { if (this.tasks.isEmpty()) // No more tasks queued: Become idle @@ -125,6 +167,7 @@ class Worker { else // Call the next task immediately this.tasks.remove()(this); } + } // Evaluate "drag" events from the UI and call handlers for drags or clicks @@ -135,6 +178,7 @@ class Worker { // otherwise as drag. // The approprate method is called with the data of the drag. class Dragger { + constructor(clickHandler, dragHandler, clickThreshold, dragThreshold) { this.clickHandler = clickHandler; this.dragHandler = dragHandler; @@ -144,10 +188,12 @@ class Dragger { this.dy = 0; this.enabled = true; } + // Enable or disable the Dragger setEnabled(b) { this.enabled = b; } + // Handle a raw drag event from the UI handleRawDrag(e) { if (!this.enabled) @@ -171,6 +217,7 @@ class Dragger { this.dy = 0; } } + // Attach the drag evaluator to the UI attach() { Bangle.on("drag", e => this.handleRawDrag(e)); @@ -185,37 +232,43 @@ class Dragger { // During an animation, a series of interim field instances is generated // which represents the locations of a stone during the animation. class Field { + // Generate a field with a left and a top coordinate. // Note that these coordinates are "cooked", i.e. they contain all offsets // needed place the elements globally correct on the screen constructor(left, top) { this.left = left; this.top = top; - this.centerx = (left + buttonsize / 2) + 1; - this.centery = (top + buttonsize / 2) + 2; + this.centerx = (left + stonesize / 2) + 1; + this.centery = (top + stonesize / 2) + 2; } + // Returns whether this field contains the given coordinate contains(x, y) { - return (this.left < x && this.left + buttonsize > x && - this.top < y && this.top + buttonsize > y); + return (this.left < x && this.left + stonesize > x && + this.top < y && this.top + stonesize > y); } + // Generate a field for the given playing field index. // Playing field indexes start at top left with "0" // and go from left to right line by line from top to bottom. static forIndex(index) { - return new Field(leftstart + (index % buttonsPerLine) * (buttonsize + 2), - topstart + (Math.floor(index / buttonsPerLine)) * (buttonsize + 2)); + return new Field(leftstart + (index % stonesPerLine) * (stonesize + 2), + topstart + (Math.floor(index / stonesPerLine)) * (stonesize + 2)); + } // Special field for the result "stone" static forResult() { - return new Field(leftstart + (buttonsPerLine * (buttonsize + 2)), - topstart + ((buttonsPerLine - 1) * (buttonsize + 2))); + return new Field(leftstart + (stonesPerLine * (stonesize + 2)), + topstart + ((stonesPerLine - 1) * (stonesize + 2))); } + // Special field for the menu static forMenu() { - return new Field(leftstart + (buttonsPerLine * (buttonsize + 2)), + return new Field(leftstart + (stonesPerLine * (stonesize + 2)), topstart); } + } // Representation of a moveable stone of the game. @@ -224,6 +277,7 @@ class Field { // Stone "0" represents the gap on the playing field. // The main knowledge of a Stone instance is how to draw itself. class Stone { + // Create stone with the given number // The constructor creates the "draw()" function which is used to draw the stone constructor(number, targetindex) { @@ -232,22 +286,23 @@ class Stone { // gap: Does not draw anything if (number === 0) this.draw = function(field) {}; - else if ((number + (buttonsPerLine % 2 == 0 ? (Math.floor((number - 1) / buttonsPerLine)) : 0)) % 2 == 0) { + else if ((number + (stonesPerLine % 2 == 0 ? (Math.floor((number - 1) / stonesPerLine)) : 0)) % 2 == 0) { // Black stone this.draw = function(field) { - g.setFont("Vector", 20).setFontAlign(0, 0).setColor(0, 0, 0); - g.fillRect(field.left, field.top, field.left + buttonsize, field.top + buttonsize); + g.setFont("Vector", (stonesPerLine === 5 ? 16 : 20)).setFontAlign(0, 0).setColor(0, 0, 0); + g.fillRect(field.left, field.top, field.left + stonesize, field.top + stonesize); g.setColor(1, 1, 1).drawString(number, field.centerx, field.centery); }; } else { // White stone this.draw = function(field) { - g.setFont("Vector", 20).setFontAlign(0, 0).setColor(0, 0, 0); - g.drawRect(field.left, field.top, field.left + buttonsize, field.top + buttonsize); + g.setFont("Vector", (stonesPerLine === 5 ? 16 : 20)).setFontAlign(0, 0).setColor(0, 0, 0); + g.drawRect(field.left, field.top, field.left + stonesize, field.top + stonesize); g.drawString(number, field.centerx, field.centery); }; } } + // Returns whether this stone is on its target index isOnTarget(index) { return index === this.targetindex; @@ -256,6 +311,7 @@ class Stone { // Helper class which knows how to clear the rectangle opened up by the two given fields class Clearer { + // Create a clearer for the area between the two given fields constructor(startfield, endfield) { this.minleft = Math.min(startfield.left, endfield.left); @@ -263,16 +319,19 @@ class Clearer { this.maxleft = Math.max(startfield.left, endfield.left); this.maxtop = Math.max(startfield.top, endfield.top); } + // Clear the area defined by this clearer clearArea() { g.setColor(1, 1, 1); g.fillRect(this.minleft, this.mintop, - this.maxleft + buttonsize, this.maxtop + buttonsize); + this.maxleft + stonesize, this.maxtop + stonesize); } + } // Helper class which moves a stone between two fields class Mover extends Clearer { + // Create a mover which moves the given stone from startfield to endfield // and animate the move in the given number of steps constructor(stone, startfield, endfield, steps) { @@ -282,11 +341,13 @@ class Mover extends Clearer { this.endfield = endfield; this.steps = steps; } + // Create the coordinate between start and end for the given step // Computation uses sinus for a smooth movement stepCoo(start, end, step) { return start + ((end - start) * ((1 + Math.sin((step / this.steps) * Math.PI - (Math.PI / 2))) / 2)); } + // Compute the interim field for the stone to place during the animation stepField(step) { return new Field( @@ -295,6 +356,7 @@ class Mover extends Clearer { (this.mintop === this.maxtop ? this.mintop : this.stepCoo(this.startfield.top, this.endfield.top, step))); } + // Perform one animation step animateStep(step, worker) { this.clearArea(); @@ -306,53 +368,60 @@ class Mover extends Clearer { else // all steps done: Inform the worker worker.endTask(); } + // Start the animation, this method is called by the worker animate(worker) { this.animateStep(1, worker); } + } // Representation of the playing field // Knows to draw the field and to move a stone into a gap -// TODO: More game mechanics (solving,...) class Board { - // Generates the actual playing field with all fields and buttons + + // Generates the actual playing field with all fields and stones constructor() { this.fields = []; this.resultField = Field.forResult(); this.menuField = Field.forMenu(); - for (i = 0; i < buttonsPerBoard; i++) + for (i = 0; i < stonesPerBoard; i++) this.fields[i] = Field.forIndex(i); this.setShuffled(); //this.setAlmostSolved(); // to test the game end } + + /* // Set the board into the "solved" position. Useful for showcasing setSolved() { - this.buttons = []; - for (i = 0; i < buttonsPerBoard; i++) - this.buttons[i] = new Stone((i + 1) % buttonsPerBoard, i); + this.stones = []; + for (i = 0; i < stonesPerBoard; i++) + this.stones[i] = new Stone((i + 1) % stonesPerBoard, i); this.moveCount = 0; } + // Initialize an almost solved playing field. Useful for tests and development setAlmostSolved() { this.setSolved(); - b = this.buttons[this.buttons.length - 1]; - this.buttons[this.buttons.length - 1] = this.buttons[this.buttons.length - 2]; - this.buttons[this.buttons.length - 2] = b; + b = this.stones[this.stones.length - 1]; + this.stones[this.stones.length - 1] = this.stones[this.stones.length - 2]; + this.stones[this.stones.length - 2] = b; } + */ + // Initialize a shuffled field. The fields are always solvable. setShuffled() { let nrs = []; // numbers of the stones - for (i = 0; i < buttonsPerBoard; i++) + for (i = 0; i < stonesPerBoard; i++) nrs[i] = i; - this.buttons = []; - let count = buttonsPerBoard; - for (i = 0; i < buttonsPerBoard; i++) { + this.stones = []; + let count = stonesPerBoard; + for (i = 0; i < stonesPerBoard; i++) { // Take a random number of the (remaining) numbers let curridx = Math.floor(Math.random() * count); let currnr = nrs[curridx]; - // Initialize the next button with that random number - this.buttons[i] = new Stone(currnr, (currnr + (buttonsPerBoard - 1)) % buttonsPerBoard); + // Initialize the next stone with that random number + this.stones[i] = new Stone(currnr, (currnr + (stonesPerBoard - 1)) % stonesPerBoard); // Remove the number just taken from the list of numbers for (j = curridx + 1; j < count; j++) nrs[j - 1] = nrs[j]; @@ -361,52 +430,68 @@ class Board { // not solvable: Swap the first and second stone which are not the gap. // This will always result in a solvable board. if (!this.isSolvable()) { - let a = (this.buttons[0].number === 0 ? 2 : 0); - let b = (this.buttons[1].number === 0 ? 2 : 1); - let bx = this.buttons[a]; - this.buttons[a] = this.buttons[b]; - this.buttons[b] = bx; + let a = (this.stones[0].number === 0 ? 2 : 0); + let b = (this.stones[1].number === 0 ? 2 : 1); + let bx = this.stones[a]; + this.stones[a] = this.stones[b]; + this.stones[b] = bx; } this.moveCount = 0; } + // Draws the complete playing field draw() { new Clearer(this.fields[0], this.fields[this.fields.length - 1]).clearArea(); for (i = 0; i < this.fields.length; i++) - this.buttons[i].draw(this.fields[i]); + this.stones[i].draw(this.fields[i]); this.drawResult(null); this.drawMenu(); } + // returns the index of the field left of the field with the given index, // -1 if there is none (index indicates already a leftmost field on the board) leftOf(index) { - return (index % buttonsPerLine === 0 ? -1 : index - 1); + return (index % stonesPerLine === 0 ? -1 : index - 1); } + + // returns the index of the field right of the field with the given index, + // -1 if there is none (index indicates already a rightmost field on the board) rightOf(index) { - return (index % buttonsPerLine === (buttonsPerLine - 1) ? -1 : index + 1); + return (index % stonesPerLine === (stonesPerLine - 1) ? -1 : index + 1); } + + // returns the index of the field top of the field with the given index, + // -1 if there is none (index indicates already a topmost field on the board) topOf(index) { - return (index >= buttonsPerLine ? index - buttonsPerLine : -1); + return (index >= stonesPerLine ? index - stonesPerLine : -1); } + + // returns the index of the field bottom of the field with the given index, + // -1 if there is none (index indicates already a bottommost field on the board) bottomOf(index) { - return (index < (buttonsPerLine - 1) * buttonsPerLine ? index + buttonsPerLine : -1); + return (index < (stonesPerLine - 1) * stonesPerLine ? index + stonesPerLine : -1); } + // Return the index of the gap in the field, -1 if there is none (should never happel) indexOf0() { - for (i = 0; i < this.buttons.length; i++) - if (this.buttons[i].number === 0) + for (i = 0; i < this.stones.length; i++) + if (this.stones[i].number === 0) return i; return -1; } + // Returns the row in which the gap is, 0 is upmost rowOf0() { let idx = this.indexOf0(); if (idx < 0) return -1; - return Math.floor(idx / buttonsPerLine); + return Math.floor(idx / stonesPerLine); } - // Moves the stone at the field with the index found by the startfunc operation - // into the gap field. + + // Searches the gap on the field and then moves one of the adjacent stones into it. + // The stone is selected by the given startfunc which returns the index + // of the selected adjacent field. + // Startfunc is one of (left|right|top|bottom)Of. moveTo0(startfunc, worker) { let endidx = this.indexOf0(); // Target field (the gap) if (endidx === -1) { @@ -418,58 +503,71 @@ class Board { worker.endTask(); return; } - let moved = this.buttons[startidx]; - this.buttons[startidx] = this.buttons[endidx]; - this.buttons[endidx] = moved; + // Replace in the internal representation + let moved = this.stones[startidx]; + this.stones[startidx] = this.stones[endidx]; + this.stones[endidx] = moved; this.moveCount += 1; + // Move on screen using an animation effect. new Mover(moved, this.fields[startidx], this.fields[endidx], animationSteps).animate(worker); } - // Move the stone right fro the gap into the gap + + // Move the stone right from the gap into the gap moveRight(worker) { this.moveTo0(this.leftOf, worker); } + + // Move the stone left from the gap into the gap moveLeft(worker) { this.moveTo0(this.rightOf, worker); } + + // Move the stone above the gap into the gap moveUp(worker) { this.moveTo0(this.bottomOf, worker); } + + // Move the stone below the gap into the gap moveDown(worker) { this.moveTo0(this.topOf, worker); } + // Check if the board is solved (all stones at the right position) isSolved() { - for (i = 0; i < this.buttons.length; i++) - if (!this.buttons[i].isOnTarget(i)) + for (i = 0; i < this.stones.length; i++) + if (!this.stones[i].isOnTarget(i)) return false; return true; } + // counts the inversions on the board // see https://www.geeksforgeeks.org/check-instance-15-puzzle-solvable/ getInversionCount() { let inversions = 0; - for (outer = 0; outer < buttonsPerBoard - 1; outer++) { - let outernr = this.buttons[outer].number; + for (outer = 0; outer < stonesPerBoard - 1; outer++) { + let outernr = this.stones[outer].number; if (outernr === 0) continue; - for (inner = outer + 1; inner < buttonsPerBoard; inner++) { - let innernr = this.buttons[inner].number; + for (inner = outer + 1; inner < stonesPerBoard; inner++) { + let innernr = this.stones[inner].number; if (innernr > 0 && outernr > innernr) inversions++; } } return inversions; } + // return whether the puzzle is solvable // see https://www.geeksforgeeks.org/check-instance-15-puzzle-solvable/ isSolvable() { let invs = this.getInversionCount(); - if (buttonsPerLine % 2 !== 0) // odd number of rows/columns + if (stonesPerLine % 2 !== 0) // odd number of rows/columns return (invs % 2 === 0); else { return ((invs + this.rowOf0()) % 2 !== 0); } } + // draw the result field, pass null as argument if not called from worker drawResult(worker) { let field = this.resultField; @@ -478,9 +576,9 @@ class Board { g.setColor(0, 1, 0); else g.setColor(1, 0, 0); - g.fillRect(field.left, field.top, field.left + buttonsize, field.top + buttonsize); + g.fillRect(field.left, field.top, field.left + stonesize, field.top + stonesize); g.setColor(0, 0, 0); - g.drawRect(field.left, field.top, field.left + buttonsize, field.top + buttonsize); + g.drawRect(field.left, field.top, field.left + stonesize, field.top + stonesize); g.setFont("Vector", 14).setFontAlign(0, 0).drawString(this.moveCount, field.centerx, field.centery); if (worker !== null) worker.endTask(); @@ -489,21 +587,29 @@ class Board { gameEnd(this.moveCount); }, 500); } + // draws the menu button drawMenu() { let field = this.menuField; g.setColor(0.5, 0.5, 0.5); - g.fillRect(field.left, field.top, field.left + buttonsize, field.top + buttonsize); + g.fillRect(field.left, field.top, field.left + stonesize, field.top + stonesize); g.setColor(0, 0, 0); - g.drawRect(field.left, field.top, field.left + buttonsize, field.top + buttonsize); + g.drawRect(field.left, field.top, field.left + stonesize, field.top + stonesize); let l = field.left + 8; - let r = field.left + buttonsize - 8; + let r = field.left + stonesize - 8; let t = field.top + 5; for (i = 0; i < 3; i++) g.fillRect(l, t + (i * 6), r, t + (i * 6) + 2); } + } + +// *** Global helper methods + +// draw some text with some surrounding to increase contrast +// text is drawn at given (x,y) position with textcol. +// frame is drawn 2 pixels around (x,y) in each direction in framecol. function framedText(text, x, y, textcol, framecol) { g.setColor(framecol); for (i = -2; i < 3; i++) @@ -515,36 +621,32 @@ function framedText(text, x, y, textcol, framecol) { g.setColor(textcol).drawString(text, x, y); } +// Show the splash screen at program start, call afterSplash afterwards. +// If spash mode is "off", call afterSplash directly. function showSplash(afterSplash) { - g.reset(); - g.drawImage(introscreen, 0, 0); - setTimeout(() => { - g.setFont("Vector", 40).setFontAlign(0, 0); - framedText("15", g.getWidth() / 2, g.getHeight() / 2 - g.getFontHeight() * 0.66, "#f00", "#fff"); + if (splashMode === "off") + afterSplash(); + else { + g.reset(); + g.drawImage(introscreen, 0, 0); setTimeout(() => { g.setFont("Vector", 40).setFontAlign(0, 0); - framedText("Puzzle", g.getWidth() / 2, g.getHeight() / 2 + g.getFontHeight() * 0.66, "#f00", "#fff"); - setTimeout(afterSplash, 2000); - }, 1000); - }, 2000); + framedText("15", g.getWidth() / 2, g.getHeight() / 2 - g.getFontHeight() * 0.66, "#f00", "#fff"); + setTimeout(() => { + g.setFont("Vector", 40).setFontAlign(0, 0); + framedText("Puzzle", g.getWidth() / 2, g.getHeight() / 2 + g.getFontHeight() * 0.66, "#f00", "#fff"); + setTimeout(afterSplash, (splashMode === "long" ? 2000 : 1000)); + }, (splashMode === "long" ? 1000 : 1)); + }, (splashMode === "long" ? 2000 : 1000)); + } } -// *** Main program -g.reset(); - -// We need a worker... -var worker = new Worker(); - -setButtonsPerLine(3); -// ...and the board -var board = new Board(); - -var dragger; +// *** Global flow control // Initialize the game with an explicit number of stones per line function initGame(bpl) { - setButtonsPerLine(bpl); + setStonesPreLine(bpl); newGame(); } @@ -564,7 +666,7 @@ function continueGame() { // Show message on game end, allows to restart new game function gameEnd(moveCount) { dragger.setEnabled(false); - E.showPrompt("You solved the\n" + buttonsPerLine + "x" + buttonsPerLine + " puzzle in\n" + moveCount + " move" + (moveCount === 1 ? "" : "s") + ".", { + E.showPrompt("You solved the\n" + stonesPerLine + "x" + stonesPerLine + " puzzle in\n" + moveCount + " move" + (moveCount === 1 ? "" : "s") + ".", { title: "Puzzle solved", buttons: { "Again": newGame, @@ -577,6 +679,17 @@ function gameEnd(moveCount) { }); } +// A tiny about screen +function showAbout(doContinue) { + E.showAlert("Author: Dirk Hillbrecht\nLicense: MIT", "Puzzle15").then(() => { + if (doContinue) + continueGame(); + else + showMenu(false); + }); +} + +// Show the in-game menu allowing to start a new game function showMenu(withContinue) { var mainmenu = { "": { @@ -588,18 +701,20 @@ function showMenu(withContinue) { mainmenu["Start 3x3"] = () => initGame(3); mainmenu["Start 4x4"] = () => initGame(4); mainmenu["Start 5x5"] = () => initGame(5); + mainmenu.About = () => showAbout(withContinue); mainmenu.Exit = () => load(); dragger.setEnabled(false); g.clear(true); E.showMenu(mainmenu); } +// Handle a "click" event (only needed for menu button) function handleclick(e) { if (board.menuField.contains(e.x, e.y)) setTimeout(() => showMenu(true), 10); } -// Handle a drag event +// Handle a drag event (moving the stones around) function handledrag(e) { worker.addTask(Math.abs(e.dx) > Math.abs(e.dy) ? (e.dx > 0 ? e => board.moveRight(e) : e => board.moveLeft(e)) : @@ -607,18 +722,44 @@ function handledrag(e) { worker.addTask(e => board.drawResult(e)); } -dragger = new Dragger(handleclick, handledrag, clickThreshold, dragThreshold); -// Start the interaction +// *** Main program + +g.clear(true); + +// Load global app settings +loadSettings(); + +// We need a worker... +var worker = new Worker(); + +// Board will be initialized after the splash screen has been shown +var board; + +// Dragger is needed for interaction during the game +var dragger = new Dragger(handleclick, handledrag, clickThreshold, dragThreshold); + +// Disable dragger as board is not yet initialized +dragger.setEnabled(false); + +// Nevertheless attach it so that it is ready once the game starts dragger.attach(); +// Start the game by handling the splash screen sequence showSplash(() => { // Clock mode allows short-press on button to exit Bangle.setUI("clock"); // Load widgets Bangle.loadWidgets(); Bangle.drawWidgets(); - showMenu(false); -}, 5000); + if (startWith === "3x3") + initGame(3); + else if (startWith === "4x4") + initGame(4); + else if (startWith === "5x5") + initGame(5); + else + showMenu(false); +}); -// end of file \ No newline at end of file +// end of file diff --git a/apps/puzzle15/puzzle15.settings.js b/apps/puzzle15/puzzle15.settings.js new file mode 100644 index 000000000..352ec4315 --- /dev/null +++ b/apps/puzzle15/puzzle15.settings.js @@ -0,0 +1,50 @@ +// Settings menu for the Puzzle15 app + +(function(back) { + var FILE = "puzzle15.json"; + // Load settings + var settings = Object.assign({ + splashMode: "long", + startWith: "4x4" + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + // Helper method which uses int-based menu item for set of string values + function stringItems(startvalue, writer, values) { + return { + value: (startvalue === undefined ? 0 : values.indexOf(startvalue)), + format: v => values[v], + min: 0, + max: values.length - 1, + wrap: true, + step: 1, + onchange: v => { + writer(values[v]); + writeSettings(); + } + }; + } + + // Helper method which breaks string set settings down to local settings object + function stringInSettings(name, values) { + return stringItems(settings[name], v => settings[name] = v, values); + } + + var mainmenu = { + "": { + "title": "15 Puzzle" + }, + "< Back": () => back(), + "Splash": stringInSettings("splashMode", ["long", "short", "off"]), + "Start with": stringInSettings("startWith", ["3x3", "4x4", "5x5", "menu"]) + }; + + // Actually display the menu + E.showMenu(mainmenu); + +}); + +// end of file \ No newline at end of file From cabb0c2d7d3529da1aa9810afbdb94678633aa84 Mon Sep 17 00:00:00 2001 From: "Dirk Hillbrecht (home)" Date: Wed, 5 Jan 2022 09:52:06 +0100 Subject: [PATCH 6/6] Puzzle15: v0.05: Central game end function --- apps.json | 2 +- apps/puzzle15/ChangeLog | 1 + apps/puzzle15/puzzle15.app.js | 20 +++++++++++++------- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/apps.json b/apps.json index 3a390938d..c213fdaa6 100644 --- a/apps.json +++ b/apps.json @@ -5393,7 +5393,7 @@ { "id": "puzzle15", "name": "15 puzzle", - "version": "0.04", + "version": "0.05", "description": "A 15 puzzle game with drag gesture interface", "readme":"README.md", "icon": "puzzle15.app.png", diff --git a/apps/puzzle15/ChangeLog b/apps/puzzle15/ChangeLog index bd6af53d8..0950b7ae0 100644 --- a/apps/puzzle15/ChangeLog +++ b/apps/puzzle15/ChangeLog @@ -2,3 +2,4 @@ 0.02: Lots of enhancements, menu system not yet functional, but packaging should be now... 0.03: Menu logic now generally functioning, splash screen added. The first really playable version! 0.04: Settings dialog, about screen +0.05: Central game end function diff --git a/apps/puzzle15/puzzle15.app.js b/apps/puzzle15/puzzle15.app.js index e66f9b9bb..eec21933c 100644 --- a/apps/puzzle15/puzzle15.app.js +++ b/apps/puzzle15/puzzle15.app.js @@ -391,23 +391,23 @@ class Board { //this.setAlmostSolved(); // to test the game end } - /* - // Set the board into the "solved" position. Useful for showcasing + /* Set the board into the "solved" position. Useful for showcasing and development setSolved() { this.stones = []; for (i = 0; i < stonesPerBoard; i++) this.stones[i] = new Stone((i + 1) % stonesPerBoard, i); this.moveCount = 0; } + /* */ - // Initialize an almost solved playing field. Useful for tests and development + /* Initialize an almost solved playing field. Useful for tests and development setAlmostSolved() { this.setSolved(); b = this.stones[this.stones.length - 1]; this.stones[this.stones.length - 1] = this.stones[this.stones.length - 2]; this.stones[this.stones.length - 2] = b; } - */ + /* */ // Initialize a shuffled field. The fields are always solvable. setShuffled() { @@ -671,7 +671,7 @@ function gameEnd(moveCount) { buttons: { "Again": newGame, "Menu": () => showMenu(false), - "Exit": load + "Exit": exitGame } }).then(v => { E.showPrompt(); @@ -702,7 +702,7 @@ function showMenu(withContinue) { mainmenu["Start 4x4"] = () => initGame(4); mainmenu["Start 5x5"] = () => initGame(5); mainmenu.About = () => showAbout(withContinue); - mainmenu.Exit = () => load(); + mainmenu.Exit = exitGame; dragger.setEnabled(false); g.clear(true); E.showMenu(mainmenu); @@ -722,6 +722,12 @@ function handledrag(e) { worker.addTask(e => board.drawResult(e)); } +// exit the game, clear screen first to prevent ghost images +function exitGame() { + g.clear(true); + setTimeout(load, 300); +} + // *** Main program @@ -762,4 +768,4 @@ showSplash(() => { showMenu(false); }); -// end of file +// end of file \ No newline at end of file