stuff
This commit is contained in:
parent
935ee6f7a1
commit
aa602c2b7f
|
@ -1 +1,6 @@
|
|||
.stack-work
|
||||
.stack-work
|
||||
report/*.aux
|
||||
report/*.log
|
||||
report/*.synctex.gz
|
||||
report/*.toc
|
||||
report/*.out
|
Binary file not shown.
|
@ -0,0 +1,179 @@
|
|||
\documentclass[11pt]{article}
|
||||
|
||||
\usepackage[margin=0.7in]{geometry}
|
||||
\usepackage{titling}
|
||||
\usepackage{listings}
|
||||
\usepackage{graphicx}
|
||||
\usepackage{hyperref}
|
||||
\usepackage{xcolor}
|
||||
\usepackage{url}
|
||||
\usepackage{enumitem}
|
||||
|
||||
\definecolor{lightblue}{RGB}{0,130,186}
|
||||
\definecolor{darkgreen}{RGB}{0,114,0}
|
||||
\definecolor{darkpurple}{RGB}{125,0,183}
|
||||
|
||||
\lstset{
|
||||
language=Haskell,
|
||||
tabsize=4,
|
||||
basicstyle=\color{black}\footnotesize\ttfamily,
|
||||
keywordstyle=\color{blue}\footnotesize\ttfamily, % style for keywords
|
||||
identifierstyle=\color{purple}\footnotesize\ttfamily,
|
||||
commentstyle=\color{lightblue}\footnotesize\ttfamily,
|
||||
numbers=left, % where to put the line-numbers
|
||||
numberstyle=\footnotesize\ttfamily, % the size of the fonts that are used for the line-numbers
|
||||
showstringspaces=false
|
||||
}
|
||||
|
||||
\hypersetup{
|
||||
colorlinks,
|
||||
citecolor=black,
|
||||
filecolor=black,
|
||||
linkcolor=black,
|
||||
urlcolor=black
|
||||
}
|
||||
|
||||
\setlength{\droptitle}{-6em}
|
||||
\setlength{\parindent}{0cm}
|
||||
\setlength{\righthyphenmin}{62}
|
||||
\setlength{\lefthyphenmin}{62}
|
||||
|
||||
\title{CSU44012 Topics in Functional Programming\\Assignment \#2\\Minesweeper}
|
||||
\author{Jack Harley jharley@tcd.ie | Student No. 16317123}
|
||||
\date{January 2021}
|
||||
|
||||
\begin{document}
|
||||
\maketitle
|
||||
|
||||
\includegraphics[width=12cm]{screenshot.png}
|
||||
|
||||
\tableofcontents
|
||||
|
||||
\newpage
|
||||
|
||||
\setlength{\parskip}{1em}
|
||||
|
||||
\section{Introduction}
|
||||
I have implemented a fully functional Minesweeper game in Haskell with the Threepenny GUI serving the interface.
|
||||
|
||||
Stack successfully builds and executes the solution binary, serving the GUI at \url{http://localhost:8023}.
|
||||
|
||||
I attempted to integrate it into an Electron application, so that the interface would launch automatically into an Electron window (embedded Chrome, see \url{https://www.electronjs.org/}) but unfortunately realized partway through that the effort required to get it working was likely to be disproportionate to the improvement in functionality.
|
||||
|
||||
\section{Design and Implementation}
|
||||
I will cover a few of the more important functions in some detail in this section. The comments included in the source files should be sufficient to explain the simpler function.
|
||||
|
||||
\subsection{Basic Minesweeper Model}
|
||||
The model for the game is implemented in Minesweeper.hs.
|
||||
|
||||
I modelled the game board as an ADT with 4 fields:
|
||||
\begin{lstlisting}
|
||||
data Board = Board { size :: Int, mines :: Grid, uncovered :: Grid, flagged :: Grid }
|
||||
\end{lstlisting}
|
||||
|
||||
\textbf{size} defines the horizontal and vertical length in squares of the game grid (all grids are squares).
|
||||
\textbf{mines}, \textbf{uncovered} and \textbf{flagged} hold a data structure indicating the squares that respectively have mines, have been uncovered by the user and have been flagged by the user.
|
||||
|
||||
The Grid type is a 2D list of Booleans with the outer list denoting rows and the inner list denoting columns:
|
||||
|
||||
\begin{lstlisting}
|
||||
type Grid = [[Bool]]
|
||||
\end{lstlisting}
|
||||
|
||||
For example, you could determine whether the 2nd row down, 4th column across has a mine with the following expression: (N.B. rows and columns are 0-indexed)
|
||||
\begin{lstlisting}
|
||||
(mines !! 1) !! 3
|
||||
\end{lstlisting}
|
||||
|
||||
And indeed this is how the hasMine function is implemented:
|
||||
\begin{lstlisting}
|
||||
hasMine :: Board -> Square -> Bool
|
||||
hasMine b (r,c) | validSquare b (r,c) = (mines b !! r) !! c
|
||||
\end{lstlisting}
|
||||
|
||||
Throughout my implementation, particular squares are referred to with a 2-tuple defining first the row and then the column as 0-indexed integers, with (0,0) being the square at the top left of the board:
|
||||
|
||||
\begin{lstlisting}
|
||||
type Square = (Int, Int)
|
||||
\end{lstlisting}
|
||||
|
||||
\newpage
|
||||
|
||||
\subsection{Creating a Game}
|
||||
A fresh game board is initialised with the createBoard function:
|
||||
|
||||
\begin{lstlisting}
|
||||
createBoard :: Int -> Float -> StdGen -> Board
|
||||
createBoard size mineRatio rng = Board size
|
||||
(seedGrid rng mineRatio (createGrid False size))
|
||||
(createGrid False size)
|
||||
(createGrid False size)
|
||||
\end{lstlisting}
|
||||
|
||||
The function requires a size (number of squares in both horizontal and vertical directions), a "mine ratio" and a random number generator instance. It then produces a Board type with three initialised grids. The uncovered and flagged grids are initialised with all False values (since the user will not have uncovered or flagged any squares yet).
|
||||
|
||||
The mines grid is initialised with all False values, but then the seedGrid function is used to randomly seed mines into the grid by making a random decision with probability of the provided mineRatio for every square on the grid.
|
||||
|
||||
For example, with a mine ratio of 0.1, every square will have a one in ten chance of having a mine, and after the decisions have been made for every square, roughly one tenth of the grid will have mines.
|
||||
|
||||
seedGrid works by splitting the random number generator repeatedly, one instance for each row of the grid, and then calling seedList on each row. The seeded rows are then joined back together at the end of the recursion. The full source for seedGrid, seedList and seedList' can be found in the appendix and project files.
|
||||
|
||||
\subsection{Handling Game Moves}
|
||||
\subsubsection{Uncover}
|
||||
Uncover is triggered in the UI by left clicking on a square. It triggers the following function:
|
||||
|
||||
\begin{lstlisting}
|
||||
uncover :: Board -> Square -> Board
|
||||
uncover b (r,c) | not $ validSquare b (r,c) = b
|
||||
| isUncovered b (r,c) = b
|
||||
| hasMine b (r,c) = let Board s m u f = b
|
||||
in Board s m (createGrid True s) f
|
||||
| otherwise = let Board s m u f = b
|
||||
(rowsA, row : rowsB) = splitAt r u
|
||||
(cellsA, _ : cellsB) = splitAt c row
|
||||
newRow = cellsA ++ True : cellsB
|
||||
newRows = rowsA ++ newRow : rowsB
|
||||
in uncoverAdjacentsIfSafe (Board s m newRows f) (r,c)
|
||||
\end{lstlisting}
|
||||
|
||||
The first guard handles cases where the provided Square is not valid (lies outside the edge of the Board), in this case, the Board is returned unchanged.
|
||||
|
||||
The second guard handles cases where the provided Square is already uncovered, and again the board is returned unchanged.
|
||||
|
||||
The third guard handles cases where a user clicks on a mine. In this case the game has ended and the player has lost, therefore the function simply replaces the uncovered Grid with an all True Grid. The user will therefore immediately see the entire grid, including all of the mines.
|
||||
|
||||
The final guard handles normal cases where the Square clicked is safe. It reconstructs the uncovered Grid, replacing the Square at (r,c) with a True status. It then also calls the uncoverAdjacentsIfSafe function on the modified Board:
|
||||
|
||||
\begin{lstlisting}
|
||||
uncoverAdjacentsIfSafe :: Board -> Square -> Board
|
||||
uncoverAdjacentsIfSafe b (r,c) | adjacentMines b (r,c) == 0 = uncoverAll b $ adjacentSquares (r,c)
|
||||
| otherwise = b
|
||||
\end{lstlisting}
|
||||
|
||||
uncoverAdjacentsIfSafe checks if the newly uncovered Square has 0 adjacent mines, and if so, uncovers all of them. This can trigger recursion where large parts of the Board will be uncovered.
|
||||
|
||||
\subsubsection{Flag}
|
||||
Flag is triggered in the UI by right clicking on a square. It is intended to be used when a user wants to mark a square they think has a mine. It triggers the following function:
|
||||
|
||||
\begin{lstlisting}
|
||||
flag :: Board -> Square -> Board
|
||||
flag b (r,c) | not $ validSquare b (r,c) = b
|
||||
| isUncovered b (r,c) = b
|
||||
| isFlagged b (r,c) = b
|
||||
| otherwise = let Board s m u f = b
|
||||
(rowsA, row : rowsB) = splitAt r f
|
||||
(cellsA, _ : cellsB) = splitAt c row
|
||||
newRow = cellsA ++ True : cellsB
|
||||
newRows = rowsA ++ newRow : rowsB
|
||||
in Board s m u newRows
|
||||
\end{lstlisting}
|
||||
|
||||
Flag works similarly to uncover.
|
||||
|
||||
If the square is not valid, already uncovered or already flagged then the board is returned unchanged. We could combine these cases into a single guard but I think readability is better with them separately.
|
||||
|
||||
Then we use the same procedure to replace the flagged Grid with a new grid, with the right clicked square's status changed to True.
|
||||
|
||||
\subsection{Rendering the UI}
|
||||
\section{Reflection}
|
||||
\end{document}
|
Binary file not shown.
After Width: | Height: | Size: 50 KiB |
|
@ -24,7 +24,7 @@ setup w = void $ do
|
|||
UI.addStyleSheet w "minesweeper.css"
|
||||
rng <- liftIO newStdGen
|
||||
|
||||
let b = createBoard 20 0.1 rng :: Board
|
||||
let b = createBoard 20 0.08 rng :: Board
|
||||
liftIO $ putStrLn $ printBoard b
|
||||
iob <- liftIO $ newIORef b
|
||||
|
||||
|
@ -37,11 +37,14 @@ setup w = void $ do
|
|||
UI.div # set UI.id_ "infoCont" #+ [
|
||||
UI.p #+ [string "Instructions: Click on a square to uncover it. Right click a square to flag it."],
|
||||
UI.p #+ [string "Flagged squares will turn yellow. If you hit a mine all mines will instantly be revealed as red squares."],
|
||||
UI.p #+ [string "Refresh the page to start a new game."],
|
||||
UI.p #+ [string "You win the game once you have uncovered all squares that do not have mines. If this occurs, the entire board will turn green (except the bomb) to indicate your win!"],
|
||||
UI.p #+ [string "At any time, you can refresh the page to start a new game."],
|
||||
UI.p #+ [string "Good luck!"]
|
||||
]
|
||||
]
|
||||
],
|
||||
|
||||
-- include custom JS at end of body to stop right clicks opening a menu
|
||||
mkElement "script" # set (attr "src") "/static/custom.js"]
|
||||
|
||||
where
|
||||
|
@ -67,6 +70,7 @@ setup w = void $ do
|
|||
|
||||
return cell
|
||||
|
||||
-- refresh the board on screen (rerender)
|
||||
refresh iob = do
|
||||
b <- liftIO $ readIORef iob
|
||||
|
||||
|
|
|
@ -11,15 +11,24 @@ data Board = Board { size :: Int
|
|||
, flagged :: Grid
|
||||
}
|
||||
|
||||
--
|
||||
-- Functions related to creating and initialising a board
|
||||
--
|
||||
|
||||
-- Creates a board given a size (width/height), mine ratio and random generator
|
||||
createBoard :: Int -> Float -> StdGen -> Board
|
||||
createBoard size mineRatio rng = Board size (seedGrid rng mineRatio (createGrid False size)) (createGrid False size) (createGrid False size)
|
||||
createBoard size mineRatio rng = Board size
|
||||
(seedGrid rng mineRatio (createGrid False size))
|
||||
(createGrid False size)
|
||||
(createGrid False size)
|
||||
|
||||
-- Creates a 2D list of booleans of given size, initialised to given boolean
|
||||
createGrid :: Bool -> Int -> Grid
|
||||
createGrid b size = replicate size (replicate size b)
|
||||
|
||||
--
|
||||
-- Functions relating to seeding a grid with mines
|
||||
--
|
||||
|
||||
seedGrid :: StdGen -> Float -> Grid -> Grid
|
||||
seedGrid _ _ [] = []
|
||||
|
@ -35,42 +44,100 @@ seedList' _ _ [] = []
|
|||
seedList' rng p (l:ls) = newBool : seedList' newRng p ls
|
||||
where (newBool, newRng) = weightedRandomBool rng p
|
||||
|
||||
-- Returns true with probability p, otherwise false
|
||||
-- returns True with probability p, otherwise False
|
||||
weightedRandomBool :: StdGen -> Float -> (Bool, StdGen)
|
||||
weightedRandomBool rng p = (generatedFloat <= p, newRng)
|
||||
where (generatedFloat, newRng) = randomR (0.0, 1.0) rng
|
||||
|
||||
--
|
||||
-- Functions for determing statuses and info on square(s)
|
||||
-- N.B. (r,c) = (row, column)
|
||||
--
|
||||
|
||||
-- returns True if the given square has a mine, otherwise False
|
||||
hasMine :: Board -> Square -> Bool
|
||||
hasMine b (r,c) | validSquare b (r,c) = (mines b !! r) !! c
|
||||
| otherwise = False
|
||||
|
||||
-- returns True if the given square is uncovered, otherwise False
|
||||
isUncovered :: Board -> Square -> Bool
|
||||
isUncovered b (r,c) | validSquare b (r,c) = (uncovered b !! r) !! c
|
||||
| otherwise = True
|
||||
| otherwise = True -- We return True when the requested square does not exist as a useful
|
||||
-- hack so that adjacent mine numbers are not shown on the edge of the
|
||||
-- board grid
|
||||
|
||||
-- returns True if the given square is flagged, otherwise False
|
||||
isFlagged :: Board -> Square -> Bool
|
||||
isFlagged b (r,c) | validSquare b (r,c) = (flagged b !! r) !! c
|
||||
| otherwise = False
|
||||
|
||||
-- returns True if the given square is within the bounds of the board
|
||||
validSquare :: Board -> Square -> Bool
|
||||
validSquare b (r,c) = r >= 0 && c >= 0 && r < size b && c < size b
|
||||
|
||||
-- returns True if the given square is on the edge of the board
|
||||
onEdge :: Board -> Square -> Bool
|
||||
onEdge b (r,c) = r == 0 || c == 0 || r+1 == size b || c+1 == size b
|
||||
|
||||
-- returns the number of mines adjacent to the given square
|
||||
adjacentMines :: Board -> Square -> Int
|
||||
adjacentMines b (r,c) = sum $ map (boolToInt . hasMine b) $ adjacentSquares (r,c)
|
||||
|
||||
-- returns true if the given square is adjacent to a covered square
|
||||
adjacentToCovered :: Board -> Square -> Bool
|
||||
adjacentToCovered b (r,c) = not $ all (isUncovered b) $ adjacentSquares (r,c)
|
||||
|
||||
-- returns a list of all the squares directly adjacent to the given square (using arithmetic)
|
||||
adjacentSquares :: Square -> [Square]
|
||||
adjacentSquares (r,c) = [(r-1,c-1), (r-1,c), (r-1,c+1), (r,c-1), (r,c+1), (r+1,c-1), (r+1,c), (r+1,c+1)]
|
||||
|
||||
-- returns 1 for boolean True and 0 for Boolean false
|
||||
boolToInt :: Bool -> Int
|
||||
boolToInt x | x = 1
|
||||
| otherwise = 0
|
||||
|
||||
-- returns true if the game has been won (all remaining covered squares have a mine)
|
||||
gameWon :: Board -> Bool
|
||||
gameWon b = all (hasMine b) (coveredSquares b)
|
||||
|
||||
-- returns a list of all squares on a board currently still covered
|
||||
coveredSquares :: Board -> [Square]
|
||||
coveredSquares (Board _ _ u _) = falseSquares 0 u
|
||||
|
||||
-- returns a list of all squares in a grid with boolean False status
|
||||
falseSquares :: Int -> Grid -> [Square]
|
||||
falseSquares _ [] = []
|
||||
falseSquares r (row:rows) = falseSquares' r 0 row ++ falseSquares (r+1) rows
|
||||
|
||||
-- returns a list of all squares in an individual row of a grid with boolean False status
|
||||
falseSquares' :: Int -> Int -> [Bool] -> [Square]
|
||||
falseSquares' _ _ [] = []
|
||||
falseSquares' r c (col:cols) | not col = (r,c) : falseSquares' r (c+1) cols
|
||||
| otherwise = falseSquares' r (c+1) cols
|
||||
|
||||
--
|
||||
-- Functions for rendering a board to a UI
|
||||
--
|
||||
|
||||
-- returns a string that should be shown in the given square for a UI render of the board
|
||||
-- typically either blank or if bordering on covered squares: the number of adjacent mines
|
||||
squareAscii :: Board -> Square -> String
|
||||
squareAscii b (r,c) | isUncovered b (r,c) && adjacentToCovered b (r,c) = show $ adjacentMines b (r,c)
|
||||
squareAscii b (r,c) | gameWon b = ""
|
||||
| isUncovered b (r,c) && adjacentToCovered b (r,c) = show $ adjacentMines b (r,c)
|
||||
| otherwise = ""
|
||||
|
||||
-- returns a string indicating the bg colour class for a given square for a UI render of the board
|
||||
-- intended to be a used as a CSS class
|
||||
squareBgColour :: Board -> Square -> String
|
||||
squareBgColour b (r,c) | isUncovered b (r,c) && hasMine b (r,c) = "bomb"
|
||||
| isUncovered b (r,c) = "uncovered"
|
||||
| isFlagged b (r,c) = "flagged"
|
||||
| otherwise = "covered"
|
||||
squareBgColour b (r,c) | gameWon b && hasMine b (r,c) = "bg-red"
|
||||
| gameWon b = "bg-green"
|
||||
| isUncovered b (r,c) && hasMine b (r,c) = "bg-red"
|
||||
| isUncovered b (r,c) = "bg-light"
|
||||
| isFlagged b (r,c) = "bg-yellow"
|
||||
| otherwise = "bg-dark"
|
||||
|
||||
-- returns a string indicating the text colour class for a given square for a UI render of the board
|
||||
-- intended to be a used as a CSS class
|
||||
squareTextColour :: Board -> Square -> String
|
||||
squareTextColour b (r,c) | hasMine b (r,c) || isFlagged b (r,c) = ""
|
||||
| isUncovered b (r,c) && adjacentToCovered b (r,c) =
|
||||
|
@ -85,20 +152,9 @@ squareTextColour b (r,c) | hasMine b (r,c) || isFlagged b (r,c) = ""
|
|||
8 -> "text-gray"
|
||||
| otherwise = ""
|
||||
|
||||
adjacentMines :: Board -> Square -> Int
|
||||
adjacentMines b (r,c) = sum $ map (boolToInt . hasMine b) $ adjacentSquares (r,c)
|
||||
|
||||
adjacentToCovered :: Board -> Square -> Bool
|
||||
adjacentToCovered b (r,c) = not $ all (isUncovered b) $ adjacentSquares (r,c)
|
||||
|
||||
adjacentSquares :: Square -> [Square]
|
||||
adjacentSquares (r,c) = [(r-1,c-1), (r-1,c), (r-1,c+1), (r,c-1), (r,c+1), (r+1,c-1), (r+1,c), (r+1,c+1)]
|
||||
|
||||
boolToInt :: Bool -> Int
|
||||
boolToInt x | x = 1
|
||||
| otherwise = 0
|
||||
|
||||
--
|
||||
-- Functions for changing the status of square(s)
|
||||
--
|
||||
|
||||
-- uncovers a square and recursively uncovers adjacent squares iff the square has zero adjacent mines
|
||||
-- N.B. not very efficient due to lots of splitting and remerging
|
||||
|
@ -134,7 +190,9 @@ flag b (r,c) | not $ validSquare b (r,c) = b
|
|||
newRows = rowsA ++ newRow : rowsB
|
||||
in Board s m u newRows
|
||||
|
||||
--
|
||||
-- Functions for turning a board into a string for debug purposes
|
||||
--
|
||||
|
||||
printBoard :: Board -> String
|
||||
printBoard b = printBoardGrid (mines b)
|
||||
|
|
|
@ -11,21 +11,11 @@ td {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
.bomb {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.flagged {
|
||||
background-color: yellow;
|
||||
}
|
||||
|
||||
.covered {
|
||||
background-color: darkgray;
|
||||
}
|
||||
|
||||
.uncovered {
|
||||
background-color: lightgray;
|
||||
}
|
||||
.bg-red { background-color: red; }
|
||||
.bg-yellow { background-color: yellow; }
|
||||
.bg-dark { background-color: darkgray; }
|
||||
.bg-light { background-color: lightgray; }
|
||||
.bg-green { background-color: lime; }
|
||||
|
||||
.text-blue { color: blue; }
|
||||
.text-green { color: green; }
|
||||
|
|
Loading…
Reference in New Issue