2023-07-28 05:46:26 +00:00
// basic shapes
const SPACE = 0 ;
const WALL = 1 ;
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 ) ;
}
}
}
2023-08-24 08:56:59 +00:00
if ( offsets . length == 1 ) {
offsets . push ( raw _maps . length ) ;
}
2023-07-28 05:46:26 +00:00
return offsets ;
}
let config = s . readJSON ( "sokoban.json" , true ) ;
if ( config === undefined ) {
2023-08-20 15:34:49 +00:00
let initial _offsets = next _map _offsets ( "sokoban.microban.sok" , 0 ) ;
2023-07-28 05:46:26 +00:00
config = {
2023-08-20 15:34:49 +00:00
levels _sets : [ "sokoban.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 ] ;
2023-08-24 08:56:59 +00:00
let set _name = set _filename . substring ( 8 , set _filename . length - 4 ) ; // remove '.txt' and 'sokoban.'
2023-07-28 05:46:26 +00:00
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 ] ;
2023-08-24 08:56:59 +00:00
let won = false ;
2023-07-28 05:46:26 +00:00
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)
2023-08-24 08:56:59 +00:00
if ( new _offsets . length != 2 ) {
won = true ;
2023-07-28 05:46:26 +00:00
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 ] ) ;
}
}
2023-08-24 08:56:59 +00:00
if ( ! won ) {
config . current _maps [ current _set ] ++ ;
s . writeJSON ( "sokoban.json" , config ) ;
load _current _map ( ) ;
}
2023-07-28 05:46:26 +00:00
}
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 ( ) ;