mirror of https://github.com/espruino/BangleApps
464 lines
18 KiB
JavaScript
464 lines
18 KiB
JavaScript
// basic shapes
|
|
const SPACE = 0;
|
|
const WALL = 1;
|
|
const PLAYER = 2;
|
|
const BOX = 3;
|
|
const HOLE = 4;
|
|
const FILLED = 5;
|
|
|
|
// basic directions
|
|
const LEFT = 0;
|
|
const UP = 1;
|
|
const DOWN = 2;
|
|
const RIGHT = 3;
|
|
|
|
function go(line, column, direction) {
|
|
let destination_line = line;
|
|
let destination_column = column;
|
|
if (direction == LEFT) {
|
|
destination_column -= 1;
|
|
} else if (direction == RIGHT) {
|
|
destination_column += 1;
|
|
} else if (direction == UP) {
|
|
destination_line -= 1;
|
|
} else {
|
|
// direction is down
|
|
destination_line += 1;
|
|
}
|
|
return [destination_line, destination_column];
|
|
}
|
|
|
|
Bangle.setOptions({
|
|
lockTimeout: 60000,
|
|
backlightTimeout: 60000,
|
|
});
|
|
|
|
let s = require("Storage");
|
|
|
|
// parse the levels a bit more to figure offsets delimiting next map.
|
|
function next_map_offsets(filename, start_offset) {
|
|
let raw_maps = s.readArrayBuffer(filename);
|
|
let offsets = [];
|
|
// this is a very dumb parser : map starts three chars after the end of a line with a ';'
|
|
// and ends two chars before next ';'
|
|
let comment_line = true;
|
|
for (let i = start_offset; i < raw_maps.length; i++) {
|
|
if (raw_maps[i] == 59) { // ';'
|
|
if (offsets.length != 0) {
|
|
offsets.push(i - 2);
|
|
return offsets;
|
|
}
|
|
comment_line = true;
|
|
} else if (raw_maps[i] == 10) { // '\n'
|
|
if (comment_line) {
|
|
comment_line = false;
|
|
offsets.push(i + 3);
|
|
}
|
|
}
|
|
}
|
|
return offsets;
|
|
}
|
|
|
|
let config = s.readJSON("sokoban.json", true);
|
|
if (config === undefined) {
|
|
let initial_offsets = next_map_offsets("Microban.txt", 0);
|
|
config = {
|
|
levels_sets: ["Microban.txt"], // all known files containing levels
|
|
levels_set: 0, // which set are we using ?
|
|
current_maps: [0], // what is current map on each set ?
|
|
offsets: [initial_offsets], // known offsets for each levels set (binary positions of maps in each file)
|
|
};
|
|
s.writeJSON("sokoban.json", config);
|
|
}
|
|
|
|
let map = null;
|
|
let in_menu = false;
|
|
let history = null; // store history to allow undos
|
|
|
|
|
|
function load_map(filename, start_offset, end_offset, name) {
|
|
console.log("loading map in", filename, "between", start_offset, "and", end_offset);
|
|
let raw_map = new Uint8Array(s.readArrayBuffer(filename), start_offset, end_offset - start_offset);
|
|
let dimensions = map_dimensions(raw_map);
|
|
history = [];
|
|
return new Map(dimensions, raw_map, filename, name);
|
|
}
|
|
|
|
function load_current_map() {
|
|
let current_set = config.levels_set;
|
|
let offsets = config.offsets[current_set];
|
|
let set_filename = config.levels_sets[current_set];
|
|
let set_name = set_filename.substring(0, set_filename.length - 4); // remove '.txt'
|
|
let current_map = config.current_maps[current_set];
|
|
map = load_map(set_filename, offsets[2 * current_map], offsets[2 * current_map + 1], set_name + " " + (current_map + 1));
|
|
map.display();
|
|
}
|
|
|
|
function next_map() {
|
|
let current_set = config.levels_set;
|
|
let current_map = config.current_maps[current_set];
|
|
let offsets = config.offsets[current_set];
|
|
if (2 * (current_map + 1) >= offsets.length) {
|
|
// we parse some new offsets
|
|
let new_offsets = next_map_offsets(config.levels_sets[current_set], offsets[offsets.length - 1] + 2); // +2 since we need to start at ';' (we did -2 from ';' in previous parser call)
|
|
if (new_offsets.length == 0) {
|
|
E.showAlert("You Win", "All levels completed").then(function() {
|
|
load();
|
|
});
|
|
} else {
|
|
config.offsets[current_set].push(new_offsets[0]);
|
|
config.offsets[current_set].push(new_offsets[1]);
|
|
}
|
|
}
|
|
config.current_maps[current_set]++;
|
|
s.writeJSON("sokoban.json", config);
|
|
load_current_map();
|
|
}
|
|
|
|
function previous_map() {
|
|
let current_set = config.levels_set;
|
|
let current_map = config.current_maps[current_set];
|
|
if (current_map > 0) {
|
|
current_map--;
|
|
config.current_maps[current_set] = current_map;
|
|
s.writeJSON("sokoban.json", config);
|
|
load_current_map();
|
|
}
|
|
}
|
|
|
|
function map_dimensions(raw_map) {
|
|
let line_start = 0;
|
|
let width = 0;
|
|
let height = 0;
|
|
for (let i = 0; i < raw_map.length; i++) {
|
|
if (raw_map[i] == 10) {
|
|
height += 1;
|
|
let line_width = i - line_start;
|
|
if (i > 0 && raw_map[i - 1] == 13) {
|
|
line_width -= 1; // remove \r
|
|
}
|
|
width = Math.max(line_width, width);
|
|
line_start = i + 1;
|
|
}
|
|
}
|
|
return [width, height];
|
|
}
|
|
|
|
class Map {
|
|
constructor(dimensions, raw_map, filename, name) {
|
|
this.filename = filename;
|
|
this.name = name;
|
|
this.width = dimensions[0];
|
|
this.height = dimensions[1];
|
|
this.remaining_holes = 0;
|
|
// start by creating an empty map
|
|
this.m = [];
|
|
for (let i = 0; i < this.height; i++) {
|
|
let line = new Uint8Array(this.width);
|
|
for (let j = 0; j < this.width; j++) {
|
|
line[j] = SPACE;
|
|
}
|
|
this.m.push(line);
|
|
}
|
|
// now fill with raw_map's content
|
|
let current_line = 0;
|
|
let line_start = 0;
|
|
for (let i = 0; i < raw_map.length; i++) {
|
|
if (raw_map[i] == 32) {
|
|
this.m[current_line][i - line_start] = SPACE;
|
|
} else if (raw_map[i] == 43) {
|
|
// '+'
|
|
this.remaining_holes += 1;
|
|
this.m[current_line][i - line_start] = HOLE;
|
|
this.player_column = i - line_start;
|
|
this.player_line = current_line;
|
|
} else if (raw_map[i] == 10) {
|
|
current_line += 1;
|
|
line_start = i + 1;
|
|
} else if (raw_map[i] == 35) {
|
|
this.m[current_line][i - line_start] = WALL;
|
|
} else if (raw_map[i] == 36) {
|
|
this.m[current_line][i - line_start] = BOX;
|
|
} else if (raw_map[i] == 46) {
|
|
this.remaining_holes += 1;
|
|
this.m[current_line][i - line_start] = HOLE;
|
|
} else if (raw_map[i] == 64) {
|
|
this.m[current_line][i - line_start] = SPACE;
|
|
this.player_column = i - line_start;
|
|
this.player_line = current_line;
|
|
} else if (raw_map[i] == 42) {
|
|
this.m[current_line][i - line_start] = FILLED;
|
|
} else if (raw_map[i] != 13) {
|
|
console.log("warning unknown map content", raw_map[i]);
|
|
}
|
|
}
|
|
this.steps = 0;
|
|
this.calibrate();
|
|
}
|
|
// compute scale
|
|
calibrate() {
|
|
let r = Bangle.appRect;
|
|
let rwidth = 1 + r.x2 - r.x;
|
|
let rheight = 1 + r.y2 - r.y;
|
|
let cell_width = Math.floor(rwidth / this.width);
|
|
let cell_height = Math.floor(rheight / this.height);
|
|
let cell_scale = Math.min(cell_width, cell_height); // we want square cells
|
|
let real_width = this.width * cell_scale;
|
|
let real_height = this.height * cell_scale;
|
|
let sx = r.x + Math.ceil((rwidth - real_width) / 2);
|
|
let sy = r.y + Math.ceil((rheight - real_height) / 2);
|
|
this.sx = sx;
|
|
this.sy = sy;
|
|
this.cell_scale = cell_scale;
|
|
}
|
|
undo(direction, pushing) {
|
|
this.steps -= 1;
|
|
|
|
let previous_position = go(this.player_line, this.player_column, 3 - direction);
|
|
let previous_line = previous_position[0];
|
|
let previous_column = previous_position[1];
|
|
|
|
if (pushing) {
|
|
// put the box back on current player position
|
|
let currently_on = this.m[this.player_line][this.player_column];
|
|
if (currently_on == HOLE) {
|
|
this.remaining_holes -= 1;
|
|
this.m[this.player_line][this.player_column] = FILLED;
|
|
} else {
|
|
this.m[this.player_line][this.player_column] = BOX;
|
|
}
|
|
// now, remove the box from its current position
|
|
let current_box_position = go(this.player_line, this.player_column, direction);
|
|
let box_line = current_box_position[0];
|
|
let box_column = current_box_position[1];
|
|
let box_on = this.m[box_line][box_column];
|
|
if (box_on == FILLED) {
|
|
this.remaining_holes += 1;
|
|
this.m[box_line][box_column] = HOLE;
|
|
} else {
|
|
this.m[box_line][box_column] = SPACE;
|
|
}
|
|
this.display_cell(box_line, box_column);
|
|
}
|
|
// cancel player display
|
|
this.display_cell(this.player_line, this.player_column);
|
|
// re-display player at previous position
|
|
this.player_line = previous_line;
|
|
this.player_column = previous_column;
|
|
this.display_player();
|
|
}
|
|
move(direction) {
|
|
let destination_position = go(this.player_line, this.player_column, direction);
|
|
let destination_line = destination_position[0];
|
|
let destination_column = destination_position[1];
|
|
let destination = this.m[destination_line][destination_column];
|
|
let pushing = false;
|
|
if (destination == BOX || destination == SPACE || destination == HOLE || destination == FILLED) {
|
|
if (destination == BOX || destination == FILLED) {
|
|
pushing = true;
|
|
let after_line = 2 * destination_line - this.player_line;
|
|
let after_column = 2 * destination_column - this.player_column;
|
|
let after = this.m[after_line][after_column];
|
|
let will_remain = SPACE;
|
|
if (destination == FILLED) {
|
|
will_remain = HOLE;
|
|
}
|
|
if (after == SPACE) {
|
|
if (will_remain == HOLE) {
|
|
this.remaining_holes += 1;
|
|
}
|
|
this.m[destination_line][destination_column] = will_remain;
|
|
this.m[after_line][after_column] = BOX;
|
|
} else if (after == HOLE) {
|
|
this.m[destination_line][destination_column] = will_remain;
|
|
this.m[after_line][after_column] = FILLED;
|
|
if (will_remain == SPACE) {
|
|
this.remaining_holes -= 1;
|
|
}
|
|
if (this.remaining_holes == 0) {
|
|
in_menu = true;
|
|
this.steps += 1;
|
|
E.showAlert("" + this.steps + "steps", "You Win").then(function() {
|
|
in_menu = false;
|
|
next_map();
|
|
});
|
|
return;
|
|
}
|
|
} else {
|
|
return;
|
|
}
|
|
this.display_cell(after_line, after_column);
|
|
this.display_cell(destination_line, destination_column);
|
|
}
|
|
history.push([direction, pushing]);
|
|
this.display_cell(this.player_line, this.player_column);
|
|
this.steps += 1;
|
|
this.player_line = destination_line;
|
|
this.player_column = destination_column;
|
|
this.display_player();
|
|
// this.display();
|
|
}
|
|
}
|
|
display_player() {
|
|
sx = this.sx;
|
|
sy = this.sy;
|
|
cell_scale = this.cell_scale;
|
|
g.setColor(0.8, 0.8, 0).fillCircle(sx + (0.5 + this.player_column) * cell_scale, sy + (0.5 + this.player_line) * cell_scale, cell_scale / 2 - 1); // -1 because otherwise it overfills
|
|
}
|
|
display_cell(line, column) {
|
|
sx = this.sx;
|
|
sy = this.sy;
|
|
cell_scale = this.cell_scale;
|
|
let shape = this.m[line][column];
|
|
if (shape == WALL) {
|
|
if (cell_scale < 10) {
|
|
g.setColor(1, 0, 0).fillRect(sx + column * cell_scale, sy + line * cell_scale, sx + (column + 1) * cell_scale, sy + (line + 1) * cell_scale);
|
|
} else {
|
|
g.setColor(0.5, 0.5, 0.5).fillRect(sx + column * cell_scale, sy + line * cell_scale, sx + (column + 1) * cell_scale, sy + (line + 1) * cell_scale);
|
|
g.setColor(1, 0, 0).fillRect(sx + column * cell_scale, sy + (line + 0.15) * cell_scale, sx + (column + 0.35) * cell_scale, sy + (line + 0.45) * cell_scale);
|
|
g.fillRect(sx + (column + 0.55) * cell_scale, sy + (line + 0.15) * cell_scale, sx + (column + 1) * cell_scale, sy + (line + 0.45) * cell_scale);
|
|
g.fillRect(sx + column * cell_scale, sy + (line + 0.65) * cell_scale, sx + (column + 0.65) * cell_scale, sy + (line + 0.95) * cell_scale);
|
|
g.fillRect(sx + (column + 0.85) * cell_scale, sy + (line + 0.65) * cell_scale, sx + (column + 1) * cell_scale, sy + (line + 0.95) * cell_scale);
|
|
}
|
|
} else if (shape == BOX) {
|
|
let border = Math.floor((cell_scale - 2) / 4);
|
|
if (border > 0) {
|
|
g.setColor(0.6, 0.4, 0.3).fillRect(sx + column * cell_scale + 1, sy + line * cell_scale + 1, sx + (column + 1) * cell_scale - 1, sy + (line + 1) * cell_scale - 1);
|
|
g.setColor(0.7, 0.5, 0.5).fillRect(sx + column * cell_scale + 1 + border, sy + line * cell_scale + 1 + border, sx + (column + 1) * cell_scale - 1 - border, sy + (line + 1) * cell_scale - 1 - border);
|
|
} else {
|
|
g.setColor(0.7, 0.5, 0.5).fillRect(sx + column * cell_scale + 1, sy + line * cell_scale + 1, sx + (column + 1) * cell_scale - 1, sy + (line + 1) * cell_scale - 1);
|
|
}
|
|
} else if (shape == HOLE) {
|
|
g.setColor(1, 1, 1).fillRect(sx + column * cell_scale, sy + line * cell_scale, sx + (column + 1) * cell_scale - 1, sy + (line + 1) * cell_scale - 1);
|
|
g.setColor(0, 0, 1).drawRect(sx + column * cell_scale, sy + line * cell_scale, sx + (column + 1) * cell_scale - 1, sy + (line + 1) * cell_scale - 1);
|
|
} else if (shape == FILLED) {
|
|
let border = Math.floor((cell_scale - 2) / 4);
|
|
if (border > 0) {
|
|
g.setColor(0.6, 0.4, 0.3).fillRect(sx + column * cell_scale + 1, sy + line * cell_scale + 1, sx + (column + 1) * cell_scale - 1, sy + (line + 1) * cell_scale - 1);
|
|
g.setColor(0, 0, 1).fillRect(sx + column * cell_scale + 1 + border, sy + line * cell_scale + 1 + border, sx + (column + 1) * cell_scale - 1 - border, sy + (line + 1) * cell_scale - 1 - border);
|
|
} else {
|
|
g.setColor(0, 0, 1).fillRect(sx + column * cell_scale + 1 + border, sy + line * cell_scale + 1 + border, sx + (column + 1) * cell_scale - 1 - border, sy + (line + 1) * cell_scale - 1 - border);
|
|
|
|
}
|
|
} else if (shape == SPACE) {
|
|
g.setColor(1, 1, 1).fillRect(sx + column * cell_scale, sy + line * cell_scale, sx + (column + 1) * cell_scale - 1, sy + (line + 1) * cell_scale - 1);
|
|
}
|
|
|
|
}
|
|
display() {
|
|
g.clear();
|
|
for (let line = 0; line < this.height; line++) {
|
|
for (let column = 0; column < this.width; column++) {
|
|
this.display_cell(line, column);
|
|
}
|
|
}
|
|
this.display_player();
|
|
g.setColor(0, 0, 0).setFont("6x8:2")
|
|
.setFontAlign(0, -1, 0)
|
|
.drawString(map.name, g.getWidth() / 2, 0);
|
|
}
|
|
}
|
|
|
|
|
|
Bangle.on('touch', function(button, xy) {
|
|
if (in_menu) {
|
|
return;
|
|
}
|
|
let half_width = g.getWidth() / 2;
|
|
let half_height = g.getHeight() / 2;
|
|
let directions_amplitudes = [0, 0, 0, 0];
|
|
directions_amplitudes[LEFT] = half_width - xy.x;
|
|
directions_amplitudes[RIGHT] = xy.x - half_width;
|
|
directions_amplitudes[UP] = half_height - xy.y;
|
|
directions_amplitudes[DOWN] = xy.y - half_height;
|
|
|
|
let max_direction;
|
|
let second_max_direction;
|
|
if (directions_amplitudes[0] > directions_amplitudes[1]) {
|
|
max_direction = 0;
|
|
second_max_direction = 1;
|
|
} else {
|
|
max_direction = 1;
|
|
second_max_direction = 0;
|
|
}
|
|
for (let direction = 2; direction < 4; direction++) {
|
|
if (directions_amplitudes[direction] > directions_amplitudes[max_direction]) {
|
|
second_max_direction = max_direction;
|
|
max_direction = direction;
|
|
} else if (directions_amplitudes[direction] >= directions_amplitudes[second_max_direction]) {
|
|
second_max_direction = direction;
|
|
}
|
|
}
|
|
if (directions_amplitudes[max_direction] - directions_amplitudes[second_max_direction] > 10) {
|
|
// if there is little possible confusions between two candidate moves let's move.
|
|
// basically we forbid diagonals of 10 pixels wide
|
|
map.move(max_direction);
|
|
}
|
|
|
|
});
|
|
|
|
Bangle.on('swipe', function(directionLR, directionUD) {
|
|
if (in_menu) {
|
|
return;
|
|
}
|
|
let last_move = history.pop();
|
|
if (last_move !== undefined) {
|
|
map.undo(last_move[0], last_move[1]);
|
|
}
|
|
});
|
|
|
|
setWatch(
|
|
function() {
|
|
if (in_menu) {
|
|
return;
|
|
}
|
|
in_menu = true;
|
|
const menu = {
|
|
"": {
|
|
title: "choose action"
|
|
},
|
|
"restart": function() {
|
|
E.showMenu();
|
|
load_current_map();
|
|
in_menu = false;
|
|
},
|
|
"current map": {
|
|
value: config.current_maps[config.levels_set] + 1,
|
|
min: 1,
|
|
max: config.offsets[config.levels_set].length / 2,
|
|
onchange: (v) => {
|
|
config.current_maps[config.levels_set] = v - 1;
|
|
load_current_map();
|
|
s.writeJSON("sokoban.json", config);
|
|
}
|
|
},
|
|
"next map": function() {
|
|
E.showMenu();
|
|
next_map();
|
|
in_menu = false;
|
|
},
|
|
"previous map": function() {
|
|
E.showMenu();
|
|
previous_map();
|
|
in_menu = false;
|
|
},
|
|
"back to game": function() {
|
|
E.showMenu();
|
|
g.clear();
|
|
map.display();
|
|
in_menu = false;
|
|
},
|
|
};
|
|
E.showMenu(menu);
|
|
},
|
|
BTN1, {
|
|
repeat: true
|
|
}
|
|
);
|
|
|
|
|
|
Bangle.setLocked(false);
|
|
|
|
current_map = config.current_map;
|
|
offsets = config.offsets;
|
|
load_current_map(); |