sokoban.nut.tns 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. //////////////////////////////////
  2. // nSquirrel examples //
  3. // Sokoban game //
  4. // By Florian 'Eiyeron' Dormont //
  5. // Defining the sprite size. //
  6. // 2016 - MIT Lience //
  7. //////////////////////////////////
  8. ////////
  9. // NOTE
  10. /////
  11. // What's listed in this comment is left as an exercise for the reader.
  12. // WHy should I program the whole thing where you could also get your hands
  13. // dirty? :p
  14. //
  15. // - Do a title screen, a victory (when finishing a level or every levels) and
  16. // a credits screen.
  17. // - Count the steps done in a level. Compare with the best solution if you can.
  18. // - Load levels from files. Here's a page containing infos on the .sok file
  19. // format : http://sokobano.de/wiki/index.php?title=Sok_format
  20. // - Store best times/steps in highscore files
  21. // - There are animation frames for the character, you could make him move from
  22. // tile to tile while pushing the block or not.
  23. // - Better graphisms? I'm bad at making pretty stuff.
  24. // - etc...
  25. ////////
  26. // Constants and enumerations
  27. // Small constant to define the size in pixels of a tile.
  28. const BLOCSIZE = 16
  29. // Using an enum here isn't really useful as we would need to call it
  30. // BlockType.Ground at each point. Not something we want, right?
  31. // Ground
  32. const _ = 0
  33. // Wall
  34. const W = 1
  35. // Box
  36. const B = 2
  37. // Plate (the boxes must get on the plates).
  38. const P = 3
  39. // Second wall (for roof)
  40. const T = 4
  41. // Box on plate
  42. const V = 5
  43. // An enumeration are managed at compile-time, so it shouldn't be too expensive
  44. // to use one. (Sure, you can edit one in run-time, but it'll break stuff like
  45. // code serialization.)
  46. enum MoveDirection {
  47. Top,
  48. Left,
  49. Right,
  50. Bottom
  51. }
  52. // this enumeration allows making the game state more clear.
  53. enum LevelState {
  54. Playing,
  55. Won,
  56. Abort
  57. }
  58. // Global variables
  59. // Variables which will hold the sprite data.
  60. local character_sheet = null
  61. local spritesheet = null
  62. // Classes
  63. // According to Squirrel's wiki, using classes for instancing data is better
  64. // than using tabls
  65. class Level{
  66. // Map width
  67. width = null
  68. // Map heiht
  69. height = null
  70. // Player's starting location
  71. player_start_x = null
  72. player_start_y = null
  73. // Copy of the map data so reloading it will be easier.
  74. map_data_start = null
  75. // Current map data.
  76. map_data = null
  77. // This is the constructor. Passing the variables to the instance.
  78. constructor(w, h, px, py, m) {
  79. width = w
  80. height = h
  81. player_start_x = px
  82. player_start_y = py
  83. map_data_start = m
  84. }
  85. // Creates the map data from the start state so we can start playing.
  86. function start_level()
  87. {
  88. // Creating the map data if it didn't existed before
  89. if(map_data == null) {
  90. map_data = array(height)
  91. for (local y = 0; y < height; y++) {
  92. map_data[y] = array(width)
  93. }
  94. }
  95. // Making the map's y dimension.
  96. // Filling the y dimension with the x stripes
  97. for (local y = 0; y < height; y++) {
  98. for (local x = 0; x < width; x++) {
  99. map_data[y][x] = map_data_start[y][x]
  100. }
  101. }
  102. }
  103. // Renders the map on the screen.
  104. function render() {
  105. for (local y = 0; y < height; y++) {
  106. for (local x = 0; x < width; x++) {
  107. if(map_data[y][x] != _)
  108. {
  109. local t = map_data[y][x]
  110. n2d.drawSpritePart(
  111. // Map spritesheet
  112. spritesheet,
  113. // Top left coordinate of the sprite on the screen
  114. x * BLOCSIZE, y * BLOCSIZE,
  115. // Dimension of the part of the sprite to draw
  116. BLOCSIZE * t, 0, BLOCSIZE, BLOCSIZE,
  117. // We don't want it to flick
  118. 0, 0
  119. )
  120. }
  121. }
  122. }
  123. }
  124. // Checks if the player can just move in the targeted tile. Returns true if so
  125. // and false if not.
  126. function move_block(blocx, blocy)
  127. {
  128. // Check first if the coordinates are in the map boundaries so we won't
  129. // read outside the map and we won't get an exception killing the game.
  130. if(blocx >= 0 && blocy >= 0 && blocx < width && blocy < height)
  131. return map_data[blocy][blocx] == _ || map_data[blocy][blocx] == P;
  132. return false;
  133. }
  134. // Checks if the selected tile is a block. If yes, pushes it in the selected
  135. // direction. Returns true if the function actually moved the block and false
  136. // if not.
  137. function push_block(blocx, blocy, direction) {
  138. // Check first if the coordinates are in the map boundaries so we won't
  139. // read outside the map and we won't get an exception killing the game.
  140. if(blocx >= 0 && blocy >= 0 && blocx < width && blocy < height)
  141. {
  142. if(map_data[blocy][blocx] == B || map_data[blocy][blocx] == V)
  143. {
  144. // We have a Box, be it on a plate or not now. Let's move it.
  145. switch(direction)
  146. {
  147. case MoveDirection.Top:
  148. // Let's stop before it'll break something so...
  149. // Stop if the blocy can't go upwards because it'll get
  150. // outside the bounds
  151. if(blocy == 0)
  152. return false;
  153. // Stop if there is a wall or a box or a box on a plate
  154. local targetTile = map_data[blocy - 1][blocx]
  155. if(targetTile == W || targetTile == T
  156. || targetTile == B || targetTile == V)
  157. return false;
  158. // So now, we can push the block, let's do it!
  159. map_data[blocy][blocx] -= B
  160. map_data[blocy - 1][blocx] += B
  161. return true;
  162. case MoveDirection.Bottom:
  163. // Let's stop before it'll break something so...
  164. // Stop if the blocy can't go downwards because it'll get
  165. // outside the bounds
  166. if(blocy == height - 1)
  167. return false;
  168. // Stop if there is a wall or a box or a box on a plate
  169. local targetTile = map_data[blocy + 1][blocx]
  170. if(targetTile == W || targetTile == T
  171. || targetTile == B || targetTile == V)
  172. return false;
  173. // So now, we can push the block, let's do it!
  174. map_data[blocy][blocx] -= B
  175. map_data[blocy + 1][blocx] += B
  176. return true;
  177. case MoveDirection.Left:
  178. // Let's stop before it'll break something so...
  179. // Stop if the blocy can't go left because it'll get
  180. // outside the bounds
  181. if(blocx == 0)
  182. return false;
  183. // Stop if there is a wall or a box or a box on a plate
  184. local targetTile = map_data[blocy][blocx - 1]
  185. if(targetTile == W || targetTile == T
  186. || targetTile == B || targetTile == V)
  187. return false;
  188. // So now, we can push the block, let's do it!
  189. map_data[blocy][blocx] -= B
  190. map_data[blocy][blocx - 1] += B
  191. return true;
  192. case MoveDirection.Right:
  193. // Let's stop before it'll break something so...
  194. // Stop if the blocy can't go right because it'll get
  195. // outside the bounds
  196. if(blocx == width - 1)
  197. return false;
  198. // Stop if there is a wall or a box or a box on a plate
  199. local targetTile = map_data[blocy][blocx + 1]
  200. if(targetTile == W || targetTile == T
  201. || targetTile == B || targetTile == V)
  202. return false;
  203. // So now, we can push the block, let's do it!
  204. map_data[blocy][blocx] -= B
  205. map_data[blocy][blocx + 1] += B
  206. return true;
  207. }
  208. }
  209. // We coudln't move the block, alas.
  210. return false;
  211. }
  212. }
  213. // Checks the whole map if the level is actually solved or isn't. To know if
  214. // it's solved, it checks if any plate doesn't have a box on it. If a plate
  215. // doesn't have a box, the level is not solved and it'll return false. If not
  216. // the level is finished and it'll return true.
  217. function is_level_solved() {
  218. for (local y = 0; y < height; y++) {
  219. for (local x = 0; x < width; x++) {
  220. if(map_data[y][x] == P) {
  221. return false;
  222. }
  223. }
  224. }
  225. return true;
  226. }
  227. }
  228. // Functions
  229. // This function only exists to organize the source code, you can do without it.
  230. function init_data() {
  231. // Loading the character's spritesheet.
  232. character_sheet = n2d.loadBMP("character.bmp.tns", 0xF81F)
  233. if(character_sheet == null) {
  234. // This shoudln't happen if the correct files are next to the script.
  235. print("character.bmp.tns not found. Make sure that this file is next"+
  236. " to the script. Exiting...")
  237. return false;
  238. }
  239. // Loading the levels' tilesheet.
  240. spritesheet = n2d.loadBMP("sheet.bmp.tns", 0xF81F)
  241. if(spritesheet == null) {
  242. // This shoudln't happen if the correct files are next to the script.
  243. print("sheet.bmp.tns not found. Make sure that this file is next"+
  244. " to the script. Exiting...")
  245. return false;
  246. }
  247. // Yay, everything is loaded, everything is awesome!
  248. return true;
  249. }
  250. // This function represents the game state running on one level.
  251. function game(level) {
  252. // - Some game variables -
  253. // The game's state. Will be returned at the end of the game state to
  254. // determine what to do next
  255. local level_state = null
  256. // These two variables are used to detect keys and avoid key repetition
  257. local key = null
  258. local previous_key = null
  259. // Measuring the time at the start (or restart) of the level?
  260. local starting_time = null
  261. // Player coordinates
  262. local player_x = null
  263. local player_y = null
  264. // Small variables to determine which sprite to use.
  265. local player_direction = null
  266. // This function, as it's coded inside another function, can access the
  267. // conataining function's local variables. I'm using it here to reset values
  268. // for the level.
  269. function reset() {
  270. // First, we load the level
  271. level.start_level()
  272. level_state = LevelState.Playing
  273. // Preparing the key detection variables.
  274. key = n2d.getKeyPressed()
  275. previous_key = key
  276. // Resetting the time.
  277. starting_time = time()
  278. // Setting our little character where it needs to be placed.
  279. player_x = level.player_start_x
  280. player_y = level.player_start_y
  281. player_direction = MoveDirection.Bottom
  282. }
  283. // Let's prepare the game state.
  284. reset()
  285. // While we're still playing the game
  286. while (level_state == LevelState.Playing) {
  287. // Let's fetch a pressed key from the keyboard
  288. local key = n2d.getKeyPressed()
  289. // Let's check if it's not the same button we press to avoid key repetition.
  290. local is_another_key_pressed = !(
  291. key.row == previous_key.row
  292. && key.col == previous_key.col
  293. && key.tpad_row == previous_key.tpad_row
  294. && key.tpad_col == previous_key.tpad_col
  295. && key.tpad_arrow == previous_key.tpad_arrow
  296. )
  297. // So if it's really another key, then...
  298. if(is_another_key_pressed)
  299. {
  300. // If we want to exit the game.
  301. if(n2d.isKey(key, n2dk.ESC)) {
  302. // Let's set the game state to Abort and break the loop.
  303. level_state = LevelState.Abort;
  304. break;
  305. }
  306. // Else if we want to go left.
  307. else if(n2d.isKey(key, n2dk.LEFT) || n2d.isKey(key, n2dk.K_4)) {
  308. // Our desired new position is one tile left.
  309. local target_x = player_x - 1
  310. player_direction = MoveDirection.Left
  311. // If the player can move without moving a block.
  312. if(level.move_block(target_x, player_y)) {
  313. player_x = target_x
  314. }
  315. // Else if he's going to push a block
  316. else if(level.push_block(target_x, player_y, MoveDirection.Left))
  317. {
  318. player_x = target_x
  319. // Let's check if we just finished the level or not.
  320. level_state = (level.is_level_solved() ?
  321. LevelState.Won :
  322. LevelState.Playing)
  323. }
  324. }
  325. else if(n2d.isKey(key, n2dk.RIGHT) || n2d.isKey(key, n2dk.K_6)) {
  326. // Our desired new position is one tile right.
  327. local target_x = player_x + 1
  328. player_direction = MoveDirection.Right
  329. // If the player can move without moving a block.
  330. if(level.move_block(target_x, player_y)) {
  331. player_x = target_x
  332. }
  333. // Else if he's going to push a block
  334. else if(level.push_block(target_x, player_y, MoveDirection.Right))
  335. {
  336. player_x = target_x
  337. // Let's check if we just finished the level or not.
  338. level_state = (level.is_level_solved() ?
  339. LevelState.Won :
  340. LevelState.Playing)
  341. }
  342. }
  343. else if(n2d.isKey(key, n2dk.UP) || n2d.isKey(key, n2dk.K_8)) {
  344. // Our desired new position is one tile up.
  345. local target_y = player_y - 1
  346. player_direction = MoveDirection.Top
  347. // If the player can move without moving a block.
  348. if(level.move_block(player_x, target_y)) {
  349. player_y = target_y
  350. }
  351. // Else if he's going to push a block
  352. else if(level.push_block(player_x, target_y, MoveDirection.Top))
  353. {
  354. player_y = target_y
  355. // Let's check if we just finished the level or not.
  356. level_state = (level.is_level_solved() ?
  357. LevelState.Won :
  358. LevelState.Playing)
  359. }
  360. }
  361. else if(n2d.isKey(key, n2dk.DOWN) || n2d.isKey(key, n2dk.K_2)) {
  362. // Our desired new position is one tile down.
  363. local target_y = player_y + 1
  364. player_direction = MoveDirection.Bottom
  365. // If the player can move without moving a block.
  366. if(level.move_block(player_x, target_y)) {
  367. player_y = target_y
  368. }
  369. // Else if he's going to push a block
  370. else if(level.push_block(player_x, target_y, MoveDirection.Bottom))
  371. {
  372. player_y = target_y
  373. // Let's check if we just finished the level or not.
  374. level_state = (level.is_level_solved() ?
  375. LevelState.Won :
  376. LevelState.Playing)
  377. }
  378. }
  379. // Else if we want to restart the game.
  380. else if(n2d.isKey(key, n2dk.R)) {
  381. // Let's call the right function to do so! :D
  382. reset()
  383. }
  384. // Put the detected key this frame in this variable so we can compare the
  385. // next one with it.
  386. previous_key = key
  387. }
  388. // Cleaning the screen
  389. n2d.clearBufferB()
  390. // Drawing the map
  391. level.render()
  392. // Drawing the character on the screen
  393. n2d.drawSpritePart(character_sheet, player_x*16, player_y*16,
  394. 16*player_direction, 0, 16, 16,
  395. 0, 0)
  396. // Let's get the spent time since the start of the level and print it.
  397. local current_time = time()
  398. // Calculating the difference between now and the starting time.
  399. local delta_time = current_time - starting_time
  400. // Calculating the minutes and converting it to integer so we don't have a
  401. // ugly floating point value
  402. local minutes = (delta_time/60)
  403. minutes = minutes.tointeger()
  404. // Calulcating the seconds
  405. local seconds = delta_time%60
  406. // Slap it on the screen, you know you like it, baby!
  407. n2d.drawString(0, 232, 0, "Time : "+minutes+":"+seconds, 0xFFFF, 0x0000)
  408. // Displays the changes on the screen.
  409. n2d.updateScreen()
  410. }
  411. return level_state;
  412. }
  413. function main() {
  414. // If we couldn't load any of the external files, we can't really play
  415. // the game, so let's exit before something break, okay?
  416. if(!init_data()) {
  417. return;
  418. }
  419. // The levels of our happy little game.
  420. local current_level = 0
  421. local levels = [
  422. // Using some levels from Microban
  423. // Source : http://sneezingtiger.com/sokoban/levels/microbanText.html
  424. Level(6, 7, 2, 3, [
  425. [T,W,W,W,_,_],
  426. [T,_,P,T,_,_],
  427. [T,_,_,W,W,T],
  428. [T,V,_,_,_,T],
  429. [T,_,_,B,_,T],
  430. [T,_,_,T,W,T],
  431. [W,W,W,W,_,_]
  432. ]),
  433. Level(6, 7, 3, 2, [
  434. [T,W,W,W,W,T],
  435. [T,_,_,_,_,T],
  436. [T,_,W,_,_,T],
  437. [T,_,B,V,_,T],
  438. [T,_,P,V,_,T],
  439. [T,_,_,_,_,T],
  440. [W,W,W,W,W,W]
  441. ]),
  442. Level(9, 6, 6, 4, [
  443. [_,_,T,W,W,T,_,_,_],
  444. [T,W,W,_,_,W,W,W,T],
  445. [T,_,_,_,_,_,B,_,T],
  446. [T,_,W,_,_,T,B,_,T],
  447. [T,_,P,_,P,T,_,_,T],
  448. [T,W,W,W,W,W,W,W,T]
  449. ]),
  450. Level(8, 6, 6, 2, [
  451. [T,W,W,W,W,W,W,T],
  452. [T,_,_,_,_,_,_,T],
  453. [T,_,P,V,V,B,_,T],
  454. [T,_,_,_,_,_,_,T],
  455. [W,W,W,W,T,_,_,T],
  456. [_,_,_,_,W,W,W,W],
  457. ]),
  458. Level(8, 7, 4, 3, [
  459. [_,T,W,W,W,W,W,T],
  460. [_,T,_,_,_,_,_,T],
  461. [_,T,_,P,B,P,_,T],
  462. [T,W,_,B,_,B,_,T],
  463. [T,_,_,P,B,P,_,T],
  464. [T,_,_,_,_,_,_,T],
  465. [W,W,W,W,W,W,W,W]
  466. ])
  467. ]
  468. // Initializing n2DLib
  469. n2d.initBuffering()
  470. // Setting a small loop variable, it's always cleaner than just a while(true)
  471. local quit = false
  472. while(!quit) {
  473. // Let's get the result of playing the current level
  474. local result = game(levels[current_level])
  475. switch(result) {
  476. // If we won this level, let's go to the next level.
  477. case LevelState.Won:
  478. current_level++
  479. // But if we ran out of levels, let's quit.
  480. if(current_level >= levels.len()) {
  481. quit = true
  482. }
  483. break;
  484. // If we want to quit earlier.
  485. case LevelState.Abort:
  486. quit = true;
  487. break;
  488. }
  489. }
  490. // Deinitializing n2DLib correctly like a correct programmer.
  491. n2d.deinitBuffering()
  492. }
  493. // The entry point of the program. That's actually the first line of code called
  494. main()