+
+
+
+
+
+
diff --git a/apps/presentor/settings.json b/apps/presentor/settings.json
new file mode 100644
index 000000000..398bf1332
--- /dev/null
+++ b/apps/presentor/settings.json
@@ -0,0 +1 @@
+{"pparts":[{"subject":"#1","minutes":10,"seconds":0,"notes":"This is a note."},{"subject":"#2","minutes":2,"seconds":50,"notes":"Change in the app!"}],"sversion":2.2}
\ No newline at end of file
diff --git a/apps/puzzle15/ChangeLog b/apps/puzzle15/ChangeLog
new file mode 100644
index 000000000..0950b7ae0
--- /dev/null
+++ b/apps/puzzle15/ChangeLog
@@ -0,0 +1,5 @@
+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
+0.05: Central game end function
diff --git a/apps/puzzle15/README.md b/apps/puzzle15/README.md
new file mode 100644
index 000000000..16c0c4593
--- /dev/null
+++ b/apps/puzzle15/README.md
@@ -0,0 +1,57 @@
+# Puzzle15 - A 15-puzzle for the Bangle.js 2
+
+This is a Bangle.js 2 adoption of the famous 15 puzzle.
+
+## The game
+
+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
+
+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.
+
+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 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.
+
+## The main menu
+
+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/introscreen.png b/apps/puzzle15/introscreen.png
new file mode 100644
index 000000000..766cd61fa
Binary files /dev/null and b/apps/puzzle15/introscreen.png differ
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..eec21933c
--- /dev/null
+++ b/apps/puzzle15/puzzle15.app.js
@@ -0,0 +1,771 @@
+// A 15-puzzle game for the Bangle.js 2 clock
+// (C) Dirk Hillbrecht 2022
+// Released unter the terms of the MIT license
+
+// 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 stone move animation
+const animationSteps = 6;
+
+// Milliseconds to wait between move animation steps
+const animationWaitMillis = 30;
+
+// 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;
+
+
+// *** 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;
+
+// Actual top start of the playing field (so that it is centered)
+var topstart;
+
+// Number of stones on the board (needed at several occasions)
+var stonesPerBoard;
+
+// 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
+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;
+ let 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);
+ }
+
+}
+
+// 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.
+// 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 + 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 + 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 % stonesPerLine) * (stonesize + 2),
+ topstart + (Math.floor(index / stonesPerLine)) * (stonesize + 2));
+
+ }
+ // Special field for the result "stone"
+ static forResult() {
+ return new Field(leftstart + (stonesPerLine * (stonesize + 2)),
+ topstart + ((stonesPerLine - 1) * (stonesize + 2)));
+ }
+
+ // Special field for the menu
+ static forMenu() {
+ return new Field(leftstart + (stonesPerLine * (stonesize + 2)),
+ topstart);
+ }
+
+}
+
+// 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 + (stonesPerLine % 2 == 0 ? (Math.floor((number - 1) / stonesPerLine)) : 0)) % 2 == 0) {
+ // Black stone
+ this.draw = function(field) {
+ 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", (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;
+ }
+}
+
+// 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 + 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) {
+ 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
+class Board {
+
+ // 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 < 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 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
+ 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() {
+ let nrs = []; // numbers of the stones
+ for (i = 0; i < stonesPerBoard; i++)
+ nrs[i] = 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 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];
+ count -= 1;
+ }
+ // 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.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.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 % 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 % 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 >= 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 < (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.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 / stonesPerLine);
+ }
+
+ // 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) {
+ worker.endTask();
+ return;
+ }
+ let startidx = startfunc(endidx); // Start field (relative to the gap)
+ if (startidx === -1) {
+ worker.endTask();
+ return;
+ }
+ // 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 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.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 < stonesPerBoard - 1; outer++) {
+ let outernr = this.stones[outer].number;
+ if (outernr === 0)
+ continue;
+ 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 (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;
+ let solved = this.isSolved();
+ if (solved)
+ g.setColor(0, 1, 0);
+ else
+ g.setColor(1, 0, 0);
+ 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 + stonesize, field.top + stonesize);
+ g.setFont("Vector", 14).setFontAlign(0, 0).drawString(this.moveCount, field.centerx, field.centery);
+ if (worker !== null)
+ worker.endTask();
+ if (solved)
+ setTimeout(() => {
+ 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 + stonesize, field.top + stonesize);
+ g.setColor(0, 0, 0);
+ g.drawRect(field.left, field.top, field.left + stonesize, field.top + stonesize);
+ let l = field.left + 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++)
+ 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);
+}
+
+// Show the splash screen at program start, call afterSplash afterwards.
+// If spash mode is "off", call afterSplash directly.
+function showSplash(afterSplash) {
+ if (splashMode === "off")
+ afterSplash();
+ else {
+ 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, (splashMode === "long" ? 2000 : 1000));
+ }, (splashMode === "long" ? 1000 : 1));
+ }, (splashMode === "long" ? 2000 : 1000));
+ }
+}
+
+
+// *** Global flow control
+
+// Initialize the game with an explicit number of stones per line
+function initGame(bpl) {
+ setStonesPreLine(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);
+}
+
+// Show message on game end, allows to restart new game
+function gameEnd(moveCount) {
+ dragger.setEnabled(false);
+ E.showPrompt("You solved the\n" + stonesPerLine + "x" + stonesPerLine + " puzzle in\n" + moveCount + " move" + (moveCount === 1 ? "" : "s") + ".", {
+ title: "Puzzle solved",
+ buttons: {
+ "Again": newGame,
+ "Menu": () => showMenu(false),
+ "Exit": exitGame
+ }
+ }).then(v => {
+ E.showPrompt();
+ setTimeout(v, 10);
+ });
+}
+
+// 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 = {
+ "": {
+ "title": "15 Puzzle"
+ }
+ };
+ if (withContinue)
+ mainmenu.Continue = continueGame;
+ mainmenu["Start 3x3"] = () => initGame(3);
+ mainmenu["Start 4x4"] = () => initGame(4);
+ mainmenu["Start 5x5"] = () => initGame(5);
+ mainmenu.About = () => showAbout(withContinue);
+ mainmenu.Exit = exitGame;
+ 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 (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)) :
+ (e.dy > 0 ? e => board.moveDown(e) : e => board.moveUp(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
+
+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();
+ 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
diff --git a/apps/puzzle15/puzzle15.app.png b/apps/puzzle15/puzzle15.app.png
new file mode 100644
index 000000000..f95366a46
Binary files /dev/null and b/apps/puzzle15/puzzle15.app.png differ
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
diff --git a/apps/puzzle15/screenshot.png b/apps/puzzle15/screenshot.png
new file mode 100644
index 000000000..3a79a3ae6
Binary files /dev/null and b/apps/puzzle15/screenshot.png differ
diff --git a/apps/weather/ChangeLog b/apps/weather/ChangeLog
index 910cd4658..101da48e1 100644
--- a/apps/weather/ChangeLog
+++ b/apps/weather/ChangeLog
@@ -11,3 +11,4 @@
0.12: Allow hiding the widget
0.13: Tweak Bangle.js 2 light theme colors
0.14: Use weather condition code for icon selection
+0.15: Fix widget icon
diff --git a/apps/weather/lib.js b/apps/weather/lib.js
index 8afdfe6df..1d48116e1 100644
--- a/apps/weather/lib.js
+++ b/apps/weather/lib.js
@@ -53,6 +53,16 @@ exports.get = function() {
scheduleExpiry(storage.readJSON('weather.json')||{});
+/**
+ *
+ * @param cond Weather condition, as one of:
+ * {number} code: (Preferred form) https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2
+ * {string} weather description (in English: breaks for other languages!)
+ * {object} use cond.code if present, or fall back to cond.txt
+ * @param x Left
+ * @param y Top
+ * @param r Icon Size
+ */
exports.drawIcon = function(cond, x, y, r) {
var palette;
@@ -249,32 +259,35 @@ exports.drawIcon = function(cond, x, y, r) {
g.setColor(g.theme.fg).setFontAlign(0, 0).setFont('Vector', r*2).drawString("?", x+r/10, y+r/6);
}
- function chooseIcon(condition) {
- if (!condition) return () => {};
- condition = condition.toLowerCase();
- if (condition.includes("thunderstorm")) return drawThunderstorm;
- if (condition.includes("freezing")||condition.includes("snow")||
- condition.includes("sleet")) {
+ /*
+ * Choose weather icon to display based on weather description
+ */
+ function chooseIconByTxt(txt) {
+ if (!txt) return () => {};
+ txt = txt.toLowerCase();
+ if (txt.includes("thunderstorm")) return drawThunderstorm;
+ if (txt.includes("freezing")||txt.includes("snow")||
+ txt.includes("sleet")) {
return drawSnow;
}
- if (condition.includes("drizzle")||
- condition.includes("shower")) {
+ if (txt.includes("drizzle")||
+ txt.includes("shower")) {
return drawRain;
}
- if (condition.includes("rain")) return drawShowerRain;
- if (condition.includes("clear")) return drawSun;
- if (condition.includes("few clouds")) return drawFewClouds;
- if (condition.includes("scattered clouds")) return drawCloud;
- if (condition.includes("clouds")) return drawBrokenClouds;
- if (condition.includes("mist") ||
- condition.includes("smoke") ||
- condition.includes("haze") ||
- condition.includes("sand") ||
- condition.includes("dust") ||
- condition.includes("fog") ||
- condition.includes("ash") ||
- condition.includes("squalls") ||
- condition.includes("tornado")) {
+ if (txt.includes("rain")) return drawShowerRain;
+ if (txt.includes("clear")) return drawSun;
+ if (txt.includes("few clouds")) return drawFewClouds;
+ if (txt.includes("scattered clouds")) return drawCloud;
+ if (txt.includes("clouds")) return drawBrokenClouds;
+ if (txt.includes("mist") ||
+ txt.includes("smoke") ||
+ txt.includes("haze") ||
+ txt.includes("sand") ||
+ txt.includes("dust") ||
+ txt.includes("fog") ||
+ txt.includes("ash") ||
+ txt.includes("squalls") ||
+ txt.includes("tornado")) {
return drawMist;
}
return drawUnknown;
@@ -298,7 +311,6 @@ exports.drawIcon = function(cond, x, y, r) {
case 531: return drawShowerRain;
default: return drawRain;
}
- break;
case 6: return drawSnow;
case 7: return drawMist;
case 8:
@@ -308,16 +320,21 @@ exports.drawIcon = function(cond, x, y, r) {
case 802: return drawCloud;
default: return drawBrokenClouds;
}
- break;
default: return drawUnknown;
}
}
- if (cond.code && cond.code > 0) {
- chooseIconByCode(cond.code)(x, y, r);
- } else {
- chooseIcon(cond.txt)(x, y, r);
+ function chooseIcon(cond) {
+ if (typeof (cond)==="object") {
+ if ("code" in cond) return chooseIconByCode(cond.code);
+ if ("txt" in cond) return chooseIconByTxt(cond.txt);
+ } else if (typeof (cond)==="number") {
+ return chooseIconByCode(cond.code);
+ } else if (typeof (cond)==="string") {
+ return chooseIconByTxt(cond.txt);
+ }
+ return drawUnknown;
}
-
+ chooseIcon(cond)(x, y, r);
};
diff --git a/apps/weather/widget.js b/apps/weather/widget.js
index f2ddf0b5b..2905d776b 100644
--- a/apps/weather/widget.js
+++ b/apps/weather/widget.js
@@ -52,8 +52,8 @@
if (!w) return;
g.reset();
g.clearRect(this.x, this.y, this.x+this.width-1, this.y+23);
- if (w.txt) {
- weather.drawIcon(w.txt, this.x+10, this.y+8, 7.5);
+ if (w.code||w.txt) {
+ weather.drawIcon(w, this.x+10, this.y+8, 7.5);
}
if (w.temp) {
let t = require('locale').temp(w.temp-273.15); // applies conversion