Showing posts with label save. Show all posts
Showing posts with label save. Show all posts

Saturday, September 29, 2012

Tutorial 18 - Saving and Loading Complicated Tables



18.1 Recap

   
Tutorial 18 brings together elements from a number of the previous Tutorials. It is an example of where asking the right question makes finding the answer much simpler. Our aim is to be able to load and save level data from our Tower Defence level generator. Most of the data we need is stored in a table. The tricky part is that Codea only allows us to save data as strings at the moment (some smart folks over at the Codea Forum have worked out how to jam data into image files and save those).
  
Tutorial 16 examined a technique for saving simple table structures like a one dimensional array. Initially we thought that saving a more complicated table wouldn't be that tough. All we needed was one of the existing Lua table to string parsers like Pickle / UnPickle. WRONG!
    

18.2 Limitations of Table Serialisation Parsers

  
We discussed Lua tables in Interlude 6. They are simple in concept but capable of representing very complicated structures. Tables can contain all the simple Lua types (booleans, strings, nil, and number), functions, other tables, closures, etc. The most complete table to string parser that we could find was Data Dumper. But even Data Dumper can not handle tables that contain:
  • Both light and full userdata
  • Coroutines
  • C functions
And of course our level data contains userdata (e.g. vec2 is represented using userdata). So where to from here?
    

18.3 The Modified Cell Class

   
This is where asking the right question makes all the difference. Looking at which data we actually need to save, it becomes apparent that we don't need to save the entire table (including functions) as this can all be reconstituted. All we really need from the Grid table is what is contained in each cell. So rather than trying to convert a two dimensional array of cell objects (which contain userdata) into a string, we extend the cell class to provides its contents (state) as a string. We also provide the inverse function which sets the cell state from a string so we can load the level back in. The extended cell class is shown below.
  
  
Once we can get the grid cell contents out as a string it is easy to write a function to create a simple two dimensional table holding each of these strings. Data Dumper makes short work of converting this table to a string which we can save to global data. Even better, Data Dumper saves the table in a format that loadstring can use to rebuild the original table. The updated Main class for dGenerator details these save and load functions.
  

We updated the File Manager class from Tutorial 17 to allow us to select the file to be loaded. As part of this update we got rid of the submenus because we didn't need them and they meant that the user had to do an extra tap to load. In addition, Delete was right next to Load which is poor design. One slip of the finger would be potentially disastrous (particularly because there is no confirmation for delete and no undo). The other improvement to this version of File Manager is we truncate keys and values which are larger than the ListScroll widths. You can see the upgraded version in the screenshot below. The About menu item doesn't do anything at this stage.
     

18.4 Loading & Saving Data

   
Tapping the Save button on the main screen of dGenerator will call the saveLevel() function in Main(). The function starts off by saving the simplified table data in saveGrid. It then generates the key which contains a header "dGen" (to indicate when loading if it is the right data type), the game name and level number, a boolean indicating if the start cell has been selected and its co-ordinates and then a boolean indicating if the end cell has been selected and its co-ordinates. Finally we use Data Dumper to convert saveGrid to a string and save the key and value to global data.
  
Loading a level is the reverse. Tapping the Load button on the dGenerator main screen will call the loadLevel() function in Main. This prints out some debug data but its only compulsory action is to set the App state to stateFileManager. This will draw the File Manager instead of the main screen and handle its touches.
  
Once the user has navigated to global data and selected an appropriate key containing level data, tapping load on the File Manager menu bar will call the loadFile() function in Main. This function splits the key back into its component parts using the explode() helper function and then uses loadstring() to create a function which rebuilds the data table. The createGrid() function was updated so that it can be loaded using this data table.
  
And that is all there is to it. You can use this link to download the entire code including the updated classes.
  
Next up we will look at adding creeps to your levels in a number of waves.

Thursday, September 20, 2012

Tutorial 17 - A Simple File Manager for Codea



17.1 The Main Class

         
Tutorial 16 explained the various ways that data can be stored using Codea. In order to facilitate the loading and storing of data for our level generator we created a simple file management class.
    
This version handles Local Data, Project Data, Project Information and Global Data. Remember that all of this data is stored in string format using a pList which is accessed by a unique string called the key.
    
The File Manager is constructed using two ListScroll objects (a class written by sanit over on the Codea Forum), a text box showing the value of the selected key and a MenuBar object (a class written by dave1707 also from the Codea Forum).
   
The Main class is used to load some test data into the four data stores and then instantiate the FileManager class. We have provided stubs for the functions called when the MenuBar items are tapped. The only menu item which has been implemented is delete file. WARNING - file deletion can NOT be undone and there is no confirmation required to delete a file so back up your data and use this class with care (or delete this functionality). Feel free to delete the test data which is loaded in setup(), this will be recreated every time you run the App.
  
The data you need to take particular care with is Global Data as this may have been stored by another Codea App. For example, if you have used Spritely (the sprite creation example program which comes with Codea) then any sprites that you have created will be stored in global data.

The delete function has not been implemented for Project Info because there is no way to determine what keys are in there (i.e. there is no listProjectInfo function).

To delete a file we just assign the value of the selected key to nil. We will provide examples of the load and save function when we incorporate FileManager into our Tower Defence Level Generator (dGenerator).
   
The line FileManager:init() will setup your FileManager class and prefetch the keys for each persistent data store.
       
