Rainsweeper

A complete recreation of Minesweeper in the form of a desktop widget for Rainmeter. This was made to demonstrate the capabilities of the Lua scripting language, which had recently been implemented as a Rainmeter plugin.

Source


--------------------------------------------------------------------------------------------
-- INITIALIZE
-- Runs when skin is loaded or refreshed. Sets up the board, declares global variables, and
-- creates a database containing the properties of each square.

function Initialize()

	---------------------------------------------------------------------
	-- GET USER SETTINGS FROM SKIN
	
	iMines     = tonumber(SKIN:GetVariable('NumberOfMines', '10'))
	iRows      = tonumber(SKIN:GetVariable('NumberOfRows',  '9' ))
	iCols      = tonumber(SKIN:GetVariable('NumberOfCols',  '9' ))
	iQuestions = tonumber(SKIN:GetVariable('Questions',     '0' ))
	
	-- These basic variables determine all of the parameters for the
	-- current game. The tonumber() wrapper means we can use their
	-- numerical values in math formulas.
	
	---------------------------------------------------------------------
	-- CREATE SQUARES DATABASE
	
	tSquares = {}
	
	for i = 1, iRows * iCols do
		local iX = (i-1) - math.floor((i-1) / iCols)*iCols
		local iY = math.floor((i-1) / iCols)
		tSquares[i] = { z=i, x=iX, y=iY, m=0, n=0, f=0, q=0, c=0 }
	end
	
	-- Here, we calculate each square's X and Y coordinates as a function
	-- of the square's linear index number. (For example, in a 10x10 grid,
	-- square #25 is at position [4,2] - fifth square on the third row.)
	-- All squares are also initially set as unmined, not adjacent to any
	-- mines, not flagged or question-marked, and not cleared.
	-- Technically, each "square" in the database is a database itself -
	-- a table within a table.
	
	---------------------------------------------------------------------
	-- CREATE SQUARES GRID IN SKIN
	
	for i = 2, #tSquares do
		if (i-1) % iCols == 0 then
			SKIN:Bang('!SetOption '..i..' MeterStyle "StyleSquare | StyleNewRow"')
		else
			SKIN:Bang('!SetOption '..i..' MeterStyle "StyleSquare"')
		end
		SKIN:Bang('!ShowMeter '..i)
	end
	
	-- The skin supports up to 720 squares. Since not all squares are
	-- needed for most games, all but #1 start in a hidden state. This
	-- loop reveals those squares needed for the current configuration,
	-- and creates "line breaks" based on the number of columns. (For
	-- example, if there are 9 squares per row, then every 9th square
	-- gets the "new row" property, which is defined as a MeterStyle in
	-- the skin.)
	
	---------------------------------------------------------------------
	-- DECLARE GLOBAL VARIABLES
	
	iFlags = iMines
	SKIN:Bang('!SetOption Flags Text '..string.format('%03d', iFlags))
	
	-- The player is given a number of "flags" equal to the number of
	-- mines, in order to mark spots where she thinks a mine might be
	-- hidden. This number will increase or decrease as flag markers are
	-- added and removed. The bang displays this number in the "Flags"
	-- meter in the skin. (The "%03d" format ensures a 3-digit layout.)
	
	iStartTime = 0
	iTimer = 0
	SKIN:Bang('!SetOption Timer Text '..string.format('%03d', iTimer))
	
	-- The timer starts at zero, and is displayed in the same format as
	-- the Flags counter. The "start time" variable serves two purposes:
	-- not only does it record the exact moment when the game begins, but
	-- the "timer" will not increase until the start time has been set
	-- (exceeds zero).
	
	iCleared = 0
	iGameOver = 0
	
	-- Declares that no squares have been cleared yet, and the game has
	-- not ended. Other functions will need to know both of these things.
	
	sLevel = DetectLevel(iRows, iCols, iMines)
	Scores(sLevel)
	Settings()
	
	-- The DetectLevel() function determines whether the current settings
	-- match the "Beginner," "Intermediate" or "Advanced" conditions. If
	-- not, the game is "Custom." The Scores() and Settings() functions
	-- update the Scores and Settings menu panels, respectively. (As a
	-- courtesy, the Scores panel starts on the tab for the current
	-- difficulty level.)
	
end

--------------------------------------------------------------------------------------------
-- UPDATE
-- Runs whenever the skin updates: once per second, by default. Updates the timer value (if
-- the start time has already been set).

function Update()
	if iStartTime > 0 then
		iTimer = os.difftime(os.time(), iStartTime)
		iTimer = iTimer < 1000 and iTimer or 999
		SKIN:Bang('!SetOption Timer Text '..string.format('%03d', iTimer))
	end
	
	-- Since the skin updates irregularly (every time the player clicks a square), we cannot
	-- reliably record the amount of time elapsed just by counting updates. Instead, we
	-- record the time when the game started - the player's first click - and repeatedly
	-- compare that time to the Windows clock. We also arbitrarily freeze the clock at 999
	-- seconds, as an homage to the original Minesweeper.
	
end

--------------------------------------------------------------------------------------------
-- CLEARING ACTIONS
-- Run when the player performs an action that clears one or more squares, including left-
-- clicking, double-clicking, or invoking the "Clear All" shortcut from the menu. All three
-- actions feed into a common "Clear()" function, providing a "queue" of squares for the
-- function to process.

