imj-game-hamazed-0.1.0.2: src/Imj/Game/Hamazed/World/Space.hs
{-# OPTIONS_HADDOCK hide #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE LambdaCase #-}
module Imj.Game.Hamazed.World.Space
( Space
, Material(..)
, mkEmptySpace
, mkDeterministicallyFilledSpace
, mkRandomlyFilledSpace
, RandomParameters(..)
, Strategy(..)
, location
, scopedLocation
, Boundaries(..)
, renderSpace
, createRandomNonCollidingPosSpeed
-- * Reexports
, module Imj.Graphics.Render
) where
import Imj.Prelude
import Control.Monad.IO.Class(MonadIO)
import Control.Monad.Reader.Class(MonadReader)
import System.Console.Terminal.Size( Window(..) )
import Data.Graph( Graph
, graphFromEdges
, components )
import Data.List(length, group, concat, mapAccumL)
import Data.Maybe(mapMaybe)
import Data.Matrix( getElem
, fromLists
, getMatrixAsVector
, Matrix
, nrows, ncols )
import Data.Vector(Vector, slice, (!))
import Foreign.C.Types( CInt(..) )
import Imj.Game.Hamazed.Color
import Imj.Game.Hamazed.World.Space.Types
import Imj.Geo.Discrete
import Imj.Graphics.Render
import Imj.Physics.Discrete
import Imj.Util
-- | Creates a 'PosSpeed' such that its position is not colliding,
-- and moves to precollision and mirrors speed if a collision is detected for
-- the next step (see 'mirrorSpeedAndMoveToPrecollisionIfNeeded').
createRandomNonCollidingPosSpeed :: Space -> IO PosSpeed
createRandomNonCollidingPosSpeed space = do
pos <- randomNonCollidingPos space
dx <- randomSpeed
dy <- randomSpeed
return $ fst
$ mirrorSpeedAndMoveToPrecollisionIfNeeded (`location` space)
$ PosSpeed pos (Coords (Coord dx) (Coord dy))
oneRandom :: Int -> Int -> IO Int
oneRandom a b = do
r <- randomRsIO a b
return $ head $ take 1 r
randomSpeed :: IO Int
randomSpeed = oneRandom (-1) 1
randomNonCollidingPos :: Space -> IO (Coords Pos)
randomNonCollidingPos space@(Space _ worldSize _) = do
coords <- randomCoords worldSize
case getMaterial coords space of
Wall -> randomNonCollidingPos space
Air -> return coords
randomInt :: Int -> IO Int
randomInt sz =
oneRandom 0 (sz-1)
randomCoords :: Size -> IO (Coords Pos)
randomCoords (Size rs cs) = do
r <- randomCoord $ fromIntegral rs
c <- randomCoord $ fromIntegral cs
return $ Coords r c
randomCoord :: Coord a -> IO (Coord a)
randomCoord (Coord sz) = Coord <$> randomInt sz
forEachRowPure :: Matrix CInt -> Size -> (Coord Row -> (Coord Col -> Material) -> b) -> [b]
forEachRowPure mat (Size nRows nColumns) f =
let rowIndexes = [0..fromIntegral $ nRows-1] -- index of inner row
internalRowLength = nInternalColumns
rowLength = internalRowLength - 2
nInternalColumns = nColumns + 2 -- size of a column in the matrix
matAsOneVector = flatten mat -- this is O(1)
in map (\rowIdx -> do
let internalRowIdx = succ rowIdx
startInternalIdx = fromIntegral internalRowIdx * fromIntegral nInternalColumns :: Int
startIdx = succ startInternalIdx
row = slice startIdx (fromIntegral rowLength) matAsOneVector
f rowIdx (\c -> mapInt $ row ! fromIntegral c)) rowIndexes
-- unfortunately I didn't find a Matrix implementation that supports arbitrary types
-- so I need to map my type on a CInt
mapMaterial :: Material -> CInt
mapMaterial Air = 0
mapMaterial Wall = 1
mapInt :: CInt -> Material
mapInt 0 = Air
mapInt 1 = Wall
mapInt _ = error "mapInt arg out of bounds"
-- | Creates a rectangular empty space of size specified in parameters.
mkEmptySpace :: Size -> Space
mkEmptySpace s =
let air = mapMaterial Air
in mkSpaceFromInnerMat s [[air]]
-- | Creates a rectangular deterministic space of size specified in parameters.
mkDeterministicallyFilledSpace :: Size -> Space
mkDeterministicallyFilledSpace s@(Size heightEmptySpace widthEmptySpace) =
let wall = mapMaterial Wall
air = mapMaterial Air
w = fromIntegral widthEmptySpace
h = fromIntegral heightEmptySpace
middleRow = replicate w air
collisionRow = replicate 2 air ++ replicate (w-4) wall ++ replicate 2 air
ncolls = 8 :: Int
nEmpty = h - ncolls
n1 = quot nEmpty 2
n2 = nEmpty - n1
l = replicate n1 middleRow ++ replicate ncolls collisionRow ++ replicate n2 middleRow
in mkSpaceFromInnerMat s l
-- | Creates a rectangular random space of size specified in parameters, with a
-- one-element border. 'IO' is used for random numbers generation.
mkRandomlyFilledSpace :: RandomParameters -> Size -> IO Space
mkRandomlyFilledSpace (RandomParameters blockSize strategy) s = do
smallWorldMat <- mkSmallWorld s blockSize strategy
let innerMat = replicateElements blockSize $ map (replicateElements blockSize) smallWorldMat
return $ mkSpaceFromInnerMat s innerMat
-- TODO We could measure, on average, how many tries it takes to generate a graph
-- that meets the requirement for usual values of:
-- - probability of having air vs. a wall at any cell
-- - size of the small world
{- | Generates a random world with the constraint that it should have
a single "Air" connected component. The function recurses and builds a new random world
until the constraint is met.
It might take "a long time" especially if worldsize is big and multFactor is small.
An interesting problem would be to compute the complexity of this function.
To do so we need to know the probability to have a unique connected component in
the random graph defined in the function.
-}
mkSmallWorld :: Size
-- ^ Size of the big world
-> Int
-- ^ Pixel width (if 1, the small world will have the same size as the big one)
-> Strategy
-> IO [[CInt]]
-- ^ the "small world"
mkSmallWorld s@(Size heightEmptySpace widthEmptySpace) multFactor strategy = do
let nCols = quot widthEmptySpace $ fromIntegral multFactor
nRows = quot heightEmptySpace $ fromIntegral multFactor
mkRandomRow _ = take (fromIntegral nCols) <$> rands -- TODO use a Matrix directly
smallMat <- mapM mkRandomRow [0..nRows-1]
let mat = fromLists smallMat
graph = graphOfIndex (mapMaterial Air) mat
case strategy of
StrictlyOneComponent -> case components graph of
[_] -> return smallMat -- TODO return Matrix (mat) instead of list of list
_ -> mkSmallWorld s multFactor strategy
graphOfIndex :: CInt -> Matrix CInt -> Graph
graphOfIndex matchIdx mat =
let sz@(nRows,nCols) = size mat
coords = [Coords (Coord r) (Coord c) | c <-[0..nCols-1], r <- [0..nRows-1], mat `at` (r, c) == matchIdx]
edges = map (\c -> (c, c, connectedNeighbours matchIdx c mat sz)) coords
(graph, _, _) = graphFromEdges edges
in graph
-- these functions adapt the API of matrix to the API of hmatrix
size :: Matrix a -> (Int, Int)
size mat = (nrows mat, ncols mat)
flatten :: Matrix a -> Vector a
flatten = getMatrixAsVector
at :: Matrix a -> (Int, Int) -> a
at mat (i, j) = getElem (succ i) (succ j) mat -- indexes start at 1 in Data.Matrix
connectedNeighbours :: CInt -> Coords Pos -> Matrix CInt -> (Int, Int) -> [Coords Pos]
connectedNeighbours matchIdx coords mat (nRows,nCols) =
let neighbours = [translateInDir LEFT coords, translateInDir Down coords]
in mapMaybe (\other@(Coords (Coord r) (Coord c)) ->
if r < 0 || c < 0 || r >= nRows || c >= nCols || mat `at` (r, c) /= matchIdx
then
Nothing
else
Just other) neighbours
mkSpaceFromInnerMat :: Size -> [[CInt]] -> Space
mkSpaceFromInnerMat s innerMatMaybeSmaller =
let innerMat = extend s innerMatMaybeSmaller
mat = fromLists $ addBorder s innerMat
in Space mat s $ matToRenderGroups mat s
extend :: Size -> [[a]] -> [[a]]
extend (Size rs cs) mat =
extend' (fromIntegral rs) $ map (extend' $ fromIntegral cs) mat
extend' :: Int -> [a] -> [a]
extend' _ [] = error "extend empty list not supported"
extend' sz l@(_:_) =
let len = length l
addsTotal = sz - assert (len <= sz) len
addsLeft = quot addsTotal 2
addsRight = addsTotal - addsLeft
in replicate addsLeft (head l) ++ l ++ replicate addsRight (last l)
rands :: IO [CInt]
rands = randomRsIO 0 1
addBorder :: Size -> [[CInt]] -> [[CInt]]
addBorder (Size _ widthEmptySpace) l =
let nCols = fromIntegral widthEmptySpace + 2 * borderSize
wall = mapMaterial Wall
wallRow = replicate nCols wall
encloseIn b e = b ++ e ++ b
in encloseIn (replicate borderSize wallRow) $ map (encloseIn $ replicate borderSize wall) l
borderSize :: Int
borderSize = 1
matToRenderGroups :: Matrix CInt -> Size -> [RenderGroup]
matToRenderGroups mat s@(Size _ cs) =
concat $
forEachRowPure mat s $
\row accessMaterial ->
snd $ mapAccumL
(\col listMaterials@(material:_) ->
let count = length listMaterials
materialColor = case material of
Wall -> wallColors
Air -> airColors
materialChar = case material of
Wall -> 'Z'
Air -> ' '
in (col + fromIntegral count,
RenderGroup (Coords row col) materialColor materialChar count))
(Coord 0) $ group $ map accessMaterial [0..fromIntegral $ pred cs]
getInnerMaterial :: Coords Pos -> Space -> Material
getInnerMaterial (Coords (Coord r) (Coord c)) (Space mat _ _) =
mapInt $ mat `at` (r+borderSize, c+borderSize)
-- | <https://hackage.haskell.org/package/matrix-0.3.5.0/docs/Data-Matrix.html#v:getElem Indices start at 1>:
-- @Coord 0 0@ corresponds to indexes 1 1 in matrix
getMaterial :: Coords Pos -> Space -> Material
getMaterial coords@(Coords r c) space@(Space _ (Size rs cs) _)
| r < 0 || c < 0 = Wall
| r > fromIntegral(rs-1) || c > fromIntegral(cs-1) = Wall
| otherwise = getInnerMaterial coords space
materialToLocation :: Material -> Location
materialToLocation m = case m of
Wall -> OutsideWorld
Air -> InsideWorld
-- | Considers that outside 'Space', everything is 'OutsideWorld'
location :: Coords Pos -> Space -> Location
location c s = materialToLocation $ getMaterial c s
-- | Considers that outside 'Space', everything is 'InsideWorld'
strictLocation :: Coords Pos -> Space -> Location
strictLocation coords@(Coords r c) space@(Space _ (Size rs cs) _)
| r < 0 || c < 0 || r > fromIntegral(rs-1) || c > fromIntegral(cs-1) = InsideWorld
| otherwise = materialToLocation $ getInnerMaterial coords space
{-# INLINABLE renderSpace #-}
renderSpace :: (Draw e, MonadReader e m, MonadIO m)
=> Space
-> Coords Pos
-- ^ World upper left coordinates w.r.t terminal frame.
-> m (Coords Pos)
renderSpace (Space _ _ renderedWorld) upperLeft = do
let worldCoords = move borderSize Down $ move borderSize RIGHT upperLeft
mapM_ (renderGroup worldCoords) renderedWorld
return worldCoords
{-# INLINABLE renderGroup #-}
renderGroup :: (Draw e, MonadReader e m, MonadIO m)
=> Coords Pos
-> RenderGroup
-> m ()
renderGroup worldCoords (RenderGroup pos colors char count) =
drawChars count char (sumCoords pos worldCoords) colors
scopedLocation :: Space
-> Maybe (Window Int)
-- ^ The terminal size
-> Coords Pos
-- ^ The world upper left coordinates w.r.t terminal frame.
-> Boundaries
-- ^ The scope
-> Coords Pos
-- ^ The coordinates to test
-> Location
scopedLocation space@(Space _ sz _) mayTermWindow wcc =
-- Use a big terminal by default, it will just make animations be updated for longer
-- than they should:
let worldLocation = (`location` space)
worldLocationExcludingBorders = (`strictLocation` space)
(Window h w) = fromMaybe (Window {height = 150, width = 400}) mayTermWindow
terminalLocation coordsInWorld =
let (Coords (Coord r) (Coord c)) = sumCoords coordsInWorld wcc
in if r >= 0 && r < h && c >= 0 && c < w
then
InsideWorld
else
OutsideWorld
productLocations l l' = case l of
InsideWorld -> l'
OutsideWorld -> OutsideWorld
in \case
WorldFrame -> worldLocation
TerminalWindow -> (\coo-> if containsWithOuterBorder coo sz
then
OutsideWorld
else
terminalLocation coo)
Both -> (\coo-> productLocations (terminalLocation coo) (worldLocationExcludingBorders coo))