hetris-0.1: src/Hetris.lhs
% vim: set tw=72:
% Part of Hetris
\section{The heart of the game}
All the modules dealing with the various pieces of the game are now
complete leaving only the central policy module, the very heart of the
game, left to write. This will serve as \hsmodule{Main} so the header is
essentially already fixed for us.
\begin{code}
module Main (main) where
\end{code}
We pull together all of the abstract modules here, so we start by
importing them all. We also import \hsmodule{Random} as we are going to
want to be able to select a random piece to become the new active piece.
\begin{code}
import Data
import Pieces
import Board
import UI
import System.Random
\end{code}
In this simplified variant the time between ticks is a constant, but in
a more sophisticated variant it might be a function on factors such as
the score. In either case it makes sense to separate this functionality
out into a function so it can easily be tweaked for good playability.
\begin{code}
start_delay :: Delay
start_delay = 1000
\end{code}
Similarly we separate out the desired width and height.
\begin{code}
desired_dimensions :: (Vector, Vector)
desired_dimensions = (9, 12)
\end{code}
We are going to need to be able to get a random new piece both when we
create the \hstype{Board} and when we find we need to add a new piece on
a \hsconstructor{Tick} event. Thus it makes sense to split the code for
doing so off into a separate function.
We use the random number generator in the \hstype{IO} monad so we return
an \hstype{IO Piece} rather than just a \hstype{Piece}. We pick a random
number in the range of the elements of the \hsfunction{pieces} list,
exported by \hsmodule{Pieces}, and return the element at that position in
the list.
\begin{code}
get_new_piece :: IO Piece
get_new_piece = randomRIO (0, length pieces - 1) >>= (return . (pieces !!))
\end{code}
The \hsfunction{main} function first performs an initialisation phase.
The first task is to initialise the user interface, noting the maximum
width and height board it can cope with. It then gets a new piece and
makes a board, as large as possible while not more than the desired
dimensions, with this as the initial piece. The user interface is then
asked to perform the relevant changes.
The next phase is performed by a function roughly equivalent to an event
loop. It takes the current representation of the board and the time
until the next tick event---in this case the time between ticks---and
deals with events as they happen.
When the loop finishes we enter the final phase; we tell the user
interface to shut down and then the program terminates.
\begin{code}
main :: IO ()
main = do (width_ui, height_ui) <- init_ui
let (width_des, height_des) = desired_dimensions
let width = width_ui `min` width_des
height = height_ui `min` height_des
make_board width height
piece <- get_new_piece
let (b, cs) = create_board width height piece
do_changes cs
event_loop b start_delay
shutdown_ui
return ()
\end{code}
\begin{code}
{-
main :: IO ()
main = do (width, height) <- init_ui
make_board width height
piece <- get_new_piece
let (b, cs) = create_board width height piece
do_changes cs
event_loop b start_delay
shutdown_ui
return ()
-}
\end{code}
The actual event loop is complicated mainly by special cases. It starts
by getting the next event, passing \hsfunction{get\_event} the time
until the next tick event is due. The event that occurred and the time
that elapsed are returned. If the event was \hsconstructor{Quit} then
the loop terminates. Otherwise more complex handling is needed.
If the elapsed time is less than the time until the next tick event and
the event wasn't \hsconstructor{Tick} then we subtract the elapsed time
from the time until the next tick event to get the new time until the
next tick and leave the event unchanged. Otherwise we have either
overrun our allocated time (XXX could lose events here) or we have
received a \hsconstructor{Tick} event; in either case we reset the time
until the next tick and continue as if we had received a
\hsconstructor{Tick} event.
If we are dealing with a \hsconstructor{Tick} event and the current
piece can't be moved down we get a new piece and use the
\hsfunction{next\_piece} function to add it to the board. If this
succeeds then we call ourselves recursively---the next iteration of the
event loop. Otherwise the game is over so we leave the loop.
Otherwise we can just use \hsfunction{get\_changes} and
\hsfunction{do\_changes} to work out and apply the changes needed
respectively. Then we continue with the next iteration of the event
loop.
\begin{code}
event_loop :: Board -> Delay -> IO ()
event_loop b d = do (e, elapsed) <- get_event d
if e == Quit
then return ()
else do let (d', e') = if elapsed < d && e /= Tick
then (d - elapsed, e)
else (start_delay, Tick)
if e' == Tick && not (can_down b)
then do piece <- get_new_piece
let (m_b', cs) = next_piece b piece
do_changes cs
case m_b' of
Just b' -> event_loop b' d'
Nothing -> return ()
else do let (b', cs) = get_changes b e'
do_changes cs
event_loop b' d'
\end{code}