function LeftClick(z)
	local z = tonumber(z)
	
	-- In this script, "z" is always a reference to a square's linear
	-- index number. When the player clicks on a square, it calls this
	-- function with its index number as a parameter, telling the script
	-- which square to manipulate. (Each square's meter in the skin is
	-- named by its number, which makes this a lot simpler.)
	
	---------------------------------------------------------------------
	-- SAFETY CHECKS
	
	if tSquares[z]['c'] == 1 or tSquares[z]['f'] == 1 or iGameOver ~= 0 then
		return
	end
	
	-- Left-clicking does nothing if the square has already been cleared,
	-- if it has a flag marker, or if the game has ended.
	
	---------------------------------------------------------------------
	-- FIRST-CLICK ACTIONS: START TIMER, PLANT MINES
	
	if iStartTime == 0 then
		iStartTime = os.time()
	
		-- Starts the timer by recording the current time. This only happens
		-- on the first left-click of each game.
		
		local iUnplantedMines = iMines
		while iUnplantedMines > 0 do
			local iRandomSquare = math.random(1,#tSquares)
			if tSquares[iRandomSquare]['m'] == 0 and tSquares[iRandomSquare]['c'] == 0 and iRandomSquare ~= z then
				tSquares[iRandomSquare]['m'] = 1
				iUnplantedMines = iUnplantedMines -1
			end
		end
		
		-- Pick random squares to become mines. This code loops through the
		-- entire table, each time picking a random uncleared square and
		-- "planting" a mine in that space (if one hasn't already been
		-- planted there). The loop breaks when the correct number of mines
		-- have been deployed. We do this only after the first square has
		-- been clicked in order to make sure that the player never loses
		-- on her first move.
		
		for i = 1, #tSquares do
			tSquares[i]['n'] = Adjacents(i, 'Threats')
		end
		
		-- Once all mines have been planted, the Adjacents() function checks
		-- each square and calculates the number of mines adjacent to that
		-- square. Adjacents() is split off into a separate function because
		-- its features are used in several places.
	end
	
	---------------------------------------------------------------------
	-- CREATE QUEUE AND CLEAR
	
	tZ = { z }
	Clear(tZ, 1)
	
	-- The "Clear()" function requires a list of squares in the form of a
	-- table. Since left-clicking only affects one square, we create a
	-- table with one cell, containing just the ID of the current square.
	-- The Clear() function will take over from here.
	
end

function LeftDoubleClick(z)
	local z = tonumber(z)
	
	---------------------------------------------------------------------
	-- SAFETY CHECKS

	if tSquares[z]['c'] == 0 or tSquares[z]['n'] == 0 or tSquares[z]['f'] == 1 or iGameOver ~= 0 then
		return
	end
	
	-- The game allows the player to double-click an empty square in
	-- order to clear all unflagged squares adjacent to it. Unlike a
	-- normal click, this can only be done on a square that has already
	-- been cleared. It also ignores any squares with no adjacent mines,
	-- since those squares will already have been cleared all around.
	-- Otherwise, it makes the same checks as a normal click.
	
	---------------------------------------------------------------------
	-- GET ADJACENT SQUARES

	local tAdjacents = Adjacents(z)
	
	-- Without being given any other parameters, the Adjacents() function
	-- returns the complete list of a square's immediate neighbors.
	
	---------------------------------------------------------------------
	-- CHECK FOR SUFFICIENT FLAGS
	
	local iAdjacentFlags = 0
	for i,v in ipairs(tAdjacents) do
		if tSquares[v]['f'] == 1 then
			iAdjacentFlags = iAdjacentFlags + 1
		end
	end
	if iAdjacentFlags < tSquares[z]['n'] then
		Message('TooFewFlags')
		return
	end
	
	-- In order to protect the player from their own stupidity, this loop
	-- counts up the number of flags among the adjacent squares, and
	-- terminates if there isn't at least one flag for every mine. Of 
	-- course, it's still possible that the flags have been wrongly
	-- placed, but when there are fewer flags than mines, the player is
	-- guaranteed to lose if the action continues.
	
	---------------------------------------------------------------------
	-- CREATE QUEUE AND CLEAR
	
	Clear(tAdjacents, 1)
	
	-- This is the same function that ends the normal LeftClick() action.
	-- But this time, we are sending the entire table of adjacent squares
	-- to process as a batch. The effect is simply as if the player had
	-- clicked all of these squares simultaneously.
	
end

function ClearAll()
	
	---------------------------------------------------------------------
	-- SAFETY CHECKS
	
	if iGameOver ~= 0 then
		return
	end
	
	-- The third and final clearing action is "Clear All," which the
	-- player can select from the menu to clear the board of all
	-- unflagged squares. Because this action isn't associated with a
	-- specific square, all we do as an initial safety check is to
	-- terminate if the game has already anded.
	
	if iStartTime == 0 then
		Message('Suicide')
		return
	end
	
	-- If the game hasn't started yet, then all squares must still be
	-- unclicked - which means that the player is trying to clear the
	-- board without having any information about the mine placement.
	-- In this case, we terminate the action with a feedback message
	-- explaining to him what a stupid idea this was.
	
	if iFlags > 0 then
		Message('TooFewFlagsAll')
		return
	end
	
	-- Next, we make the same check as the double-click action: if the
	-- player has placed fewer flags than the total number of mines, we
	-- terminate the action in order to prevent a guaranteed loss.
	
	---------------------------------------------------------------------
	-- CREATE QUEUE AND CLEAR
	
	local tAll = {}
	for i = 1, #tSquares do table.insert(tAll, i) end
	Clear(tAll)
	
	-- Finally, we send the queue - which, in this case, consists of
	-- literally the entire table of squares - to the Clear() function.
	-- And now we'll find out exactly what happens there.
	
end

function Clear(tQueue, iAdjacents)

	---------------------------------------------------------------------
	-- MARK SQUARES CLEAR, CHECK FOR MINES
	
	local iTrippedMines = 0
	for i,v in ipairs(tQueue) do
	
	-- First, the function loops through the list of squares it was given,
	-- performing checks on each individual square in turn.
	
		if tSquares[v]['c'] == 0 and tSquares[v]['f'] == 0 then
			
		-- Squares which are flagged, or have already been cleared, are
		-- ignored.
			
			tSquares[v]['c'] = 1
			iCleared = iCleared + 1
			
			-- The square is marked as cleared in the database. In addition, we
			-- update the variable "iCleared," which records the total number of
			-- cleared squares. This isn't strictly necessary - we can run
			-- through the database and count squares with a certain property at
			-- any time - but as long as we're here, we can save ourselves a
			-- little time this way.
			
			if tSquares[v]['m'] == 1 then
				iTrippedMines = iTrippedMines + 1
				Render('TrippedMine', v)

				-- If the square turns out to be a mine, we mark it as "tripped." The
				-- Render() function handles changing the square's actual appearance
				-- in the skin, changing the square's text, colors, tooltips, etc.
				
			else
				Render('Clear', v)
				if iAdjacents then Adjacents(v, 'Clear') end
				
				-- Otherwise, the square is "clear," in which case we send it off to
				-- the Adjacents() function for some extra checks. Specifically, if
				-- the square is both empty and has no adjacent mines, we do the
				-- player the courtesy of clearing all the neighboring squares -
				-- and then doing the same check on *those* squares, until the entire
				-- formation of contiguous unthreatened squares has been emptied.
				-- The "iCleared" counter will also be increased to account for each
				-- of the affected squares.
				
			end
		end
	end
	
	---------------------------------------------------------------------
	-- CHECK ENDGAME CONDITIONS
	
	if iTrippedMines > 0 then
	
		---------------------------------------------------------------------
		-- LOSS
	
		iGameOver = -1
		Update()
		iStartTime = 0
		Scores(sLevel, 'Update')
		
		-- At this point, if the player has tripped any mines, then we know
		-- she has lost the game. We change the "iGameOver" variable to
		-- indicate that the game has ended in a loss (-1), freeze the timer
		-- after one last update, and send a command to the Scores() function
		-- to update the player's statistics for the current difficulty level.
		
		for i = 1, #tSquares do
			if tSquares[i]['c'] == 0 then
				if tSquares[i]['m'] == 1 and tSquares[i]['f'] == 0 then
					Render('UntrippedMine', i)
				elseif tSquares[i]['m'] == 0 and tSquares[i]['f'] == 1 then
					Render('WrongFlag', i)
				elseif tSquares[i]['m'] == 1 and tSquares[i]['f'] == 1 then
					Render('RightFlag', i)
				else
					Render('Remainder', i)
				end
			end
		end
		
		-- The previous loop rendered all the newly-cleared squares; now we
		-- have to deal with the ones that were left uncleared when the game
		-- ended, revealing the locations of mines that were correctly
		-- flagged, mines that were never detected, and flags that were
		-- wrongly placed.
		
		Message('Lose')
		SKIN:Bang('!SetOption Background SolidColor "#ColorBackgroundLose#"')
		SKIN:Bang('#OpenMenu#')
		
		-- Finally, we send a feedback message informing the player of her
		-- loss, along with a red coloration on the background. We also open
		-- the menu, since she'll probably want to either start a new game or
		-- close the skin in disgust, depending on her mood.
		
	elseif iCleared == #tSquares - iMines then
	
		---------------------------------------------------------------------
		-- WIN
	
		iGameOver = 1
		Update()
		iStartTime = 0
		Scores(sLevel, 'Update')
		
		-- If the player has left a number of uncleared squares equal to the
		-- number of mines - and has not tripped any mines up to this point -
		-- then we know that she has won the game. We don't need to make her
		-- flag all of the mines, since the numbers leave no doubt about
		-- their locations.
		
		for i = 1, #tSquares do
			if tSquares[i]['c'] == 0 then 
				if tSquares[i]['f'] == 0 then
					tSquares[i]['f'] = 1
					iFlags = iFlags - 1
				end
				Render('DefusedMine', i)
			end
		end
		SKIN:Bang('!SetOption Flags Text '..string.format('%03d', iFlags))
		
		-- We don't need to do nearly as much rendering this time, since the
		-- only remaining uncleared squares are necessarily mines. The last
		-- loop automatically flags these squares and renders them as
		-- "defused" mines, indicating victory.
		
		Message('Win')
		SKIN:Bang('#OpenMenu#')
		SKIN:Bang('!SetOption Background SolidColor "#ColorBackgroundWin#"')
		
		-- We end with the opposite equivalents of the "defeat" feedback
		-- messages.
		
	else
		Message('Close')
		SKIN:Bang('#CloseMenu#')
		
		-- If the player has neither won nor lost yet, we simply end the
		-- action by dismissing any feedback messages from the last cycle,
		-- and close the menu (in case the "Clear All" command was selected.)
		
	end
end

--------------------------------------------------------------------------------------------
-- OTHER ACTIONS
-- Other independent gameplay actions, including right-clicking to "flag" a square, as well
-- as restarting the current scenario with a new minefield.

function RightClick(z)
	local z = tonumber(z)
	
	-- Right-clicking allows the player to "flag" a square that she knows
	-- (or thinks) is a mine. Flagged squares are immune to clearing
	-- actions.
	
	---------------------------------------------------------------------
	-- SAFETY CHECKS
	
	if tSquares[z]['c'] == 1 or iGameOver ~= 0 then
		return
	end
	
	-- The action will not proceed if the square is already cleared, or
	-- if the game has ended.
	
	---------------------------------------------------------------------
	-- CYCLE FLAG STATES
	
	if tSquares[z]['f'] == 0 then
		tSquares[z]['f'] = 1
		iFlags = iFlags - 1
		Render('Flag', z)
		
		-- The first condition changes a blank square to a flagged square.
		-- We update the square's properties in the database, reduce the
		-- number of available flags by one, and change the square's
		-- color in the skin to mark its special status.
		
	elseif tSquares[z]['q'] == 0 and iQuestions == 1 then
		tSquares[z]['q'] = 1
		Render('Question', z)
		
		-- If the player has enabled the "question marks" setting, a
		-- second right-click changes a normal flag to a "question" flag.
		-- This is purely for the player's benefit; mechanically, both
		-- types of flags behave exactly the same way.
		
	else
		tSquares[z]['f'] = 0
		tSquares[z]['q'] = 0
		iFlags = iFlags + 1
		Render('Opaque', z)
		
		-- If question marks are not enabled, or the square is already a
		-- question mark, a final right-click changes the square back to
		-- normal and re-adds to the flag counter.
		
	end
	SKIN:Bang('!SetOption Flags Text '..string.format('%03d', iFlags))
	Message('Close')
	
	-- All flagging or unflagging updates the flag counter display in the
	-- skin, and dismisses feedback messages left over from the previous
	-- action.
	
end

function NewGame()
	for i = 1, #tSquares do
		Render('Opaque', i)
	end
	Message('Close')
	SKIN:Bang('!SetOption Background SolidColor "#ColorBackground#"')
	SKIN:Bang('#CloseMenu#')
	Initialize()
	
	-- The "new game" function resets the skin to the state it was in
	-- when the skin was first loaded. All squares are rendered as opaque,
	-- feedback messages are dismissed, the menu is closed, and the
	-- background color loses its win/loss highlighting. Once that's done,
	-- the entire initialization function is run again, resetting global
	-- variables and rebuilding the database from scratch.
	
end

----------------------------------------------------------------------------------------------
-- HELPER FUNCTIONS
-- These scripts have been split into separate functions because they're used in more than one
-- place; their parameters change depending on what kind of information they're given by their
-- "parent" functions. Defining them in this way means that we only have to write the same
-- code one time, instead of pasting multiple copies in different places.

function DetectLevel(r, c, m)
	if     r == 9  and c == 9  and m == 10 then
		return 'Beginner'
	elseif r == 16 and c == 16 and m == 40 then
		return 'Intermediate'
	elseif r == 16 and c == 30 and m == 99 then
		return 'Advanced'
	else
		return 'Custom'
	end
	
	-- This function takes a number of rows, columns and mines, and
	-- determines whether they correspond to one of the standardized
	-- difficulty levels. This is used in two places: in Initialize(),
	-- to get the level of the current game, and in Settings(), to get
	-- the level of the new settings chosen by the player.
	
end

function CoordsToSquare(x,y)
	local z = x + y*iCols + 1
	return z
	
	-- This function takes the X and Y coordinates of a square and
	-- calculates the square's linear index number (Z) - the reverse of
	-- the formula in Initialize() that gets X and Y from Z. This is used
	-- several times in Adjacents() to identify one square based on its
	-- spacial relationship to another.
	
end

function Adjacents(z, sRequest)

	---------------------------------------------------------------------
	-- CREATE TABLE OF ADJACENT SQUARES

	local tAdjacents = {}
	
	-- This function's most basic purpose is to determine the immediate
	-- neighbors of a given square. As usual when dealing with a set of
	-- multiple data points, we create a table to contain the results.
	
	local x = tSquares[z]['x']
	local y = tSquares[z]['y']
	if x > 0         and y > 0         then table.insert(tAdjacents, CoordsToSquare(x - 1, y - 1)) end
	if                   y > 0         then table.insert(tAdjacents, CoordsToSquare(x    , y - 1)) end
	if x < (iCols-1) and y > 0         then table.insert(tAdjacents, CoordsToSquare(x + 1, y - 1)) end
	if x < (iCols-1)                   then table.insert(tAdjacents, CoordsToSquare(x + 1, y    )) end
	if x < (iCols-1) and y < (iRows-1) then table.insert(tAdjacents, CoordsToSquare(x + 1, y + 1)) end
	if                   y < (iRows-1) then table.insert(tAdjacents, CoordsToSquare(x    , y + 1)) end
	if x > 0         and y < (iRows-1) then table.insert(tAdjacents, CoordsToSquare(x - 1, y + 1)) end
	if x > 0                           then table.insert(tAdjacents, CoordsToSquare(x - 1, y    )) end
	
	-- This is probably the most complicated section of the entire script.
	-- Every square may have as many as 8 neighbors, and we check whether
	-- each one exists based on the square's coordinates. For example, if
	-- a square's X=5, we know that another square must exist to its
	-- immediate left at X=4. This is also a necessary, but not
	-- sufficient, condition for the squares to the upper-left and lower-
	-- left, which also depend on the square's Y. Here's a diagram of all
	-- of a square's possible neighbors:
	
	-- [X-1, Y-1]   [X, Y-1]     [X+1, Y-1]
	-- Upper-Left   Upper        Upper-Right
	--
	-- [X-1, Y]     [X, Y]       [X+1, Y]
	-- Left                      Right
	--
	-- [X-1, Y+1]   [X, Y+1]     [X+1, Y+1]
	-- Lower-Left   Lower        Lower-Right
	
	-- For the right and bottom edges, we subtract 1 from the number of
	-- columns and rows because the coordinates start at 0, not 1. For
	-- example, in a game with 16 columns, a square on the right edge
	-- would have X=15. This might seem inconvenient, but it actually
	-- makes the math easier in other places.
	
	-- Once we know that a certain neighboring square exists, we enter
	-- its coordinates into the "Adjacents" table, using the earlier
	-- "CoordsToSquare" function to convert its known X-Y coordinates
	-- into its Z index number.
	
	---------------------------------------------------------------------
	-- COUNT THREATS
	
	local iThreats = 0
	for i,v in ipairs(tAdjacents) do
		if tSquares[v]['m'] == 1 then
			iThreats = iThreats + 1
		end
	end
	
	-- Now that we have a complete list of this square's neighbors, we
	-- can quickly loop through them and count how many of them are mines.
	
	---------------------------------------------------------------------
	-- PERFORM REQUESTED ACTIONS
	
	if sRequest == 'Clear' and iThreats == 0 then
		for i,v in ipairs(tAdjacents) do
			if tSquares[v]['m'] == 0 and tSquares[v]['f'] == 0 and tSquares[v]['c'] == 0 then
				iCleared = iCleared + 1
				tSquares[v]['c'] = 1
				Render('Clear', v)
				Adjacents(v, 'Clear')
			end
		end
		
		-- This action is requested by the Clear() function. When a
		-- square is successfully cleared, and has no adjacent mines, we
		-- also clear the surrounding squares as a courtesy, to save
		-- time. This function is called recursively until no more
		-- unthreatened squares can be found.
		
	elseif sRequest == 'Threats' then
		return iThreats
	else
		return tAdjacents
	end
end

--------------------------------------------------------------------------------------------

function Render(sRequest, z)
	if sRequest == 'Opaque' then
		SKIN:Bang('!SetOption '..z..' SolidColor "#ColorSquare#"')
		SKIN:Bang('!SetOption '..z..' Text ""')
		SKIN:Bang('!SetOption '..z..' ToolTipTitle ""')
		SKIN:Bang('!SetOption '..z..' ToolTipText ""')
		SKIN:Bang('!SetOption '..z..' MouseOverAction """[!SetOption #*CURRENTSECTION*# SolidColor "#ColorSquareRevealed#"][!Update]"""')
		SKIN:Bang('!SetOption '..z..' MouseLeaveAction """[!SetOption #*CURRENTSECTION*# SolidColor "#ColorSquare#"][!Update]"""')
	elseif sRequest == 'Clear' then
		SKIN:Bang('!SetOption '..z..' SolidColor "#ColorSquareClear#"')
		if tSquares[z]['n'] > 0 then
			SKIN:Bang('!SetOption '..z..' Text "'..tSquares[z]['n']..'"')
		end
		SKIN:Bang('!SetOption '..z..' MouseOverAction "[]"')
		SKIN:Bang('!SetOption '..z..' MouseLeaveAction "[]"')
	elseif sRequest == 'Flag' then
		SKIN:Bang('!SetOption '..z..' SolidColor "#ColorSquareFlag#"')
		SKIN:Bang('!SetOption '..z..' MouseOverAction "[]"')
		SKIN:Bang('!SetOption '..z..' MouseLeaveAction "[]"')
	elseif sRequest == 'Question' then
		SKIN:Bang('!SetOption '..z..' SolidColor "#ColorSquareQuestion#"')
		SKIN:Bang('!SetOption '..z..' Text "?"')
	elseif sRequest == 'DefusedMine' then
		SKIN:Bang('!SetOption '..z..' SolidColor "#ColorSquareMineDefused#"')
		SKIN:Bang('!SetOption '..z..' ToolTipTitle "Defused Mine"') 
		SKIN:Bang('!SetOption '..z..' ToolTipText "You defused this mine. Nicely done."')
		SKIN:Bang('!SetOption '..z..' MouseOverAction "[]"')
		SKIN:Bang('!SetOption '..z..' MouseLeaveAction "[]"')
	elseif sRequest == 'TrippedMine' then
		SKIN:Bang('!SetOption '..z..' SolidColor "#ColorSquareMineTripped#"')
		SKIN:Bang('!SetOption '..z..' ToolTipTitle "Tripped Mine"') 
		SKIN:Bang('!SetOption '..z..' ToolTipText "You stepped on this mine. It\'s ok. Lots of people live without legs."')
		SKIN:Bang('!SetOption '..z..' MouseOverAction "[]"')
		SKIN:Bang('!SetOption '..z..' MouseLeaveAction "[]"')
	elseif sRequest == 'UntrippedMine' then
		SKIN:Bang('!SetOption '..z..' SolidColor "#ColorSquareMine#"') 
		SKIN:Bang('!SetOption '..z..' ToolTipTitle "Mine"') 
		SKIN:Bang('!SetOption '..z..' ToolTipText "There was a mine here."')
		SKIN:Bang('!SetOption '..z..' MouseOverAction """[!SetOption #*CURRENTSECTION*# SolidColor "255,0,0"][!Update]"""')
		SKIN:Bang('!SetOption '..z..' MouseLeaveAction """[!SetOption #*CURRENTSECTION*# SolidColor "#ColorSquareMine#"][!Update]"""')
	elseif sRequest == 'RightFlag' then
		SKIN:Bang('!SetOption '..z..' ToolTipTitle "Flag"') 
		SKIN:Bang('!SetOption '..z..' ToolTipText "You correctly identified this mine. Or, you just got lucky. But we won\'t hold it against you."')
		SKIN:Bang('!SetOption '..z..' MouseOverAction """[!SetOption #*CURRENTSECTION*# SolidColor "255,0,0"][!Update]"""')
		SKIN:Bang('!SetOption '..z..' MouseLeaveAction """[!SetOption #*CURRENTSECTION*# SolidColor "#ColorSquareFlag#"][!Update]"""')
	elseif sRequest == 'WrongFlag' then
		SKIN:Bang('!SetOption '..z..' SolidColor "#ColorSquareFlagWrong#"') 
		SKIN:Bang('!SetOption '..z..' ToolTipTitle "False Positive"') 
		SKIN:Bang('!SetOption '..z..' ToolTipText "You flagged this spot, but there was no mine here."')
		SKIN:Bang('!SetOption '..z..' MouseOverAction """[!SetOption #*CURRENTSECTION*# SolidColor "#ColorSquareClear#"][!Update]"""')
		SKIN:Bang('!SetOption '..z..' MouseLeaveAction """[!SetOption #*CURRENTSECTION*# SolidColor "#ColorSquareFlagWrong#"][!Update]"""')
	elseif sRequest == 'Remainder' then
		SKIN:Bang('!SetOption '..z..' SolidColor "#ColorSquareRevealed#"')
		SKIN:Bang('!SetOption '..z..' MouseOverAction """[!SetOption #*CURRENTSECTION*# SolidColor "#ColorSquareClear#"][!Update]"""')
		SKIN:Bang('!SetOption '..z..' MouseLeaveAction """[!SetOption #*CURRENTSECTION*# SolidColor "#ColorSquareRevealed#"][!Update]"""')
	end
end

function Message(sRequest)
	SKIN:Bang('!SetOption Message FontColor "#ColorText#"')
	SKIN:Bang('!SetOption Message Text ""')
	SKIN:Bang('!HideMeterGroup MessageConfirm')
	if sRequest == 'Close' then
		SKIN:Bang('!HideMeterGroup Message')
		return
	elseif sRequest == 'Suicide' then
		SKIN:Bang('!SetOption Message FontColor "#ColorSquareMineTripped#"')
		SKIN:Bang('!SetOption Message Text "I\'m sorry, Dave. I can\'t let you do that."')
	elseif sRequest == 'TooFewFlags' then
		SKIN:Bang('!SetOption Message FontColor "#ColorSquareMineTripped#"')
		SKIN:Bang('!SetOption Message Text "Too few flags."')
	elseif sRequest == 'TooFewFlagsAll' then
		SKIN:Bang('!SetOption Message FontColor "#ColorSquareMineTripped#"')
		SKIN:Bang('!SetOption Message Text "You still have '..iFlags..' unflagged mines."')
	elseif sRequest == 'Win' then
		SKIN:Bang('!SetOption Message FontColor "#ColorSquareMineDefused#"')
		SKIN:Bang('!SetOption Message Text "You won!"')
	elseif sRequest == 'Lose' then
		SKIN:Bang('!SetOption Message FontColor "#ColorSquareMineTripped#"')
		SKIN:Bang('!SetOption Message Text "Yeah, that was a mine. Sorry."')
	elseif sRequest == 'TooFewRows' then
		SKIN:Bang('!SetOption Message FontColor "#ColorSquareMineTripped#"')
		SKIN:Bang('!SetOption Message Text "You need at least 9 rows."')
	elseif sRequest == 'TooManyRows' then
		SKIN:Bang('!SetOption Message FontColor "#ColorSquareMineTripped#"')
		SKIN:Bang('!SetOption Message Text "You can only have 24 rows."')
	elseif sRequest == 'TooFewCols' then
		SKIN:Bang('!SetOption Message FontColor "#ColorSquareMineTripped#"')
		SKIN:Bang('!SetOption Message Text "You need at least 9 columns."')
	elseif sRequest == 'TooManyCols' then
		SKIN:Bang('!SetOption Message FontColor "#ColorSquareMineTripped#"')
		SKIN:Bang('!SetOption Message Text "You can only have 30 columns."')
	elseif sRequest == 'TooFewMines' then
		SKIN:Bang('!SetOption Message FontColor "#ColorSquareMineTripped#"')
		SKIN:Bang('!SetOption Message Text "You need at least 1 mine."')
	elseif sRequest == 'TooManyMines' then
		SKIN:Bang('!SetOption Message FontColor "#ColorSquareMineTripped#"')
		SKIN:Bang('!SetOption Message Text "You can\'t have more mines than squares."')
	elseif sRequest == 'ResetConfirm' then
		SKIN:Bang('!SetOption Message Text "Are you sure?"')
		SKIN:Bang('!ShowMeterGroup MessageConfirm')
	end
	SKIN:Bang('!ShowMeterGroup Message')
end

----------------------------------------------------------------------------------------------

function Settings(sSetting, sInput)
	local iInput = tonumber(sInput)
	local iWriteNumberOfRows = tonumber(SKIN:GetVariable('WriteNumberOfRows'))
	local iWriteNumberOfCols = tonumber(SKIN:GetVariable('WriteNumberOfCols'))
	local iWriteNumberOfMines = tonumber(SKIN:GetVariable('WriteNumberOfMines'))
	local iWriteQuestions = tonumber(SKIN:GetVariable('WriteQuestions'))
	
	if sSetting == 'Rows' then
		if iInput < 9 then
			iWriteNumberOfRows = 9
			Message('TooFewRows')
		elseif iInput > 24 then
			iWriteNumberOfRows = 24
			Message('TooManyRows')
		else
			iWriteNumberOfRows = iInput
			Message('Close')
		end
		SKIN:Bang('!SetVariable WriteNumberOfRows '..iWriteNumberOfRows)
		if iWriteNumberOfMines > iWriteNumberOfRows * iWriteNumberOfCols - 1 then
			iWriteNumberOfMines = iWriteNumberOfRows * iWriteNumberOfCols - 1
			SKIN:Bang('!SetVariable WriteNumberOfMines '..iWriteNumberOfMines)
		end
		
	elseif sSetting == 'Cols' then
		if iInput < 9 then
			iWriteNumberOfCols = 9
			Message('TooFewCols')
		elseif iInput > 30 then
			iWriteNumberOfCols = 30
			Message('TooManyCols')
		else
			iWriteNumberOfCols = iInput
			Message('Close')
		end
		SKIN:Bang('!SetVariable WriteNumberOfCols '..iWriteNumberOfCols)
		if iWriteNumberOfMines > iWriteNumberOfRows * iWriteNumberOfCols - 1 then
			iWriteNumberOfMines = iWriteNumberOfRows * iWriteNumberOfCols - 1
			SKIN:Bang('!SetVariable WriteNumberOfMines '..iWriteNumberOfMines)
		end
		
	elseif sSetting == 'Mines' then
		msMaxMines = SKIN:GetMeasure('MeasureCalcMaxMines')
		iMaxMines = tonumber(msMaxMines:GetStringValue())
		if iInput < 1 then
			iWriteNumberOfMines = 1
			Message('TooFewMines')
		elseif iInput > iMaxMines then
			iWriteNumberOfMines = iMaxMines
			Message('TooManyMines')
		else
			iWriteNumberOfMines = iInput
			Message('Close')
		end
		SKIN:Bang('!SetVariable WriteNumberOfMines '..iWriteNumberOfMines)
		
	elseif sSetting == 'Apply' then
		local iSquareSize = tonumber(SKIN:GetVariable('SquareSize'))
		local iSquareMargin = tonumber(SKIN:GetVariable('SquareMargin'))
		local iSkinWidth = (iSquareSize + iSquareMargin) * iWriteNumberOfCols + iSquareMargin
		local iSkinHeight = (iSquareSize + iSquareMargin) * iWriteNumberOfRows + iSquareMargin + 70
		
		local iWriteInputX = iSkinWidth - 118
		local iWriteInputY1 = iSkinHeight - 234
		local iWriteInputY2 = iWriteInputY1 + 36
		local iWriteInputY3 = iWriteInputY2 + 36
		
		SKIN:Bang('!WriteKeyValue Variables NumberOfRows '..iWriteNumberOfRows..' "#CURRENTPATH#Settings.inc"')
		SKIN:Bang('!WriteKeyValue Variables NumberOfCols '..iWriteNumberOfCols..' "#CURRENTPATH#Settings.inc"')
		SKIN:Bang('!WriteKeyValue Variables NumberOfMines '..iWriteNumberOfMines..' "#CURRENTPATH#Settings.inc"')
		SKIN:Bang('!WriteKeyValue Variables Questions '..iWriteQuestions..' "#CURRENTPATH#Settings.inc"')
		SKIN:Bang('!WriteKeyValue Variables InputX '..iWriteInputX..' "#CURRENTPATH#Settings.inc"')
		SKIN:Bang('!WriteKeyValue Variables InputY1 '..iWriteInputY1..' "#CURRENTPATH#Settings.inc"')
		SKIN:Bang('!WriteKeyValue Variables InputY2 '..iWriteInputY2..' "#CURRENTPATH#Settings.inc"')
		SKIN:Bang('!WriteKeyValue Variables InputY3 '..iWriteInputY3..' "#CURRENTPATH#Settings.inc"')
		return
	end
	
	local sWriteLevel = DetectLevel(iWriteNumberOfRows, iWriteNumberOfCols, iWriteNumberOfMines)
	local sColorLevel1 = sWriteLevel == 'Beginner'     and '#ColorSquareFlag#' or '#ColorTextDim#'
	local sColorLevel2 = sWriteLevel == 'Intermediate' and '#ColorSquareFlag#' or '#ColorTextDim#'
	local sColorLevel3 = sWriteLevel == 'Advanced'     and '#ColorSquareFlag#' or '#ColorTextDim#'
	SKIN:Bang('!SetOption SettingsBeginner FontColor "'..sColorLevel1..'"')
	SKIN:Bang('!SetOption SettingsIntermediate FontColor "'..sColorLevel2..'"')
	SKIN:Bang('!SetOption SettingsAdvanced FontColor "'..sColorLevel3..'"')
end

--------------------------------------------------------------------------------------------

function WriteSetVariable(sKey, sValue, sFile)
	SKIN:Bang('!SetVariable "'..sKey..'" "'..sValue..'"')
	SKIN:Bang('!WriteKeyValue Variables "'..sKey..'" "'..sValue..'" "'..sFile..'"')
end

function Scores(sShowLevel, sRequest)
	if sShowLevel ~= 'Custom' then
		local iPlayed = tonumber(SKIN:GetVariable(sShowLevel..'Played'))
		local iWon = tonumber(SKIN:GetVariable(sShowLevel..'Won'))
		local iStreak = tonumber(SKIN:GetVariable(sShowLevel..'Streak'))
		local iMostWins = tonumber(SKIN:GetVariable(sShowLevel..'MostWins'))
		local iMostLosses = tonumber(SKIN:GetVariable(sShowLevel..'MostLosses'))
		local tScores = {}
		for i = 1,5 do
			tScores[i] = {}
			tScores[i]['time'] = tonumber(SKIN:GetVariable(sShowLevel..'Time'..i))
			tScores[i]['date'] = SKIN:GetVariable(sShowLevel..'Date'..i)
		end
		
		if sRequest then
			local sCurrentPath = SKIN:GetVariable('CURRENTPATH')
			local sSettings = sCurrentPath..'Settings.inc'
			if sRequest == 'Update' then
				
				
				iPlayed = iPlayed + 1
				
				if iGameOver == 1 then
					iWon = iWon + 1
					if iStreak > 0 then
						iStreak = iStreak + 1
					else
						iStreak = 1
					end
					if iStreak > iMostWins then iMostWins = iStreak end
					if iTimer < tScores[5]['time'] then
						msDate = SKIN:GetMeasure('MeasureDate')
						sDate = msDate:GetStringValue()
						table.insert(tScores, { ['time']=iTimer, ['date']=sDate })
						table.sort(tScores, function(a,b) return a['time'] < b['time'] end)
					end
				else
					if iStreak < 0 then
						iStreak = iStreak - 1
					else
						iStreak = -1
					end
					if math.abs(iStreak) > iMostLosses then iMostLosses = math.abs(iStreak) end
					
				end
			
			elseif sRequest == 'Reset' then
				iPlayed = 0
				iWon = 0
				iStreak = 0
				iMostWins = 0
				iMostLosses = 0
				for i = 1,5 do
					tScores[i]['time'] = 999
					tScores[i]['date'] = 'Never'
				end
			end
			
			WriteSetVariable(sShowLevel..'Played', iPlayed, sSettings)
			WriteSetVariable(sShowLevel..'Won', iWon, sSettings)
			WriteSetVariable(sShowLevel..'Streak', iStreak, sSettings)
			WriteSetVariable(sShowLevel..'MostWins', iMostWins, sSettings)
			WriteSetVariable(sShowLevel..'MostLosses', iMostLosses, sSettings)
			for i = 1,5 do
				WriteSetVariable(sShowLevel..'Time'..i, tScores[i]['time'], sSettings)
				WriteSetVariable(sShowLevel..'Date'..i, tScores[i]['date'], sSettings)
			end
		end
		
		iPercent = iPlayed > 0 and math.ceil((iWon / iPlayed) * 10000) / 100 or 0
		
		SKIN:Bang('!SetOption ScoresPlayedValue Text '..iPlayed)
		SKIN:Bang('!SetOption ScoresWonValue Text "'..iPercent..'%"')
		SKIN:Bang('!SetOption ScoresWonValue ToolTipText "'..iWon..' of '..iPlayed)
		SKIN:Bang('!SetOption ScoresStreakValue Text '..iStreak)
		SKIN:Bang('!SetOption ScoresMostWinsValue Text '..iMostWins)
		SKIN:Bang('!SetOption ScoresMostLossesValue Text '..iMostLosses)
		SKIN:Bang('!SetOption ScoresBestTimeValue Text '..tScores[1]['time'])
		SKIN:Bang('!SetOption ScoresBestTimeValue ToolTipText "'..tScores[1]['time']..'#CRLF#'..tScores[1]['date']..'#CRLF##CRLF#'..tScores[2]['time']..'#CRLF#'..tScores[2]['date']..'#CRLF##CRLF#'..tScores[3]['time']..'#CRLF#'..tScores[3]['date']..'#CRLF##CRLF#'..tScores[4]['time']..'#CRLF#'..tScores[4]['date']..'#CRLF##CRLF#'..tScores[5]['time']..'#CRLF#'..tScores[5]['date']..'"')
		SKIN:Bang('!SetOption ScoresReset FontColor "#ColorTextBright#"')
		SKIN:Bang('!SetOption MessageYes LeftMouseUpAction """[!CommandMeasure MeasureScript Message(\'Close\')][!CommandMeasure MeasureScript "Scores(\''..sShowLevel..'\', \'Reset\')"][!Update]"""')
		SKIN:Bang('!SetOption ScoresReset ToolTipText "Click to reset your statistics for the '..sShowLevel..' level."')
	end
	
	local sColorLevel1 = sShowLevel == 'Beginner'     and '#ColorSquareFlag#' or '#ColorTextDim#'
	local sColorLevel2 = sShowLevel == 'Intermediate' and '#ColorSquareFlag#' or '#ColorTextDim#'
	local sColorLevel3 = sShowLevel == 'Advanced'     and '#ColorSquareFlag#' or '#ColorTextDim#'
	SKIN:Bang('!SetOption ScoresBeginner FontColor "'..sColorLevel1..'"')
	SKIN:Bang('!SetOption ScoresIntermediate FontColor "'..sColorLevel2..'"')
	SKIN:Bang('!SetOption ScoresAdvanced FontColor "'..sColorLevel3..'"')
end

Photo

Michael Engard was born shortly before the Berlin Wall came down. By day, he does web design and development for a marketing agency in northern Virginia. On weekends, he helps his local Starbucks fulfill its legal obligation to have at least one pretentious introvert writing a novel in the corner. He uses words like “tautological” in conversation. He will pet your dog.

Kaelri is an Internet pseudonym. It means nothing, and you’re welcome to pronounce it however you want to.