Sfoglia il codice sorgente

Added a sokoban game example.

It's a small 5 levels sokoban to show how a game could be programmed on
nSquirrel.
Eiyeron Fulmincendii 9 anni fa
parent
commit
1c32ec91e8

BIN
samples/n2DLib/sokoban/character.bmp.tns


BIN
samples/n2DLib/sokoban/sheet.bmp.tns


+ 534 - 0
samples/n2DLib/sokoban/sokoban.nut.tns

@@ -0,0 +1,534 @@
+//////////////////////////////////
+// nSquirrel examples           //
+// Sokoban game                 //
+// By Florian 'Eiyeron' Dormont //
+// Defining the sprite size.    //
+// 2016 - MIT Lience            //
+//////////////////////////////////
+
+////////
+// NOTE
+/////
+// What's listed in this comment is left as an exercise for the reader.
+// WHy should I program the whole thing where you could also get your hands
+// dirty? :p
+//
+// - Do a title screen, a victory (when finishing a level or every levels) and
+//   a credits screen.
+// - Count the steps done in a level. Compare with the best solution if you can.
+// - Load levels from files. Here's a page containing infos on the .sok file
+//   format : http://sokobano.de/wiki/index.php?title=Sok_format
+// - Store best times/steps in highscore files
+// - There are animation frames for the character, you could make him move from
+//   tile to tile while pushing the block or not.
+// - Better graphisms? I'm bad at making pretty stuff.
+// - etc...
+////////
+
+// Constants and enumerations
+
+// Small constant to define the size in pixels of a tile.
+const BLOCSIZE = 16
+// Using an enum here isn't really useful as we would need to call it
+// BlockType.Ground at each point. Not something we want, right?
+// Ground
+const _ = 0
+// Wall
+const W = 1
+// Box
+const B = 2
+// Plate (the boxes must get on the plates).
+const P = 3
+// Second wall (for roof)
+const T = 4
+// Box on plate
+const V = 5
+
+// An enumeration are managed at compile-time, so it shouldn't be too expensive
+// to use one. (Sure, you can edit one in run-time, but it'll break stuff like
+// code serialization.)
+enum MoveDirection {
+  Top,
+  Left,
+  Right,
+  Bottom
+}
+
+// this enumeration allows making the game state more clear.
+enum LevelState {
+  Playing,
+  Won,
+  Abort
+}
+
+// Global variables
+
+// Variables which will hold the sprite data.
+local character_sheet = null
+local spritesheet = null
+
+// Classes
+
+// According to Squirrel's wiki, using classes for instancing data is better
+// than using tabls
+class Level{
+  // Map width
+  width = null
+  // Map heiht
+  height = null
+  // Player's starting location
+  player_start_x = null
+  player_start_y = null
+  // Copy of the map data so reloading it will be easier.
+  map_data_start = null
+  // Current map data.
+  map_data = null
+
+  // This is the constructor. Passing the variables to the instance.
+  constructor(w, h, px, py, m) {
+    width = w
+    height = h
+    player_start_x = px
+    player_start_y = py
+    map_data_start = m
+  }
+
+  // Creates the map data from the start state so we can start playing.
+  function start_level()
+  {
+    // Creating the map data if it didn't existed before
+    if(map_data == null) {
+      map_data = array(height)
+      for (local y = 0; y < height; y++) {
+        map_data[y] = array(width)
+      }
+    }
+    // Making the map's y dimension.
+    // Filling the y dimension with the x stripes
+    for (local y = 0; y < height; y++) {
+      for (local x = 0; x < width; x++) {
+        map_data[y][x] = map_data_start[y][x]
+      }
+    }
+  }
+
+  // Renders the map on the screen.
+  function render() {
+    for (local y = 0; y < height; y++) {
+      for (local x = 0; x < width; x++) {
+        if(map_data[y][x] != _)
+        {
+          local t = map_data[y][x]
+          n2d.drawSpritePart(
+            // Map spritesheet
+            spritesheet,
+            // Top left coordinate of the sprite on the screen
+            x * BLOCSIZE, y * BLOCSIZE,
+            // Dimension of the part of the sprite to draw
+            BLOCSIZE * t, 0, BLOCSIZE, BLOCSIZE,
+            // We don't want it to flick
+            0, 0
+          )
+        }
+      }
+    }
+  }
+
+  // Checks if the player can just move in the targeted tile. Returns true if so
+  // and false if not.
+  function move_block(blocx, blocy)
+  {
+    // Check first if the coordinates are in the map boundaries so we won't
+    // read outside the map and we won't get an exception killing the game.
+    if(blocx >= 0 && blocy >= 0 && blocx < width && blocy < height)
+      return map_data[blocy][blocx] == _ || map_data[blocy][blocx] == P;
+    return false;
+  }
+
+  // Checks if the selected tile is a block. If yes, pushes it in the selected
+  // direction. Returns true if the function actually moved the block and false
+  // if not.
+  function push_block(blocx, blocy, direction) {
+    // Check first if the coordinates are in the map boundaries so we won't
+    // read outside the map and we won't get an exception killing the game.
+    if(blocx >= 0 && blocy >= 0 && blocx < width && blocy < height)
+    {
+      if(map_data[blocy][blocx] == B || map_data[blocy][blocx] == V)
+      {
+        // We have a Box, be it on a plate or not now. Let's move it.
+        switch(direction)
+        {
+          case MoveDirection.Top:
+            // Let's stop before it'll break something so...
+            // Stop if the blocy can't go upwards because it'll get
+            // outside the bounds
+            if(blocy == 0)
+              return false;
+            // Stop if there is a wall or a box or a box on a plate
+            local targetTile = map_data[blocy - 1][blocx]
+            if(targetTile == W || targetTile == T
+              || targetTile == B || targetTile == V)
+              return false;
+
+            // So now, we can push the block, let's do it!
+            map_data[blocy][blocx] -= B
+            map_data[blocy - 1][blocx] += B
+          return true;
+
+          case MoveDirection.Bottom:
+            // Let's stop before it'll break something so...
+            // Stop if the blocy can't go downwards because it'll get
+            // outside the bounds
+            if(blocy == height - 1)
+              return false;
+            // Stop if there is a wall or a box or a box on a plate
+            local targetTile = map_data[blocy + 1][blocx]
+            if(targetTile == W || targetTile == T
+              || targetTile == B || targetTile == V)
+              return false;
+
+            // So now, we can push the block, let's do it!
+            map_data[blocy][blocx] -= B
+            map_data[blocy + 1][blocx] += B
+          return true;
+
+          case MoveDirection.Left:
+            // Let's stop before it'll break something so...
+            // Stop if the blocy can't go left because it'll get
+            // outside the bounds
+            if(blocx == 0)
+              return false;
+            // Stop if there is a wall or a box or a box on a plate
+            local targetTile = map_data[blocy][blocx - 1]
+            if(targetTile == W || targetTile == T
+              || targetTile == B || targetTile == V)
+              return false;
+
+            // So now, we can push the block, let's do it!
+            map_data[blocy][blocx] -= B
+            map_data[blocy][blocx - 1] += B
+          return true;
+
+          case MoveDirection.Right:
+            // Let's stop before it'll break something so...
+            // Stop if the blocy can't go right because it'll get
+            // outside the bounds
+            if(blocx == width - 1)
+              return false;
+            // Stop if there is a wall or a box or a box on a plate
+            local targetTile = map_data[blocy][blocx + 1]
+            if(targetTile == W || targetTile == T
+              || targetTile == B || targetTile == V)
+              return false;
+
+            // So now, we can push the block, let's do it!
+            map_data[blocy][blocx] -= B
+            map_data[blocy][blocx + 1] += B
+          return true;
+
+        }
+      }
+      // We coudln't move the block, alas.
+      return false;
+    }
+  }
+
+  // Checks the whole map if the level is actually solved or isn't. To know if
+  // it's solved, it checks if any plate doesn't have a box on it. If a plate
+  // doesn't have a box, the level is not solved and it'll return false. If not
+  // the level is finished and it'll return true.
+  function is_level_solved() {
+    for (local y = 0; y < height; y++) {
+      for (local x = 0; x < width; x++) {
+        if(map_data[y][x] == P) {
+          return false;
+        }
+      }
+    }
+    return true;
+  }
+}
+
+// Functions
+
+// This function only exists to organize the source code, you can do without it.
+function init_data() {
+  // Loading the character's spritesheet.
+  character_sheet = n2d.loadBMP("character.bmp.tns", 0xF81F)
+  if(character_sheet == null) {
+    // This shoudln't happen if the correct files are next to the script.
+    print("character.bmp.tns not found. Make sure that this file is next"+
+    " to the script. Exiting...")
+    return false;
+  }
+
+  // Loading the levels' tilesheet.
+  spritesheet = n2d.loadBMP("sheet.bmp.tns", 0xF81F)
+  if(spritesheet == null) {
+    // This shoudln't happen if the correct files are next to the script.
+    print("sheet.bmp.tns not found. Make sure that this file is next"+
+    " to the script. Exiting...")
+    return false;
+  }
+  // Yay, everything is loaded, everything is awesome!
+  return true;
+}
+
+// This function represents the game state running on one level.
+function game(level) {
+
+  // - Some game variables -
+  // The game's state. Will be returned at the end of the game state to
+  // determine what to do next
+  local level_state = null
+
+  // These two variables are used to detect keys and avoid key repetition
+  local key = null
+  local previous_key = null
+  // Measuring the time at the start (or restart) of the level?
+  local starting_time = null
+  // Player coordinates
+  local player_x = null
+  local player_y = null
+  // Small variables to determine which sprite to use.
+  local player_direction = null
+
+  // This function, as it's coded inside another function, can access the
+  // conataining function's local variables. I'm using it here to reset values
+  // for the level.
+  function reset() {
+    // First, we load the level
+    level.start_level()
+    level_state = LevelState.Playing
+    // Preparing the key detection variables.
+    key = n2d.getKeyPressed()
+    previous_key = key
+    // Resetting the time.
+    starting_time = time()
+
+    // Setting our little character where it needs to be placed.
+    player_x = level.player_start_x
+    player_y = level.player_start_y
+    player_direction = MoveDirection.Bottom
+  }
+
+  // Let's prepare the game state.
+  reset()
+
+  // While we're still playing the game
+  while (level_state == LevelState.Playing) {
+    // Let's fetch a pressed key from the keyboard
+    local key = n2d.getKeyPressed()
+    // Let's check if it's not the same button we press to avoid key repetition.
+    local is_another_key_pressed = !(
+      key.row == previous_key.row
+      && key.col == previous_key.col
+      && key.tpad_row == previous_key.tpad_row
+      && key.tpad_col == previous_key.tpad_col
+      && key.tpad_arrow == previous_key.tpad_arrow
+      )
+    // So if it's really another key, then...
+    if(is_another_key_pressed)
+    {
+      // If we want to exit the game.
+      if(n2d.isKey(key, n2dk.ESC)) {
+        // Let's set the game state to Abort and break the loop.
+        level_state = LevelState.Abort;
+        break;
+      }
+      // Else if we want to go left.
+      else if(n2d.isKey(key, n2dk.LEFT) || n2d.isKey(key, n2dk.K_4)) {
+        // Our desired new position is one tile left.
+        local target_x = player_x - 1
+        player_direction = MoveDirection.Left
+        // If the player can move without moving a block.
+        if(level.move_block(target_x, player_y)) {
+          player_x = target_x
+        }
+        // Else if he's going to push a block
+        else if(level.push_block(target_x, player_y, MoveDirection.Left))
+        {
+          player_x = target_x
+          // Let's check if we just finished the level or not.
+          level_state = (level.is_level_solved() ?
+                          LevelState.Won :
+                          LevelState.Playing)
+        }
+      }
+      else if(n2d.isKey(key, n2dk.RIGHT) || n2d.isKey(key, n2dk.K_6)) {
+        // Our desired new position is one tile right.
+        local target_x = player_x + 1
+        player_direction = MoveDirection.Right
+        // If the player can move without moving a block.
+        if(level.move_block(target_x, player_y)) {
+          player_x = target_x
+        }
+        // Else if he's going to push a block
+        else if(level.push_block(target_x, player_y, MoveDirection.Right))
+        {
+          player_x = target_x
+          // Let's check if we just finished the level or not.
+          level_state = (level.is_level_solved() ?
+                          LevelState.Won :
+                          LevelState.Playing)
+        }
+      }
+      else if(n2d.isKey(key, n2dk.UP) || n2d.isKey(key, n2dk.K_8)) {
+        // Our desired new position is one tile up.
+        local target_y = player_y - 1
+        player_direction = MoveDirection.Top
+        // If the player can move without moving a block.
+        if(level.move_block(player_x, target_y)) {
+          player_y = target_y
+        }
+        // Else if he's going to push a block
+        else if(level.push_block(player_x, target_y, MoveDirection.Top))
+        {
+          player_y = target_y
+          // Let's check if we just finished the level or not.
+          level_state = (level.is_level_solved() ?
+                          LevelState.Won :
+                          LevelState.Playing)
+        }
+      }
+      else if(n2d.isKey(key, n2dk.DOWN) || n2d.isKey(key, n2dk.K_2)) {
+        // Our desired new position is one tile down.
+        local target_y = player_y + 1
+        player_direction = MoveDirection.Bottom
+        // If the player can move without moving a block.
+        if(level.move_block(player_x, target_y)) {
+          player_y = target_y
+        }
+        // Else if he's going to push a block
+        else if(level.push_block(player_x, target_y, MoveDirection.Bottom))
+        {
+          player_y = target_y
+          // Let's check if we just finished the level or not.
+          level_state = (level.is_level_solved() ?
+                          LevelState.Won :
+                          LevelState.Playing)
+        }
+      }
+      // Else if we want to restart the game.
+      else if(n2d.isKey(key, n2dk.R)) {
+        // Let's call the right function to do so! :D
+        reset()
+      }
+      // Put the detected key this frame in this variable so we can compare the
+      // next one with it.
+      previous_key = key
+    }
+
+    // Cleaning the screen
+    n2d.clearBufferB()
+    // Drawing the map
+    level.render()
+    // Drawing the character on the screen
+    n2d.drawSpritePart(character_sheet, player_x*16, player_y*16,
+      16*player_direction, 0, 16, 16,
+      0, 0)
+    // Let's get the spent time since the start of the level and print it.
+    local current_time = time()
+    // Calculating the difference between now and the starting time.
+    local delta_time = current_time - starting_time
+    // Calculating the minutes and converting it to integer so we don't have a
+    // ugly floating point value
+    local minutes = (delta_time/60)
+    minutes = minutes.tointeger()
+    // Calulcating the seconds
+    local seconds = delta_time%60
+    // Slap it on the screen, you know you like it, baby!
+    n2d.drawString(0, 232, 0, "Time : "+minutes+":"+seconds, 0xFFFF, 0x0000)
+    // Displays the changes on the screen.
+    n2d.updateScreen()
+  }
+
+  return level_state;
+}
+
+function main() {
+  // If we couldn't load any of the external files, we can't really play
+  // the game, so let's exit before something break, okay?
+  if(!init_data()) {
+    return;
+  }
+
+  // The levels of our happy little game.
+  local current_level = 0
+  local levels = [
+    // Using some levels from Microban
+    // Source : http://sneezingtiger.com/sokoban/levels/microbanText.html
+    Level(6, 7, 2, 3, [
+      [T,W,W,W,_,_],
+      [T,_,P,T,_,_],
+      [T,_,_,W,W,T],
+      [T,V,_,_,_,T],
+      [T,_,_,B,_,T],
+      [T,_,_,T,W,T],
+      [W,W,W,W,_,_]
+    ]),
+    Level(6, 7, 3, 2, [
+      [T,W,W,W,W,T],
+      [T,_,_,_,_,T],
+      [T,_,W,_,_,T],
+      [T,_,B,V,_,T],
+      [T,_,P,V,_,T],
+      [T,_,_,_,_,T],
+      [W,W,W,W,W,W]
+      ]),
+    Level(9, 6, 6, 4, [
+      [_,_,T,W,W,T,_,_,_],
+      [T,W,W,_,_,W,W,W,T],
+      [T,_,_,_,_,_,B,_,T],
+      [T,_,W,_,_,T,B,_,T],
+      [T,_,P,_,P,T,_,_,T],
+      [T,W,W,W,W,W,W,W,T]
+      ]),
+    Level(8, 6, 6, 2, [
+      [T,W,W,W,W,W,W,T],
+      [T,_,_,_,_,_,_,T],
+      [T,_,P,V,V,B,_,T],
+      [T,_,_,_,_,_,_,T],
+      [W,W,W,W,T,_,_,T],
+      [_,_,_,_,W,W,W,W],
+      ]),
+    Level(8, 7, 4, 3, [
+      [_,T,W,W,W,W,W,T],
+      [_,T,_,_,_,_,_,T],
+      [_,T,_,P,B,P,_,T],
+      [T,W,_,B,_,B,_,T],
+      [T,_,_,P,B,P,_,T],
+      [T,_,_,_,_,_,_,T],
+      [W,W,W,W,W,W,W,W]
+      ])
+  ]
+
+  // Initializing n2DLib
+  n2d.initBuffering()
+  // Setting a small loop variable, it's always cleaner than just a while(true)
+  local quit = false
+  while(!quit) {
+    // Let's get the result of playing the current level
+    local result = game(levels[current_level])
+    switch(result) {
+      // If we won this level, let's go to the next level.
+      case LevelState.Won:
+        current_level++
+        // But if we ran out of levels, let's quit.
+        if(current_level >= levels.len()) {
+          quit = true
+        }
+      break;
+      // If we want to quit earlier.
+      case LevelState.Abort:
+        quit = true;
+      break;
+    }
+  }
+  // Deinitializing n2DLib correctly like a correct programmer.
+  n2d.deinitBuffering()
+}
+
+// The entry point of the program. That's actually the first line of code called
+
+main()