Writer and web developer.

Rainsweeper

Featured image

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
© 2021