The class handles both portrait and landscape orientation. The orientationChanged() function adjusts the FileManager position when the iPad is rotated. It works but not very well as ListScroll wasn't designed to handle this. You may have to drag the ListScroll box to show the contents. We haven't had a chance to modify this.
      

17.2 The FileManager Class

             
Most of the hard work is done in this class. We start off by defining variables to represent the four supported Directory Types to make the code easier to read. The init() function creates the two ListScrolls (one for the list of Directories called directoryList and one for the keys it contains called dataKeyList).
     
We then pre-load the keys for Project Data, Local Data and Global Data (we already know the Project Info keys since we loaded them). We set the current directory to Project Info and set the current key to the first one. There is a variable called valueString which we use to contain the value of the selected string. Only the first 150 characters of the saved value are displayed. The last thing we do in init() is setup the MenuBar.
     
One of the modifications we had to make to the MenuBar class was to prevent touches on the ListScroll when a sub menu is displayed (otherwise you could accidentally delete the wrong file).
       

17.3 Download the Code

          
You can download all of the code for this program using the following links:
  1. FileManager Complete Code v1.1 - All of the code in one file.
  2. Main.lua - The Main tab class only.
  3. FileManager.lua - The FileManager v1.1 class only.
  4. ListScroll.lua - The ListScroll class from sanit.
  5. TextItem.lua - The TextItem class used in ListScroll from sanit.
  6. MenuBar.lua - The MenuBar class v1.1 from dave1707 (modified) .
Next up we will incorporate FileManager into our Tower Defence Level Generator.

Saturday, September 8, 2012

Tutorial 16 - Convert String to Table and Table to String

16.1 Why do we need this?

  
In our last tutorial we developed a simple level editor. This is not much use unless you can save and then load the level data produced. Codea provides a function to saveProjectData and one to saveGlobalData(). Since we would like to save our level data from dGenerator and then read it into our game program, saveGlobalData() is the function we need, as saveProjectData() saves data which is only accessible by the program which saved it.
  
Unfortunately it is not as simple as saying saveGlobalData("Level1", ourTableData) as you can only use this function to save a number, string or nil. Consequently, we need to convert our table to a string in order to save it. To load the data we need to reverse this process and reconstitute our table from the saved string.
  
We used a similar technique in MineSweeper to save and load high scores, albeit we were only concatenating three strings rather than converting a table, but the theory is the same.
     

16.2 Codea Data Persistence Under the Hood

      
There are four primary ways to save and load data in Codea (excluding image data, which we wont discuss in this tutorial). Namely:
  1. clearLocalData(), saveLocalData() and readLocalData() - used for storing information which is unique to the program and the device. Other programs and devices running a copy of the same program don't have access to this data. It is useful for things like high scores and perhaps user preferences. For each project, a key of the form Project Name_DATA is used to store local data in the com.twolivesleft.Codify.plist which may be found in the Library -> Preferences sub folder (i.e. the same location as global data).
  2. clearProjectData(), saveProjectData and readProjectData() - gets bundled with your program and is not device specific. It can be used for level data, maps, and other program specific data which doesn't change with different users. Project Data is stored in the Data pList in the Documents directory of your App.
  3. saveProjectInfo() and readProjectInfo - has similar access to Project Data (bundled with program and is not device specific) but is used for saving metadata about your App. There are two data keys which are natively supported by Codea, "Description" and "Author". The data associated with the "Description" key will show in the Codea Project Browser when this App is selected. If you are planning on using the Codea runtime, be aware that this uses the "Version" key and expects a string. If you use a number like we do then you will get an error and the runtime wont compile. The fix for this is described in our earlier tutorial on submitting an application to App store. If you whip out iExplorer you will see that all of the project information is stored in Info.plist which is part of your application bundle. This pList also includes a key called "Buffer Order" which tells the runtime which order your tabs should be loaded in.
  4. saveGlobalData() and readGlobalData() - is data available between all projects on a device. The subfolder Preferences of folder Library contains files including com.twolivesleft.Codify.plist which contains any global data that you have saved (under the key "CODEA_GLOBAL_DATA_STORE"). If you have used the sprite generator Spritely which comes with Codea, you will see that it stores some data in the global data store.
You can read more about what is happening under the hood on the Codea Wiki. Since version 1.4.3 of Codea three additional functions have been provided: listLocalData(), listProjectData() and listGlobalData(). These return the keys already saved in these data stores.
    

16.3 A Solution for Single Dimension Arrays & Tables

    
The simplest version of a Lua table is an array so lets work out a solution for that first. It is an easy matter to concatenate the elements of a table together to form a string, as long as the elements of that table are something that can be converted to a string (i.e. this wont work on a table of tables). It is easy because Lua provides a function to do this very thing. Even better you can specify a delimiter to be placed between each element in the resulting string.
  
The function looks like table.concat(yourArray, "delimiter") so:
  
table.concat({ "one", "two", "three", "four", "five" }, ",") will produce a string "one,two,three,four,five"
   
Assuming we pass in the array to our saveData() function, it will look like:
  
function saveData(mArray)
   
    local saveString = table.concat(mArray, ",")
    saveGlobalData("dataKey", saveString)
     
end
   
