packages feed

hexmino-0.1.0.0: src/TileGrid.hs

module TileGrid where

import qualified Tile
import qualified Hex
import qualified Data.Map.Strict as Map
import Data.List (foldl', sortOn)
import qualified System.Random as Rand
import qualified Graphics.Gloss.Data.Color as Color
import qualified Graphics.Gloss.Data.Picture as Pict

-- implementation based on https://www.redblobgames.com/grids/hexagons/
data TileGrid = TileGrid {tileMap :: TileMap, range :: Int, tileSize :: Float} deriving Show
type TileMap = Map.Map Axial Tile.Tile
-- coordinate systems - chosen offset is odd-q
newtype Axial = Axial (Int, Int) deriving (Eq, Ord, Show)
newtype Offset = Offset (Int, Int) deriving (Eq, Ord, Show)
newtype Cubic = Cubic (Int, Int, Int) deriving (Eq, Ord, Show)

-- creation functions
empty :: Int -> TileGrid
empty rg = TileGrid {tileMap = Map.empty, range = rg, tileSize = rangeToSize rg}

newGame :: TileGrid -> Rand.StdGen -> (TileGrid, [Tile.Tile], Rand.StdGen)
newGame grid gen = (clear grid, lst, lastGen)
  where
    rg = range grid
    idxedTiles = zip (everyIndex rg) . repeat . Tile.empty $ tileSize grid
    (fullMap, middleGen) = foldl' fillTile (Map.fromList idxedTiles, gen) idxedTiles
    (randInts, lastGen) = randomInts (totalIndexNum rg) middleGen
    lst = map rotateAndTake . sortOn fst . zip randInts $ Map.elems fullMap

fillTile :: (TileMap, Rand.StdGen) -> (Axial, Tile.Tile) -> (TileMap, Rand.StdGen)
fillTile (tMap, gen) (idx, tile) = (Map.insert idx (tile {Tile.faces = (a,b,c)}) tMap, newGen)
  where
    (a, genA) = fillFace (neighVal idx Tile.North tMap, neighVal idx Tile.NorthEast tMap) gen
    (b, genB) = fillFace (neighVal idx Tile.SouthEast tMap, neighVal idx Tile.South tMap) genA
    (c, newGen) = fillFace (neighVal idx Tile.SouthWest tMap, neighVal idx Tile.NorthWest tMap) genB

fillFace :: (Maybe Int, Maybe Int) -> Rand.StdGen -> (Int, Rand.StdGen)
fillFace neigs gen = case neigs of
  (Just 0, Just 0) -> newRand
  (Just n, Just k) -> if n /= 0 then (n, gen) else (k, gen)
  (Nothing, Just n) -> if n /= 0 then (n, gen) else newRand
  (Just n, Nothing) -> if n /= 0 then (n, gen) else newRand
  _ -> (0, gen)
  where newRand = Rand.randomR (1,6) gen

randomInts :: Int -> Rand.StdGen -> ([Int], Rand.StdGen)
randomInts 0 gen = ([], gen)
randomInts n gen = (val : follRand, follGen)
  where
    (val, nextGen) = Rand.random gen
    (follRand, follGen) = randomInts (n-1) nextGen

rotateAndTake :: (Int, Tile.Tile) -> Tile.Tile
rotateAndTake (n, tile) = (!! mod n 3) $ iterate Tile.rotate tile

-- rendering functions
render :: Color.Color -> TileGrid -> Pict.Picture
render col grid = case range grid of
  0 -> Pict.color col $ Hex.hexagonSolidPointy 230
  rg -> Pict.pictures . map (renderIndex col grid) $ everyIndex rg

renderIndex :: Color.Color -> TileGrid -> Axial ->  Pict.Picture
renderIndex col grid axi = case Map.lookup axi $ tileMap grid of
  Just tile -> Tile.render tile
  _ -> renderEmpty col axi $ tileSize grid

renderEmpty :: Color.Color -> Axial -> Float -> Pict.Picture
renderEmpty col axi rad = Pict.color col $ Hex.render hex
  where hex = Hex.Hexagon {Hex.center = indexCenter axi rad, Hex.radius = rad-1}

-- manipulation functions
putTile :: Tile.Tile -> Axial -> TileGrid -> TileGrid
putTile tile idx grid = grid {tileMap = Map.insert idx fixedTile $ tileMap grid}
  where fixedTile = Tile.moveTo (indexCenter idx (tileSize grid)) tile

grab :: Pict.Point -> TileGrid -> (TileGrid, Maybe Tile.Tile)
grab point grid = case pointToIndex point grid of
  Just idx -> grabIndex idx grid
  _ -> (grid, Nothing)

grabIndex :: Axial -> TileGrid -> (TileGrid, Maybe Tile.Tile)
grabIndex idx grid = case Map.lookup idx $ tileMap grid of
  Just tile -> (grid {tileMap = Map.delete idx $ tileMap grid}, Just tile)
  _ -> (grid, Nothing)

isFull :: TileGrid -> Bool
isFull TileGrid {tileMap = tMap, range = rg} = totalIndexNum rg == Map.size tMap

isCompleted :: TileGrid -> Bool
isCompleted grid
  | isFull grid = all (matchesNeighs (tileMap grid)) . everyIndex $ range grid
  | otherwise = False

clear :: TileGrid -> TileGrid
clear = empty . range

-- utility functions
rangeToSize :: Int -> Float
rangeToSize n = 418 / (2 * (fn+1) + fn)
  where fn = fromIntegral n

indexIsEmpty :: Axial -> TileGrid -> Bool
indexIsEmpty idx = Map.notMember idx . tileMap

totalIndexNum :: Int -> Int
totalIndexNum range = range * (range + 1) `div` 2 * 6 + 1

everyIndex :: Int -> [Axial]
everyIndex = map cubicToAxial . everyCubicIndex

everyCubicIndex :: Int -> [Cubic]
everyCubicIndex range = [Cubic (x,y,z) |
    x <- [(-range)..range],
    y <- [(max (-range) ((-x)-range))..(min range (range-x))],
    let z = (-x)-y,
    x + y + z == 0
  ]

indexCenter :: Axial -> Float -> Pict.Point
indexCenter axi rad = (rad * fromIntegral cl * 1.5, (off + fromIntegral rw) * (-h))
  where
    Offset (cl, rw) = axialToOffset axi
    h = Hex.heightFromRadius rad
    off = if odd cl then 0.5 else 0

pointToIndex :: Pict.Point -> TileGrid -> Maybe Axial
pointToIndex point grid
  | isValidIndex axi grid = Just axi
  | otherwise = Nothing
  where axi = pointToAxial point $ tileSize grid

pointToAxial :: Pict.Point -> Float -> Axial
pointToAxial (x,y) size = Axial (round q, round r)
  where
    q = 2/3 * x / size
    r = ((-1)/3 * x + sqrt 3 / 3 * (-y)) / size

isValidIndex :: Axial -> TileGrid -> Bool
isValidIndex axi grid = cubicDistance (Cubic (0,0,0)) (axialToCubic axi) <= range grid

cubicDistance :: Cubic -> Cubic -> Int
cubicDistance (Cubic (x1,y1,z1)) (Cubic (x2,y2,z2)) = maximum $ map abs [x1-x2, y1-y2, z1-z2]

matchesNeighs :: TileMap -> Axial -> Bool
matchesNeighs tMap idx = case Map.lookup idx tMap of
  Just tile -> all (matchesNeighSide tile idx tMap) Tile.allCardinal
  _ -> False

matchesNeighSide :: Tile.Tile -> Axial -> TileMap -> Tile.Cardinal -> Bool
matchesNeighSide tile idx tMap card = case neighVal idx card tMap of
  Just 0 -> False
  Just n -> Tile.sideValue card tile == n
  Nothing -> True -- always matches an out-of-range tiles

neighVal :: Axial -> Tile.Cardinal -> TileMap -> Maybe Int
neighVal idx card tMap = case neighTile idx card tMap of
  Just tile -> Just $ Tile.sideValue (Tile.opposedCardinal card) tile
  _ -> Nothing

neighTile :: Axial -> Tile.Cardinal -> TileMap -> Maybe Tile.Tile
neighTile idx card = Map.lookup (neighAxial idx card)

neighAxial :: Axial -> Tile.Cardinal -> Axial
neighAxial (Axial (q, r)) card = case card of
  Tile.North -> Axial (q, r-1)
  Tile.NorthEast -> Axial (q+1, r-1)
  Tile.SouthEast -> Axial (q+1, r)
  Tile.South -> Axial (q, r+1)
  Tile.SouthWest -> Axial (q-1, r+1)
  Tile.NorthWest -> Axial (q-1, r)

-- coordinates conversion functions
axialToOffset :: Axial -> Offset
axialToOffset = cubicToOffset . axialToCubic

axialToCubic :: Axial -> Cubic
axialToCubic (Axial (q, r)) = Cubic (q, (-q) - r, r)

cubicToOffset :: Cubic -> Offset
cubicToOffset (Cubic (x, y, z)) = Offset (x, z + (x - (if odd x then 1 else 0)) `div` 2)

cubicToAxial :: Cubic -> Axial
cubicToAxial (Cubic (x, _, z)) = Axial (x, z)