sokoban.js (6107B)
1 'use strict'; 2 3 4 console.log('sokoban.js start.'); 5 6 7 let sokoban = (function() { 8 let levels = [ 9 ` 10 ############## 11 #.@...*....z.# 12 ############## 13 `, 14 ` 15 ##### 16 #...# 17 #*..# 18 ###..*## 19 #..*.*.# 20 ###.#.##.# ###### 21 #...#.##.#####..zz# 22 #.*..*..........zz# 23 #####.###.#@##..zz# 24 #.....######### 25 ####### 26 `, 27 ]; 28 let level_win_test_not_all_endings = ` 29 ##### 30 #...# 31 #...# 32 ###...## 33 #......# 34 ###.#.##.# ###### 35 #...#.##.#####..zz# 36 #..z.*..........zz# 37 #####.###.#@##..zz# 38 #.....######### 39 ####### 40 `; 41 let level_win_test_all_endings = ` 42 ##### 43 #...# 44 #...# 45 ###...## 46 #......# 47 ###.#.##.# ###### 48 #...#.##.#####....# 49 #..z.*............# 50 #####.###.#@##....# 51 #.....######### 52 ####### 53 `; 54 55 function read_level_text(s) { 56 let state = { 57 rocks: [], 58 walls: [], 59 floors: [], 60 endings: [], 61 }; 62 let lines = s.split('\n'); 63 let max_x = Math.max(...lines.map(line=>line.length)); 64 for (let y = 0; y < lines.length; y++) { 65 state.walls[y] = []; 66 state.rocks[y] = []; 67 state.floors[y] = []; 68 state.endings[y] = []; 69 for (let x = 0; x < lines[y].length; x++) { 70 if (lines[y][x] == '@') { 71 state.player_position = [x, y]; 72 state.floors[y][x] = '.'; 73 } else if (lines[y][x] == '*') { 74 state.rocks[y][x] = '*'; 75 state.floors[y][x] = '.'; 76 } else if (lines[y][x] == '#') { 77 state.walls[y][x] = '#'; 78 } else if (lines[y][x] == 'z') { 79 state.endings[y][x] = 'z'; 80 } else if (lines[y][x] == '.') { 81 state.floors[y][x] = '.'; 82 }; 83 }; 84 }; 85 86 return state; 87 }; 88 89 function here_player(state, [x, y]) { 90 return ( 91 state.player_position[0] == x && 92 state.player_position[1] == y 93 ); 94 } 95 function here_wall(state, [x, y]) { 96 return state.walls[y][x] == '#'; 97 } 98 function here_rock(state, [x, y]) { 99 return state.rocks[y][x] == '*'; 100 } 101 function here_ending(state, [x, y]) { 102 return state.endings[y][x] == 'z'; 103 } 104 function here_floor(state, [x, y]) { 105 return state.floors[y][x] == '.'; 106 } 107 function move_rock(state, [fx, fy], [tx, ty]) { 108 delete state.rocks[fy][fx]; 109 state.rocks[ty][tx] = '*'; 110 } 111 112 113 function is_won(game) { 114 for (let y = 0; y < game.level_state.rocks.length; y++) { 115 if (game.level_state.rocks[y] != undefined) { 116 for (let x = 0; x < game.level_state.rocks[y].length; x++) { 117 if (here_rock(game.level_state, [x, y]) != here_ending(game.level_state, [x, y])) { 118 return false; 119 } 120 } 121 } 122 } 123 return true; 124 }; 125 126 127 function copy_level_state(game) { 128 let state_copy = [ 129 {}, 130 'rocks', 131 'floors', 132 'walls', 133 'endings', 134 ].reduce((acc, entity_name)=>{ 135 acc[entity_name] = ( 136 game 137 .level_state[entity_name] 138 .map(line=>line.map(c=>c)) 139 ); 140 return acc; 141 }); 142 state_copy.player_position = game.level_state.player_position.map(p=>p); 143 return state_copy; 144 }; 145 146 147 function action_step(game, direction) { 148 if (!is_won(game)) { 149 let p0 = game.level_state.player_position; 150 let p1 = [ 151 p0[0] + direction[0], 152 p0[1] + direction[1]]; 153 let p2 = [ 154 p1[0] + direction[0], 155 p1[1] + direction[1]]; 156 157 let is_blocked = ( 158 here_wall(game.level_state, p1) || ( 159 here_rock(game.level_state, p1) && ( 160 here_rock(game.level_state, p2) || 161 here_wall(game.level_state, p2)))); 162 let is_pushing = ( 163 here_rock(game.level_state, p1) && 164 !here_rock(game.level_state, p2)); 165 166 if (!is_blocked) { 167 game.number_of_steps += 1; 168 let old_state = copy_level_state(game); 169 game.undo_stack.push(old_state); 170 if (is_pushing) { 171 move_rock(game.level_state, p1, p2); 172 }; 173 game.level_state.player_position = p1; 174 }; 175 }; 176 }; 177 178 function action_step_up(game) { action_step(game, [0, -1]); }; 179 function action_step_down(game) { action_step(game, [0, 1]); }; 180 function action_step_left(game) { action_step(game, [-1, 0]); }; 181 function action_step_right(game) { action_step(game, [1, 0]); }; 182 183 184 function action_next_level(game) { 185 if (is_won(game) && game.current_level_number < levels.length) { 186 game.number_of_steps = 0; 187 game.undo_stack = []; 188 game.current_level_number += 1; 189 game.level_state = read_level_text(levels[game.current_level_number]); 190 }; 191 }; 192 193 194 function action_restart_level(game) { 195 game.number_of_steps = 0; 196 game.undo_stack = []; 197 game.level_state = read_level_text(levels[game.current_level_number]); 198 }; 199 200 201 function action_undo(game) { 202 if (game.undo_stack.length > 0) { 203 game.number_of_steps -= 1; 204 game.level_state = game.undo_stack.pop(); 205 } 206 } 207 208 209 function game_to_text(game) { 210 console.log(game.undo_state); 211 let s = ''; 212 213 if (is_won(game)) { 214 s = s.concat('Won! Press c to continue.'); 215 } 216 s = s.concat(`Level ${game.current_level_number + 1}. Step ${game.number_of_steps}.`); 217 218 let max_y = Math.max( 219 game.level_state.rocks.length, 220 game.level_state.endings.length, 221 game.level_state.floors.length, 222 game.level_state.walls.length, 223 game.level_state.player_position[1], 224 ); 225 for (let y = 0; y < max_y; y++) { 226 let max_x = Math.max( 227 game.level_state.rocks[y].length, 228 game.level_state.endings[y].length, 229 game.level_state.floors[y].length, 230 game.level_state.walls[y].length, 231 game.level_state.player_position[0], 232 ); 233 for (let x = 0; x < max_x; x++) { 234 if (here_player(game.level_state, [x, y])) { 235 s = s.concat('@'); 236 } else if (here_wall(game.level_state, [x, y])) { 237 s = s.concat('#'); 238 } else if (here_rock(game.level_state, [x, y])) { 239 s = s.concat('*'); 240 } else if (here_floor(game.level_state, [x, y])) { 241 s = s.concat('.'); 242 } else if (here_ending(game.level_state, [x, y])) { 243 s = s.concat('z'); 244 } else { 245 s = s.concat(' '); 246 } 247 }; 248 s = s.concat('\n'); 249 }; 250 251 return s.trimEnd(); 252 }; 253 254 255 function new_game() { 256 return { 257 current_level_number: 0, 258 level_state: read_level_text(levels[0]), 259 undo_stack: [], 260 number_of_steps: 0, 261 }; 262 }; 263 264 265 return { 266 action_next_level, 267 action_restart_level, 268 action_step_down, 269 action_step_left, 270 action_step_right, 271 action_step_up, 272 action_undo, 273 game_to_text, 274 new_game, 275 }; 276 })(); 277 278 279 console.log('sokoban.js end.');