BangleApps/apps/sokoban/app.js

465 lines
18 KiB
JavaScript
Raw Normal View History

2023-07-28 05:46:26 +00:00
// 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) {
2023-08-17 07:11:23 +00:00
let initial_offsets = next_map_offsets("Microban.sok", 0);
2023-07-28 05:46:26 +00:00
config = {
2023-08-17 07:11:23 +00:00
levels_sets: ["Microban.sok"], // all known files containing levels
2023-07-28 05:46:26 +00:00
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;
2023-08-17 07:11:23 +00:00
load_current_map();