-- 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.
- Step 1: ehsData = {} - Create or empty a table if it has already been created, which will hold the high score data.
- 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.
- 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.
- 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.
- Step 5: playerName = ehsData[1] - It is now a trivial matter to access the player name (ehsData[1]) and score (ehsData[2]).
- 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.