| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534 |
- //////////////////////////////////
- // 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()
|