Obviously you could just as easily use a global array, in which case you wont need to pass the array to the function. Loading the data is a bit more work, but as with a lot of common problems, it is one which has already been solved (credit to http://richard.warburton.it for writing the explode function which does all the heavy lifting).
      
function explode(div,str) 
    
    if (div=='') then return false end
    local pos,arr = 0,{}
    -- for each divider found
    for st,sp in function() return string.find(str,div,pos,true) end do
        table.insert(arr,string.sub(str,pos,st-1)) -- Attach chars left of current divider
        pos = sp + 1 -- Jump past current divider
    end
    table.insert(arr,string.sub(str,pos)) -- Attach chars right of last divider
    return arr
   
end
    
Using the above function the loadData() function is trivial, and will return your original array:
   
function loadData()
     
    local stringArray = readGlobalData("dataKey")
    return explode(",", stringArray)
     
end
     

16.4 A Solution for Multidimension Arrays & Tables

   
Once you get past the simple array or non-nested table things get complicated quickly. As you can stick just about anything in a table (e.g. another table, closures / functions, Userdata and Metatables) a comprehensive conversion function would need to take all of the possible cases into account. These table serialisation libraries exist but they are overkill for our purposes.
  
Instead we will use the charmingly named Pickle and UnPickle functions written by Steve Dekorte.
    
To be continued...

Wednesday, July 25, 2012

Tutorial 7 - MineSweeper Part 2


7.1 Overview



In Part 1 of this tutorial we covered elements of game design. We will now get much closer to the metal and look at the key parts of the code for MineSweeper. Between tutorials we updated the functionality to include High Scores. There aren't many games which don't have this and they are one method of adding longevity to your App (a subject for a later tutorial).


7.2 Download the Code



As the code base has been updated we have provided links to the latest versions. Some of the classes haven't changed from version 1 so you could just copy those across from your current project but for completeness they are all included below. You can either download the entire MineSweeper code in one file or by class:

  1. Main.lua v1.2 - this has been updated from the previous version.
  2. Cell.lua v1.1 - this has been updated from the previous version.
  3. Button.lua v1.1 - unchanged from previous version.
  4. RoundBorder v1.1 - unchanged from previous version.
  5. ScoreScreen v1.0 - a new class to display your High Scores.
  6. SplashScreen v1.1 - unchanged from previous version.
  7. Fader v1.1 - unchanged from previous version.
  8. TextBox v1.0 - a new class, used to enter the high score player's name.
  9. Twinkle v1.1 - unchanged from previous version.
  10. IconImages v1.1 - unchanged from previous version.

7.3 How it Works


7.3.1 The Main Class


The best place to start is usually the beginning so we will look at the Main class first. The code is fairly well commented but we will provide additional commentary at key points.


--# Main

-- MineSweeper
-- Reefwing Software (reefwing.com.au)
--
-- Version 1.2
--
-- A reprisal of the classic MineSweeper game, written entirely on the iPad.
-- Game icons were built using Spritely, the pixel editor included with Codea.
--
-- Version 1.2 adds high score functionality.
--
-- This demonstration game was built as part of a series of Tutorials on Codea
-- and programming with Lua for the iPad. These tutorials may be found at
-- www.codeatuts.blogspot.com.au
--
-- To make import of code into Codea easier, each class is available as a separate file
-- in our dropbox repository. This will also make it easier to reuse classes in
-- your own projects.

-- Define supported iPad orientations
-- This way we don't forget to handle orientation changes if required.
-- "ANY" is the default assignment. This version of MineSweeper only handles landscape
-- orientation. The next tutorial will demonstrate how to handle changing orientations.

Note that supportedOrientations is set before the setup() function. It is best to do this to ensure that no drawing gets done before this is assigned.

supportedOrientations(LANDSCAPE_ANY)

function setup()

   version = 1.2

   saveProjectInfo("Description", "Codea version of Minesweeper")
   saveProjectInfo("Author", "Reefwing Software")
   saveProjectInfo("Date", "1st July 2012")
   saveProjectInfo("Version", version)
   saveProjectInfo("Comments", "High Score added. FPS code streamlined.")

   print("Minesweeper v"..version.."\n")

   -- Define the game colour palette

   whiteColour = color(255, 255, 255)
   blackColour = color(0, 0, 0)
   redColour = color(243, 157, 33, 255)
   blueColour = color(0, 188, 255, 255)
   greenColour = color(45, 226, 23, 255)

   -- Keep an eye on your Frames Per Second (FPS)

   FPS = 0
   watch("FPS")

   -- keep track of your Game State, Tap State, and Game Difficulty
   -- using Finite State Machines.
   --
   -- gameState is the overall state of the game.
   --
   -- tapState is used when the game is running. It toggles the tap function between
   -- revealing a cell and flagging it as a possible mine. The button text of flagButton
   -- indicates the current state.
   --
   -- gameDifficulty allows us to setup a new game with the correct parameters when the
   -- newGameButton is tapped.

You don't have to define each state using variables, you could just use the numbers directly but doing it this way is easier to remember and makes your code much easier to read and maintain. Lua doesn't have enums so this is one way to provide similar functionality. Another option would be to use a table.

   stateSplash = 0
   stateMenu = 1
   stateRun = 2
   stateWon = 3
   stateLost = 4
   stateScore = 5

   gameState = stateSplash

   stateReveal = 5
   stateFlag = 6

   tapState = stateReveal

   stateEasy = 7
   stateMedium = 8
   stateHard = 9

   gameDifficulty = stateEasy

   -- initialise the game variables and grid matrix

   score = 0
   numberOfCells = 0
   numberOfMines = 0
   gridWidth = 0
   gridHeight = 0

   cellsRevealed = 0
   watch("cellsRevealed")

   cellsLeft = 0
   watch("cellsLeft")

   gameTime = 0
   watch("gameTime")
   gameTimerOn = false

The grid table contains all the data for the mine field. It is the real work horse for the game but we share the load a bit by creating a cell class. The grid then becomes a table of cell "objects".

   grid = {}

   -- initialise the cell icon images
   -- These images were drawn using Spritely.

   ii = IconImages()

   newCellImage = ii:getNewCell()
   emptyCellImage = ii.getEmptyCell()
   mineCellImage = ii.getMineCell()
   flagCellImage = ii.getFlagCell()

   iconSize = newCellImage.width

   -- create the 4 menu buttons, they wont be visible until you draw them.
   -- Note that 50 pixels is the minimum height for the default font size

   local mButtonSize = vec2(180, 50)
   local mLocX = WIDTH/2 - mButtonSize.x/2
   local mLocY = HEIGHT/2 + 20

   easyButton = Button("Easy", vec2(mLocX, mLocY), mButtonSize.x, mButtonSize.y)
   easyButton.action = function() easyButtonPressed() end

   mLocY = mLocY - 80
   mediumButton = Button("Medium", vec2(mLocX, mLocY), mButtonSize.x, mButtonSize.y)
   mediumButton.action = function() mediumButtonPressed() end

   mLocY = mLocY - 80
   hardButton = Button("Hard", vec2(mLocX, mLocY), mButtonSize.x, mButtonSize.y)
   hardButton.action = function() hardButtonPressed() end

   mLocY = mLocY - 160
   scoreButton = Button("High Score", vec2(mLocX, mLocY), mButtonSize.x, mButtonSize.y)
   scoreButton.action = function() scoreButtonPressed() end

   menuBorder = RoundBorder(10, 10, WIDTH - 10, HEIGHT - 10, 1, blueColour, blackColour)

   -- create the run screen buttons

   mLocX = WIDTH - mButtonSize.x/2 - 25
   mLocY = HEIGHT - 195
   mButtonSize = vec2(100, 50)

   flagButton = Button("Show", vec2(mLocX, mLocY), mButtonSize.x, mButtonSize.y)
   flagButton.action = function() flagButtonPressed() end

   mLocY = 110 + mButtonSize.y*2
   newGameButton = Button("New", vec2(mLocX, mLocY), mButtonSize.x, mButtonSize.y)
   newGameButton.action = function() newGameButtonPressed() end

   mLocY = 110
   menuButton = Button("Menu", vec2(mLocX, mLocY), mButtonSize.x, mButtonSize.y)
   menuButton.action = function() menuButtonPressed() end

   -- create the splash screen

   splashScreen = SplashScreen("Minesweeper", 3)

   -- Create the twinkling star background

   twinkleBackground = Twinkle(50)

   -- Load previous High Score data if available
   -- High scores are stored for easy, medium and hard games.

We initially set highScoreSaved to true as this flag is set false when we want to save a new high score.

   playerName = "Player 1"
   highScoreSaved = true

   -- Load High Score data into the three high score data tables.

   loadHighScoreData()

   -- Create the text box used to enter the highscore name

   textBox = TextBox(WIDTH / 2 - 100, HEIGHT - 120, 200, playerName)

   -- Create the High Score Screen

   highScoreScreen = ScoreScreen()

end

-- Draw routines

function draw()

   -- The main drawing function, called 60 times per second if possible.
   -- This sets a black background color

   background(blackColour)

   -- Calculate and display FPS and track game time if game running.

We have moved to displaying instantaneous FPS as the code used is a bit shorter. The downside is the displayed FPS changes every frame. DeltaTime is the time elapsed since the last frame was drawn. It is a number provided by Codea.

   if gameTimerOn then
       gameTime = gameTime + DeltaTime
   end
   FPS = math.round(1/DeltaTime)

   -- Draw appropriate screen based on gameState

This is where the Finite State Machine magic happens. The pattern should be familiar from earlier tutorials. You can change what gets drawn to the screen each frame by just changing the gameState. An alternative to using a string of if-then-elseif-end blocks is to use a table. Yes tables can be used for just about anything and everything in Lua. We will explore this technique in the next tutorial.

   if gameState == stateSplash then
       splashScreen: draw()
   elseif gameState == stateMenu then
       drawMenu()
   elseif gameState == stateRun then
       twinkleBackground: draw()
       drawGrid()
       drawGameButtons()
       drawCellsLeftDisplay()
   elseif gameState == stateWon then
       twinkleBackground: draw()
       drawGrid()
       drawGameButtons()
       drawCellsLeftDisplay()
       text("Game Won!", WIDTH/2, 60)
       if highScore() and not highScoreSaved then
           showKeyboard()
           textBox:draw()
       end
   elseif gameState == stateLost then
       twinkleBackground: draw()
       drawGrid()
       drawGameButtons()
       drawCellsLeftDisplay()
       text("Game Over!", WIDTH/2, 60)
   elseif gameState == stateScore then
       highScoreScreen: draw()
   end

end

function drawGameButtons()

   -- These are the buttons visible on the game run screen

   flagButton:draw()
   newGameButton:draw()
   menuButton:draw()

end

function drawCellsLeftDisplay()

   -- Draw the number of cells left to be revealed using the
   -- funky LCD font. This is a more accurate representation
   -- of progress than mines flagged.

   pushStyle()

   font("DB LCD Temp")
   fontSize(64)
   local w, h = textSize(cellsLeft)
   fill(whiteColour)
   text(cellsLeft, w + 50, HEIGHT - 100)

   text(math.round(gameTime), WIDTH - 150, HEIGHT - 100)

   popStyle()

end

function drawMenu()

   -- Draw the Game Menu Buttons

   menuBorder: draw()

   font("Arial-BoldMT")
   fill(whiteColour)
   fontSize(20)
   text("Select Game Difficulty", WIDTH/2, HEIGHT/2 + 150)

   fill(blueColour)
   fontSize(72)
   text("Minesweeper", WIDTH/2, HEIGHT/2 + 220)

   easyButton:draw()
   mediumButton:draw()
   hardButton:draw()
   scoreButton:draw()

end

function drawGrid()

   -- Iterate through the grid matrix and draw each cell

   for i = 1, gridWidth do
       for j = 1, gridHeight do
           grid[i][j]: draw()
       end
   end

end

-- High Score Functions

function highScore()

   -- Check if a new high score has been achieved for the
   -- current game difficulty state. A high score is saved
   -- for each difficulty level.

When a game is won, this function is used to work out whether this is a new high score for the current difficulty level. We use a table to contain the data for each level. There are lots of different ways that you can approach this. The table method is fairly simple and easy to read and write to disk (as described below). We only use two fields in each table, index 1 contains the player's name and index 2 the score. 

This function will return true if the current score is higher than the saved score (e.g. ehsData[2] for easy difficulty], or if no score has previously been saved (i.e. the table is empty).

Initially we only checked to see if the table was nil to return true, but you can get the situation where the table is initialised but empty. To ensure we cover off this case, we check the length of the high score table using the #function (e.g. #ehsData == 0).

   if gameDifficulty == stateEasy then
       if ehsData == nil or #ehsData == 0 or gameTime <= ehsData[2] then
           return true
       end
   elseif gameDifficulty == stateMedium then
       if mhsData == nil or #mhsData == 0 or gameTime <= mhsData[2] then
           return true
       end
   elseif gameDifficulty == stateHHard then
       if hhsData == nil or #hhsData == 0 or gameTime <= hhsData[2] then
           return true
       end
   end
   return false

end

function loadHighScoreData()

This function loads previously saved high score data (if it exists) into the relevant table for use later. We have used a technique described by @John at Two Lives Left. 

The readLocalData() and saveLocalData() functions allow you to read and save a string against a key, which is another string. To load the player name and score from the combined saved string and allocate to our table we use a very powerful function called loadstring().

To understand how this works we will step through the code for reading the easy high score data. The medium and hard routines use exactly the same methodology.

  1. Step 1: ehsData = {} - Create or empty a table if it has already been created, which will hold the high score data.
  2. Step 2: if readLocalData("easyHighScore") ~= nil then - Check if any high score data has been saved previously. If not an empty table will be returned.
  3. Step 3: If high score data is available then easyHighScoreString = loadstring(readLocalData("easyHighScore")). This needs some explanation. Starting in the deepest nested function, readLocalData(key) will return a string which has previously been saved against key ("easyHighScore" in this case). The string returned will look something like "return {"Player 1", 1000}". We will demonstrate how to save a string like this when we get to the saveHighScore() function. The loadstring(mString) function will return a function which executes whatever is contained in mString, and we assign this to easyHighScoreString.
  4. Step 4: ehsData = easyHighScoreString() - Using the created easyHighScoreString() function we load up the player name and score into our table. The easyHighScoreString() function looks like return {"Player 1", 1000}. So this returns a table with two fields which is assigned to ehsData.
  5. Step 5: playerName = ehsData[1] - It is now a trivial matter to access the player name (ehsData[1]) and score (ehsData[2]).
  6. Step 6: print("Easy High Score -> Name: "..playerName.." Time: "..ehsData[2]) - The last line of code was just used to assist with debugging. We left it in here to help illustrate the workings, you can remove from your production code.
   ehsData = {}
   if readLocalData("easyHighScore") ~= nil then
       easyHighScoreString = loadstring(readLocalData("easyHighScore"))
       ehsData = easyHighScoreString()
       playerName = ehsData[1]
       print("Easy High Score -> Name: "..playerName.." Time: "..ehsData[2])
   end

   mhsData = {}
   if readLocalData("mediumHighScore") ~= nil then
       mediumHighScoreString = loadstring(readLocalData("mediumHighScore"))
       mhsData = mediumHighScoreString()
       playerName = mhsData[1]
       print("Medium High Score -> Name: "..playerName.." Time: "..mhsData[2])
   end

   hhsData = {}
   if readLocalData("hardHighScore") ~= nil then
       hardHighScoreString = loadstring(readLocalData("hardHighScore"))
       hhsData = hardHighScoreString()
       playerName = hhsData[1]
       print("Hard High Score -> Name: "..playerName.." Time: "..hhsData[2])
   end

end

function saveHighScore(d)

   -- Build the high score data into a string which is saved
   -- using saveLocalData(key, value).
   --
   -- n = playerName
   -- t = gameTime
   -- d = os.date() [current date] not used in this version

We get the player name from whatever was entered into the text box which appears following a winning game.

   playerName = textBox.text
   print("New High Score by: "..playerName)

For the loadstring() trick to work when we loadHighScoreData(), the saved string needs to be in the following format: "return {"player name", score}. hsDataString creates this string. Note the backslash quotes to include double quotes in the string. saveLocalData(key, savedString) will save the savedString against the key (which is just another string).

   local hsDataString = string.format("return {\"%s\", %d}", playerName, gameTime)

   if gameDifficulty == stateEasy then
       saveLocalData("easyHighScore", hsDataString)
   elseif gameDifficulty == stateMedium then
       saveLocalData("mediumHighScore", hsDataString)
   elseif gameDifficulty == stateHard then
       saveLocalData("hardHighScore", hsDataString)
   end

Once we have saved the high score we can hide the keyboard and set the highScoreSaved flag (which is used in draw() to determine whether to show the keyboard when a game is won).

   hideKeyboard()
   highScoreSaved = true

end

-- Count Neighbouring Mine Functions
--
-- We will discuss closure functions in a separate tutorial, but
-- for now to understand what is going on in the count neighbouring mine
-- functions you need to know that when a function is enclosed in
-- another function, it has full access to local variables from the
-- enclosing function. In this example, inNeighbourCells() increments the local
-- variable mineNum in countMines().

function inNeighbourCells(startX, endX, startY, endY, closure)
   for i = math.max(startX, 1), math.min(endX, gridWidth) do
       for j = math.max(startY, 1), math.min(endY, gridHeight) do
           closure(i, j)
       end
   end
end

function countMines(index)

   local mineNum = 0

   inNeighbourCells(index.x - 1, index.x + 1, index.y - 1, index.y + 1,
       function(x, y) if grid[x][y].mine then mineNum = mineNum + 1 end
       end)

   return mineNum
end

-- Grid Creation Function

function createGrid()

   local baseX = WIDTH/2 - (iconSize * gridWidth) / 2
   local y = HEIGHT/2 - (iconSize * gridHeight) / 2
   local x = baseX

   -- Create the grid using nested tables.
   -- It operates as a two dimensional array (or matrix)

The grid is in effect a two dimensional array of cell "objects", with a dimension of gridWidth x gridHeight. The Cell is defined with its index in the grid [i, j], whether it contains a mine (initially false), and its position on the screen (x, y).

   for i = 1, gridWidth do
       grid[i] = {}     -- create a new row
       for j = 1, gridHeight do
           grid[i][j] = Cell(i, j, false, x, y)
           grid[i][j].action = function() handleCellTouch(grid[i][j].index) end
           x = x + iconSize
       end
       x = baseX
       y = y + iconSize
   end

   -- Seed the mines at random locations on the grid

   for i = 1, numberOfMines do
       local mineX, mineY
       repeat
           mineX = math.random(1, gridWidth)
           mineY = math.random(1, gridHeight)
       until not grid[mineX][mineY].mine == true   -- we dont want to duplicate mine location
       grid[mineX][mineY].mine = true
   end

   -- Count the neighbouring mines for each cell and save in the cell.
   --
   -- You could alternatively calculate this each time a cell is revealed
   -- to shorten loading times, but for simplicity we will do it here.

   for i = 1, gridWidth do
       for j = 1, gridHeight do
           grid[i][j].neighbourMines = countMines(grid[i][j].index)
       end
   end

end

function resetGrid()

   -- When starting a new game this function will
   -- reset the table

   for i = 1, gridWidth do
       for j = 1, gridHeight do
           grid[i][j] = nil
       end
   end

   grid = {}

end

-- KeyBoard handling function
-- Used to enter name if High Score achieved

function keyboard(key)

   if key ~= nil then
       if string.byte(key) == 10 then         -- <RETURN> Key pressed
           saveHighScore(os.date())
       elseif string.byte(key) ~= 44 then -- filter out commas
           textBox:acceptKey(key)
       end
   end

end

-- Touch & Button Handling Functions

-- In Game Buttons

function flagButtonPressed()

   -- Toggle tapState every time this button is pressed

   if tapState == stateReveal then
       tapState = stateFlag
       flagButton.text = "Flag"
   else
       tapState = stateReveal
       flagButton.text = "Show"
   end

end

function newGameButtonPressed()

If the user forgets to hit the <return> key we still want to save the high score if a game was just won.

   if not highScoreSaved then
       saveHighScore(os.date())
   end

   resetGrid()
   gameTimerOn = false

   if gameDifficulty == stateEasy then
       easyButtonPressed()
   elseif gameDifficulty == stateMedium then
       mediumButtonPressed()
   elseif gameDifficulty == stateHard then
       hardButtonPressed()
   end

end

function menuButtonPressed()

If the user forgets to hit the <return> key we still want to save the high score if a game was just won.

   if not highScoreSaved then
       saveHighScore(os.date())
   end

   gameState = stateMenu
   gameTimerOn = false

end

-- Menu Buttons and associated functions.

function resetGameParameters()

   -- Regardless of the game difficulty selected, these parameters
   -- need to be reset when a new game is started.

   cellsLeft = numberOfCells
   cellsRevealed = 0
   gameTime = 0

   -- Make sure that the Flag / Show cell button is in the default state.

   tapState = stateReveal
   flagButton.text = "Show"

   -- Create the cell grid and seed with mines then run game

   createGrid()
   gameState = stateRun

end

function easyButtonPressed()

   -- Initialise the parameters which determine difficulty
   -- Namely: number of mines and the grid size

   gameDifficulty = stateEasy

   numberOfMines = 10
   gridWidth = 8
   gridHeight = 8
   numberOfCells = gridWidth * gridHeight
   resetGameParameters()

end

function mediumButtonPressed()

   -- Initialise the parameters which determine difficulty
   -- Namely: number of mines and the grid size

   gameDifficulty = stateMedium

   numberOfMines = 15
   gridWidth = 12
   gridHeight = 12
   numberOfCells = gridWidth * gridHeight
   resetGameParameters()

end

function hardButtonPressed()

   -- Initialise the parameters which determine difficulty
   -- Namely: number of mines and the grid size

   gameDifficulty = stateHard

   numberOfMines = 40
   gridWidth = 16
   gridHeight = 16
   numberOfCells = gridWidth * gridHeight
   resetGameParameters()

end

function scoreButtonPressed()

   -- High Score button tapped in the menu screen

   loadHighScoreData()
   gameState = stateScore

end

-- Handle Touches

function touched(touch)

   -- It is important to explicitly state when to handle touches.
   --
   -- If you don't do this based on gameState then for example, when in the
   -- game run state your menu button handling functions will still get called
   -- even if the buttons are not being drawn.

   if gameState == stateMenu then
       easyButton:touched(touch)
       mediumButton:touched(touch)
       hardButton:touched(touch)
       scoreButton:touched(touch)
   elseif gameState == stateRun then
       for i = 1, gridWidth do
           for j = 1, gridHeight do
               grid[i][j]:touched(touch)
           end
       end
       menuButton:touched(touch)
       newGameButton:touched(touch)
       flagButton:touched(touch)
   elseif gameState == stateLost or gameState == stateWon then
       menuButton:touched(touch)
       newGameButton:touched(touch)
       flagButton:touched(touch)
   elseif gameState == stateScore then
       highScoreScreen:touched(touch)
   end

end

function revealCell(index)

   -- If neighbourMines = 0 for the touched cell then
   -- we reveal the neighbour cells as well using a
   -- recursive call to this function.

   grid[index.x][index.y].revealed = true
   cellsRevealed = cellsRevealed + 1
   cellsLeft = numberOfCells - cellsRevealed - numberOfMines

   if grid[index.x][index.y].neighbourMines == 0 then
       inNeighbourCells(index.x - 1, index.x + 1, index.y - 1, index.y + 1, function(x, y)
           if not grid[x][y].revealed and not grid[x][y].mine and not grid[x][y].flagged then
               revealCell({x = x, y = y})
           end
       end)
   end

   if cellsRevealed == (numberOfCells - numberOfMines) then
       gameState = stateWon
       highScoreSaved = false
       gameTimerOn = false
       sound(SOUND_POWERUP, 20494)
   end

end

function handleCellTouch(index)

   -- Cell touched call back function.
   --
   -- Reveal or flag touched cell depending on tap state

   if gameState == stateRun then
       gameTimerOn = true
   else
       gameTimerOn = false
   end

   if tapState == stateReveal then
       revealCell(index)
   else    -- tapState == flagged, toggle cell flagged state
       grid[index.x][index.y].flagged = not grid[index.x][index.y].flagged
   end

end

7.3.2 The Cell Class


You would have seen that the two dimensional grid contains cells. The Cell class is described below.

--# Cell
Cell = class()

-- MineSweeper Cell Class
-- Reefwing Software
--
-- Version 1.1
--
-- Each element in the MineSweeper two dimensional grid{}
-- consists of a cell object. Each cell is responsible for
-- tracking its own state and drawing the appropriate icon
-- based on this state.

function Cell:init(i, j, mine, x, y)

   -- Cell Initialisation.
   -- you can accept and set the cell parameters here

   self.index = vec2(i, j)     -- location of cell within the grid{} table
   self.mine = mine            -- boolean indicating if cell contains a mine
   self.revealed = false     -- boolean indicating if cell has been revealed
   self.flagged = false       -- boolean indicating cell has been flagged
   self.pos = vec2(x, y)     -- position of cell on the screen
   self.action = nil              -- call back function when cell tapped
   self.neighbourMines = 0     -- number of mines in surrounding cells
   self.size = vec2(newCellImage.width, newCellImage.height)    -- size of cell on screen

end

-- Cell Draw Functions

function Cell:drawMineCountText()

   -- This function will draw the number of mines
   -- in adjacent cells, if the cell is revealed.
   --
   -- If there are no surrounding mines then no
   -- number is shown.

   -- Save the graphic context

   pushStyle()

   if self.neighbourMines ~= 0 then

       -- Set the text colour based on number of mines
       -- in neigbouring cells.

       if self.neighbourMines == 1 then
           fill(blueColour)
       elseif self.neighbourMines == 2 then
           fill(greenColour)
       elseif self.neighbourMines == 3 then
           fill(redColour)
       else
           fill(blackColour)
       end

       -- Draw the text centred in the cell

       fontSize(16)
       text(self.neighbourMines, self.pos.x, self.pos.y)

   end

   -- return the graphic context to its original parameters.

   popStyle()

end

function Cell:draw()
   -- Codea does not automatically call this method

   -- Draw the appropriate cell image based on whether it has been revealed or not,
   -- has been flagged or whether it contains a mine.
   --
   -- At the end of the game (lost or won) display all mine locations

   local endOfGame = (gameState == stateWon or gameState == stateLost)

   if endOfGame and self.mine then
       sprite(mineCellImage, self.pos.x, self.pos.y)
   elseif self.mine and self.revealed then
       sprite(mineCellImage, self.pos.x, self.pos.y)
   elseif self.revealed then
       sprite(emptyCellImage, self.pos.x, self.pos.y)
       self: drawMineCountText()
   elseif self.flagged then
       sprite(flagCellImage, self.pos.x, self.pos.y)
   else
       sprite(newCellImage, self.pos.x, self.pos.y)
   end

end

-- Cell Touch Handling

function Cell:hit(p)

   -- Was the touch on this cell?
   -- Note code repurposed from the original button class
   -- provide in the Codea examples.

   local l = self.pos.x - self.size.x/2
   local r = self.pos.x + self.size.x/2
   local t = self.pos.y + self.size.y/2
   local b = self.pos.y - self.size.y/2
   if p.x > l and p.x < r and
      p.y > b and p.y < t then
       return true
   end
   return false

end

function Cell:touched(touch)

   -- Codea does not automatically call this method

   if touch.state == ENDED and self:hit(vec2(touch.x,touch.y)) then
       if self.mine and tapState == stateReveal then
           -- You just tapped a mine! Game over.
           gameState = stateLost
           highScoreSaved = true
           gameTimerOn = false
           sound(SOUND_EXPLODE, 43277)
       end

       if self.action then
           -- call back method called.
           self.action(self.index)
       end

   end

end



7.3.3 The High Score Screen


The last class we will look at is the one used to draw the High Score screen. It is fairly simple and draws on the load and save high score functions described in the Main class above. For those looking for something more advanced have a look at Vega's sample project to save high score data to the cloud.

--# ScoreScreen
ScoreScreen = class()

-- ScoreScreen
-- Reefwing Software (reefwing.com.au)
--
-- This class demonstrates a simple high score screen for your App.
--
-- Version 1.0

function ScoreScreen:init()
   -- Initialise the High Score Screen

   local mButtonSize = vec2(180, 50)
   local mLocX = 100
   local mLocY = HEIGHT/2 - 300

   backButton = Button("Back", vec2(mLocX, mLocY), mButtonSize.x, mButtonSize.y)
   backButton.action = function() backButtonPressed() end

   clearButton = Button("Clear", vec2(WIDTH - 280, mLocY), mButtonSize.x, mButtonSize.y)
   clearButton.action = function() clearButtonPressed() end

end

function ScoreScreen:draw()

   -- Codea does not automatically call this method

   pushStyle()

   -- Set the screen background to blue

   background(blackColour)

   menuBorder:draw()

   -- To use the column formatting below you have to use a fixed
   -- width font. This means either Inconsolata or one of the
   -- Courier varieties.

   font("Courier-Bold")
   fill(blueColour)
   fontSize(72)
   textAlign(CENTER)

   text("High Scores", WIDTH/2, HEIGHT/2 + 220)

   fill(whiteColour)
   fontSize(24)

   local str

   -- The high score data for easy, medium and hard levels are stored
   -- temporarily in 3 tables, ehsData, mhsData and hhsData respectively.
   --
   -- Index 1 of the table (e.g. ehsData[1]) contains the playerName.
   -- Index 2 contains the score.
   --
   -- If there is no current high score then a default name and score is used.
   -- You need to make the score high so that it is replaced when beaten by
   -- your player.
   --
   -- To understand the formatting strings used below (e.g. %-10.10s) have a
   -- look at Interlude 10 on the Codea Tutorial site (codeatuts.blogspot.com.au).

   if ehsData ~= nil and #ehsData > 0 then
       str = string.format("%-10.10s\t%-15.15s\t%10d", "Easy", ehsData[1], ehsData[2])
   else
       str = string.format("%-10.10s\t%-15.15s\t%10d", "Easy", "Player 1", 1000)
   end
   text(str, WIDTH/2, HEIGHT/2 + 40)

   if mhsData ~= nil and #mhsData > 0 then
       str = string.format("%-10.10s\t%-15.15s\t%10d", "Medium", mhsData[1], mhsData[2])
   else
       str = string.format("%-10.10s\t%-15.15s\t%10d", "Medium", "Player 2", 2000)
   end
   text(str, WIDTH/2, HEIGHT/2)

   if hhsData ~= nil and #hhsData > 0 then
       str = string.format("%-10.10s\t%-15.15s\t%10d", "Hard", hhsData[1], hhsData[2])
   else
       str = string.format("%-10.10s\t%-15.15s\t%10d", "Hard", "Player 3", 3000)
   end
   text(str, WIDTH/2, HEIGHT/2 - 40)

   popStyle()

   backButton:draw()
   clearButton:draw()

end

function ScoreScreen:touched(touch)

   -- Codea does not automatically call this method

   backButton:touched(touch)
   clearButton:touched(touch)

end

-- High Score Button Action Methods

function clearButtonPressed()

   -- This function will erase the High Score Data.
   --
   -- Note that this deletion is permanent and the App doesn't
   -- confirm that this is what the user wants to do.
   -- In a real App you should include a confirming dialog box.

   clearLocalData()
   loadHighScoreData()

end

function backButtonPressed()

   -- This function makes use of the gameState Finite State Machine
   -- to indicate that your App should now draw the Menu screen.

   gameState = stateMenu

end

7.4 Conclusion


That concludes our review of the MineSweeper game. This should provide you with some valuable techniques for writing your own games.