packages feed

board-games-0.4: src/Game/Labyrinth.hs

module Game.Labyrinth where

import qualified System.Random as Rnd
import Game.Utility (randomSelect)

import qualified Graphics.Ascii.Haha.Terminal as ANSI
import qualified Text.PrettyPrint.Boxes as Box
import Text.PrettyPrint.Boxes (Box)
import Text.Printf (printf)

import qualified Test.QuickCheck as QC

import qualified Control.Monad.Exception.Synchronous as ME
import qualified Control.Monad.Trans.State as MS
import qualified Control.Applicative.HT as App
import qualified Control.Functor.HT as FuncHT
import Control.Monad (mfilter)
import Control.Applicative (Applicative, pure, (<*>), (<$>))

import qualified Data.Traversable as Trav
import qualified Data.Foldable as Fold
import qualified Data.Monoid.HT as Mn
import qualified Data.NonEmpty.Class as NonEmptyC
import qualified Data.NonEmpty as NonEmpty
import qualified Data.List.Match as Match
import qualified Data.List.HT as ListHT
import qualified Data.List as List
import qualified Data.EnumMap as EnumMap
import qualified Data.EnumSet as EnumSet
import qualified Data.Map as Map
import qualified Data.Sequence as Seq
import qualified Data.Bits as Bits
import qualified Data.Char as Char
import Data.EnumMap (EnumMap)
import Data.EnumSet (EnumSet)
import Data.Map (Map)
import Data.Sequence (Seq)
import Data.Bits ((.&.), (.|.))
import Data.Word (Word64)
import Data.Traversable (Traversable, sequenceA)
import Data.Foldable (Foldable, foldMap)
import Data.NonEmpty ((!:))
import Data.List (zipWith4)
import Data.Monoid (Monoid, mconcat, mappend, mempty)
import Data.Semigroup (Semigroup, (<>))
import Data.Tuple.HT (mapPair, mapFst, mapSnd)
import Data.Ord.HT (comparing)


{- $setup
>>> import qualified Game.Labyrinth as Labyrinth
-}

newtype Board = Board Word64
   deriving (Eq)

instance Show Board where
   showsPrec p (Board bits) =
      showParen (p>=10) $
         showString "Board " . showString (printf "0x%016x" bits)

instance Semigroup Board where
   Board x <> Board y = Board (x.|.y)

instance Monoid Board where
   mempty = Board 0
   mappend = (<>)

instance QC.Arbitrary Board where
   arbitrary = Board . (boardMask .&.) <$> QC.arbitrary


data Coord = C0 | C1 | C2 | C3 | C4 | C5 | C6
   deriving (Eq, Ord, Enum, Bounded, Show)

instance QC.Arbitrary Coord where
   arbitrary = QC.arbitraryBoundedEnum

boardIndex :: (Coord,Coord) -> Int
boardIndex (row,column) = fromEnum row * 8 + fromEnum column

isCellFixed :: (Coord,Coord) -> Bool
isCellFixed (row,column) = even (fromEnum row) && even (fromEnum column)

boardGet :: Board -> (Coord,Coord) -> Bool
boardGet (Board bits) pos = Bits.testBit bits $ boardIndex pos

boardSet :: Bool -> (Coord,Coord) -> Board
boardSet b pos = Board $ fromIntegral (fromEnum b) `Bits.shiftL` boardIndex pos

singleCell :: (Coord,Coord) -> Board
singleCell = Board . Bits.bit . boardIndex

listsFromBoard :: Board -> [[Bool]]
listsFromBoard board =
   ListHT.outerProduct (curry $ boardGet board) allEnums allEnums

allEnums :: (Enum a, Bounded a) => [a]
allEnums = [minBound .. maxBound]

rowMask, columnMask, boardMask :: Word64
rowMask = 0x7F
columnMask = 0x1010101010101
boardMask = rowMask*columnMask

rowKMask, columnKMask :: Coord -> Word64
rowKMask pos = rowMask `Bits.shiftL` (8 * fromEnum pos)
columnKMask pos = columnMask `Bits.shiftL` fromEnum pos

infixl 7 .-.

(.-.) :: Bits.Bits a => a -> a -> a
a.-.b = a .&. Bits.complement b

shiftBoardLeft :: Board -> Board
shiftBoardLeft (Board bits) =
   Board $ (bits .-. columnKMask C0) `Bits.shiftR` 1

shiftBoardRight :: Board -> Board
shiftBoardRight (Board bits) =
   Board $ (bits .-. columnKMask C6) `Bits.shiftL` 1

shiftBoardUp :: Board -> Board
shiftBoardUp (Board bits) =
   Board $ (bits .-. rowKMask C0) `Bits.shiftR` 8

shiftBoardDown :: Board -> Board
shiftBoardDown (Board bits) =
   Board $ (bits .-. rowKMask C6) `Bits.shiftL` 8


splitBoard :: Board -> Board -> (Board,Board)
splitBoard (Board mask) (Board board) =
   (Board (board.&.mask), Board (board.-.mask))

{- |
prop> \k b -> b == Labyrinth.shiftRowLeft k (Labyrinth.shiftRowRight k b)
prop> \k b -> b == Labyrinth.shiftRowRight k (Labyrinth.shiftRowLeft k b)
-}
shiftRowLeft :: Coord -> (Bool, Board) -> (Bool, Board)
shiftRowLeft r (extra,board) =
   let (row, remainder) = splitBoard (Board $ rowKMask r) board
   in (boardGet row (r,C0),
       remainder <> shiftBoardLeft row <> boardSet extra (r,C6))

shiftRowRight :: Coord -> (Bool, Board) -> (Bool, Board)
shiftRowRight r (extra,board) =
   let (row, remainder) = splitBoard (Board $ rowKMask r) board
   in (boardGet row (r,C6),
       remainder <> shiftBoardRight row <> boardSet extra (r,C0))

{- |
prop> \k b -> b == Labyrinth.shiftColumnUp k (Labyrinth.shiftColumnDown k b)
prop> \k b -> b == Labyrinth.shiftColumnDown k (Labyrinth.shiftColumnUp k b)
-}
shiftColumnUp :: Coord -> (Bool, Board) -> (Bool, Board)
shiftColumnUp c (extra,board) =
   let (column, remainder) = splitBoard (Board $ columnKMask c) board
   in (boardGet column (C0,c),
       remainder <> shiftBoardUp column <> boardSet extra (C6,c))

shiftColumnDown :: Coord -> (Bool, Board) -> (Bool, Board)
shiftColumnDown c (extra,board) =
   let (column, remainder) = splitBoard (Board $ columnKMask c) board
   in (boardGet column (C6,c),
       remainder <> shiftBoardDown column <> boardSet extra (C0,c))


cycleBoardLeft :: Board -> Board
cycleBoardLeft (Board bits) =
   Board $
      (bits .-. columnKMask C0) `Bits.shiftR` 1
      .|.
      (bits .&. columnKMask C0) `Bits.shiftL` 6

cycleBoardRight :: Board -> Board
cycleBoardRight (Board bits) =
   Board $
      (bits .-. columnKMask C6) `Bits.shiftL` 1
      .|.
      (bits .&. columnKMask C6) `Bits.shiftR` 6

cycleBoardUp :: Board -> Board
cycleBoardUp (Board bits) =
   Board $
      (bits .-. rowKMask C0) `Bits.shiftR` 8
      .|.
      (bits .&. rowKMask C0) `Bits.shiftL` (8*6)

cycleBoardDown :: Board -> Board
cycleBoardDown (Board bits) =
   Board $
      (bits .-. rowKMask C6) `Bits.shiftL` 8
      .|.
      (bits .&. rowKMask C6) `Bits.shiftR` (8*6)

cycleGen :: (Board -> Board) -> (Coord -> Word64) -> Coord -> Board -> Board
cycleGen cycleBoard mask coord =
   uncurry mappend . mapFst cycleBoard . splitBoard (Board $ mask coord)

{- |
prop> \k b -> b == Labyrinth.cycleRowLeft k (Labyrinth.cycleRowRight k b)
prop> \k b -> b == Labyrinth.cycleRowRight k (Labyrinth.cycleRowLeft k b)
-}
cycleRowLeft :: Coord -> Board -> Board
cycleRowLeft = cycleGen cycleBoardLeft rowKMask

cycleRowRight :: Coord -> Board -> Board
cycleRowRight = cycleGen cycleBoardRight rowKMask

{- |
prop> \k b -> b == Labyrinth.cycleColumnUp k (Labyrinth.cycleColumnDown k b)
prop> \k b -> b == Labyrinth.cycleColumnDown k (Labyrinth.cycleColumnUp k b)
-}
cycleColumnUp :: Coord -> Board -> Board
cycleColumnUp = cycleGen cycleBoardUp columnKMask

cycleColumnDown :: Coord -> Board -> Board
cycleColumnDown = cycleGen cycleBoardDown columnKMask

cycleStripe :: Border -> Coord -> Board -> Board
cycleStripe b =
   case b of
      West -> cycleRowRight
      East -> cycleRowLeft
      South -> cycleColumnUp
      North -> cycleColumnDown



rotateMasked ::
   (Directions Board -> Directions Board) ->
   Board -> Directions Board -> Directions Board
rotateMasked rotate mask board =
   case FuncHT.unzip (splitBoard mask <$> board) of
      (masked, remaining) -> rotate masked <> remaining

rotateDir90, rotateDir180 :: Directions a -> Directions a
rotateDir90 (Directions n w s e) = Directions e n w s
rotateDir180 (Directions n w s e) = Directions s e n w

randomRotate ::
   (Rnd.RandomGen g) => Directions Board -> MS.State g (Directions Board)
randomRotate board =
   ($ board) <$>
   App.lift2 (.)
      (rotateMasked rotateDir180 . Board <$> MS.state Rnd.random)
      (rotateMasked rotateDir90  . Board <$> MS.state Rnd.random)


data Directions a = Directions {north, west, south, east :: a}
   deriving (Eq)

instance Semigroup a => Semigroup (Directions a) where
   (<>) = App.lift2 (<>)

instance Monoid a => Monoid (Directions a) where
   mempty = pure mempty
   mappend = App.lift2 mappend

instance Functor Directions where
   fmap f (Directions n w s e) = Directions (f n) (f w) (f s) (f e)

instance Foldable Directions where
   foldMap f (Directions n w s e) =
      f n `mappend` f w `mappend` f s `mappend` f e

instance Traversable Directions where
   sequenceA (Directions n w s e) = App.lift4 Directions n w s e

instance Applicative Directions where
   pure a = Directions a a a a
   Directions fn fw fs fe <*> Directions n w s e =
      Directions (fn n) (fw w) (fs s) (fe e)

transposeDirections :: Directions [[a]] -> [[Directions a]]
transposeDirections (Directions n w s e) =
   zipWith4 (zipWith4 Directions) n w s e


boolChar :: Char -> Bool -> Char
boolChar c b = if b then c else ' '

formatBoard :: Board -> String
formatBoard = unlines . map (map (boolChar '*')) . listsFromBoard

wallChar :: Char
wallChar = '\x2588'

wayChar :: Char
wayChar = '.'

selectWallWayChar :: Bool -> Bool -> Char
selectWallWayChar wall connect =
   case (wall, connect) of
      (False, False) -> ' '
      (True,  False) -> wallChar
      (False, True)  -> wayChar
      (True,  True)  -> error "character cannot be both wall and way"

boxTable :: [[Box]] -> Box
boxTable = Box.vcat Box.left . map (Box.hcat Box.top)

formatCell ::
   EnumSet Player -> Directions Bool -> Char -> Directions Bool -> Box
formatCell players connects center walls =
   let plChar p =
         if EnumSet.member p players then playerChar True p else wallChar
   in case App.lift2 selectWallWayChar walls connects of
         Directions n w s e ->
            boxTable $ map (map Box.char) $
            [plChar Player0, n, plChar Player1] :
            [w, center, e] :
            [plChar Player2, s, plChar Player3] :
            []

format :: Directions Board -> Box
format =
   boxTable .
   map (map (formatCell EnumSet.empty (pure False) ' ')) .
   transposeDirections . fmap listsFromBoard

formatWays :: Directions Board -> Board -> Box
formatWays walls reachable =
   boxTable $
   zipWith3
      (zipWith3 (formatCell EnumSet.empty))
      (transposeDirections $ fmap listsFromBoard $
       connectReachable walls reachable)
      (map (map (boolChar wayChar)) $ listsFromBoard reachable)
      (transposeDirections $ fmap listsFromBoard walls)

connectReachable :: Directions Board -> Board -> Directions Board
connectReachable walls reachable =
   let blockWall (Board mask) (Board bits) = Board $ bits.-.mask
       shiftAndBlock block1 shift block0 =
         blockWall
            (block1 walls <> shift (block0 walls))
            (reachable <> shift reachable)
   in Directions {
         north = shiftAndBlock north shiftBoardDown south,
         south = shiftAndBlock south shiftBoardUp north,
         west = shiftAndBlock west shiftBoardRight east,
         east = shiftAndBlock east shiftBoardLeft west
      }

arrows :: Directions Char
arrows =
   Directions {
      north = '\x21D3', west = '\x21D2', south = '\x21D1', east = '\x21D0'
   }

arrowFrame :: Maybe (Border, Coord) -> Box -> Box
arrowFrame forbidden box =
   let rowBox arrow c = Box.text [' ', arrow, c]
       columnBox c0 c1 = Box.alignVert Box.center1 3 $ Box.text [c0, c1]
       makeBoxes =
         Directions {
            north = rowBox, west = flip columnBox,
            south = rowBox, east = columnBox
         }
       horizVert h v = Directions {north = h, south = h, west = v, east = v}
       labels =
         maybe id
            (\(b,c) -> singleBorder b $ Map.insert c)
            forbidden (pure $ const id)
         <*>
         horizVert (Box.emptyBox 1 3) (Box.emptyBox 3 1)
         <*>
         App.lift2 fmap (makeBoxes <*> arrows) shiftCharMap
       labelBoxes =
         horizVert (Box.hsep 3 Box.top) (Box.vsep 3 Box.center1)
         <*>
         ((Box.nullBox :) . Map.elems <$> labels)
       emptyBox = Box.emptyBox 1 2
   in boxTable $
      [emptyBox, north labelBoxes, emptyBox] :
      [west labelBoxes, box, east labelBoxes] :
      [emptyBox, south labelBoxes, emptyBox] :
      []

constMap :: (Ord k) => [k] -> a -> Map k a
constMap keys a = Map.fromList $ map (flip (,) a) keys

shiftCharMap :: Directions (Map Coord Char)
shiftCharMap =
   flip MS.evalState 'A' $ Trav.traverse Trav.sequence $
   pure $ constMap [C1,C3,C5] $ MS.state $ \c -> (c, succ c)

arrowMap :: Map Char (Border,Coord)
arrowMap =
   Fold.fold $
   App.lift2
      (\b -> Map.fromList . map (\(c,char) -> (char,(b,c))) . Map.toList)
      borderSet shiftCharMap


coordinateFrame :: Box -> Box
coordinateFrame box =
   let row =
         Box.emptyBox 1 1 Box.<>
         (Box.hsep 2 Box.top $ map Box.char $ Map.keys columnMap)
       column =
         Box.emptyBox 1 1 Box.//
         (Box.vsep 2 Box.top $ map Box.char $ Map.keys rowMap)
       emptyBox = Box.emptyBox 1 2
   in boxTable $
      [emptyBox, row, emptyBox] :
      [column Box.<> Box.emptyBox 1 1, box, Box.emptyBox 1 1 Box.<> column] :
      [emptyBox, row, emptyBox] :
      []

rowMap, columnMap :: Map Char Coord
rowMap = Map.fromList $ zip ['A'..] allEnums
columnMap = Map.fromList $ zip ['0'..] allEnums


data Shape = I | L | T
   deriving (Eq, Ord, Enum, Show)

type InitTile = (Shape, Maybe Char)

data Rotation = R0 | R1 | R2 | R3
   deriving (Eq, Ord, Enum, Bounded)


dirsFromShape :: Shape -> Directions Bool
dirsFromShape shape =
   case shape of
      I -> (pure True) {north = False, south = False}
      L -> (pure True) {north = False, east = False}
      T -> (pure True) {west = False, east = False, south = False}

defaultBoardDirs :: [[Directions Bool]]
defaultBoardDirs =
   let f :: Rotation -> Shape -> Directions Bool
       f rot =
         (if Bits.testBit (fromEnum rot) 1 then rotateDir180 else id) .
         (if Bits.testBit (fromEnum rot) 0 then rotateDir90 else id) .
         dirsFromShape
       r0 = f R0; r1 = f R1; r2 = f R2; r3 = f R3
   in [r3 L, r0 T, r0 T, r2 L] :
      [r1 T, r1 T, r0 T, r3 T] :
      [r1 T, r2 T, r3 T, r3 T] :
      [r0 L, r2 T, r2 T, r1 L] :
      []

defaultBoard :: Directions Board
defaultBoard =
   let evenCoords = [C0, C2 .. C6]
   in mconcat $
      zipWith
         (\row ->
            mconcat .
            zipWith (\col -> fmap (flip Mn.when (singleCell (row,col))))
               evenCoords)
         evenCoords defaultBoardDirs


randomChoose :: (Rnd.RandomGen g) => MS.StateT [a] (MS.State g) a
randomChoose = MS.StateT $ randomSelect . ListHT.removeEach

{-
	total	fixed	symbol
		position
I:	 13	  0	  0
T:	 18	 12	 18
L:	 19	  4	 10

symbols: 24	 12
-}
shuffle :: (Rnd.RandomGen g) => MS.State g (InitTile, [InitTile])
shuffle =
   let (symbolsL, symbolsT) = splitAt 6 moveableSymbols
       xs =
         replicate 13 (I, Nothing) ++
         map (\s -> (T, Just s)) symbolsT ++
         replicate 9 (L, Nothing) ++
         map (\s -> (L, Just s)) symbolsL
   in flip MS.evalStateT xs $ App.lift2 (,) randomChoose $
      sequence $ Match.replicate (drop 1 xs) randomChoose

singleCellShape :: (Coord, Coord) -> Shape -> Directions Board
singleCellShape pos shape =
   flip Mn.when (singleCell pos) <$> dirsFromShape shape

grid :: [a] -> [(a,a)]
grid xs = App.lift2 (,) xs xs

layoutMoveableCells :: [InitTile] -> (Directions Board, SymbolMap)
layoutMoveableCells =
   mapPair (Fold.fold . Map.mapWithKey singleCellShape, Map.mapMaybe id) .
   FuncHT.unzip . Map.fromList .
   zip (filter (not . isCellFixed) $ grid allEnums)

randomBoard ::
   (Rnd.RandomGen g) => MS.State g (InitTile, (Directions Board, SymbolMap))
randomBoard = do
   (extra,shapes) <- shuffle
   let (board,symbolMap) = layoutMoveableCells shapes
   moveable <- randomRotate board
   return (extra, (defaultBoard <> moveable, symbolMap))

shuffleTargets :: (Rnd.RandomGen g) => MS.State g [Char]
shuffleTargets =
   let xs = moveableSymbols ++ fixedSymbols
   in flip MS.evalStateT xs $ sequence $ Match.replicate xs randomChoose

associateTargets ::
   NonEmpty.T [] Player -> [Char] ->
   NonEmpty.T Seq (Player, NonEmpty.T [] Char)
associateTargets players =
   NonEmpty.mapTail Seq.fromList .
   NonEmptyC.zipWith
      (\player targets ->
         (player, NonEmpty.snoc targets $ playerChar False player))
      players .
   maybe (error "empty players list") id .
   NonEmpty.fetch . ListHT.sliceHorizontal (1 + length (NonEmpty.tail players))


bfsStep :: Directions Board -> Board -> Board
bfsStep walls board =
   let blockWall (Board mask) (Board bits) = Board $ bits.-.mask
       shiftAndBlock block1 shift block0 =
         blockWall (block1 walls) (shift (blockWall (block0 walls) board))
   in board
      <>
      shiftAndBlock east  shiftBoardLeft  west
      <>
      shiftAndBlock west  shiftBoardRight east
      <>
      shiftAndBlock north shiftBoardDown  south
      <>
      shiftAndBlock south shiftBoardUp    north

bfs :: Directions Board -> Board -> Board
bfs walls =
   snd . head . dropWhile (not . fst) .
   ListHT.mapAdjacent (\x y -> (x==y, x)) . iterate (bfsStep walls)


data Player = Player0 | Player1 | Player2 | Player3
   deriving (Eq, Ord, Enum, Bounded, Show)

playerChar :: Bool -> Player -> Char
playerChar b p =
   case p of
      Player0 -> if b then '\x2666' else '\x2662'
      Player1 -> if b then '\x2665' else '\x2661'
      Player2 -> if b then '\x2660' else '\x2664'
      Player3 -> if b then '\x2663' else '\x2667'

playerSymbols, fixedSymbols, moveableSymbols :: [Char]
playerSymbols = map (playerChar False) allEnums
(fixedSymbols, moveableSymbols) = splitAt 12 $ take 24 ['\x2600' ..]

type SymbolMap = Map (Coord,Coord) Char

fixedSymbolMap :: SymbolMap
fixedSymbolMap =
   let corners = Map.fromList $ zip (grid [C0,C6]) playerSymbols
   in Map.union corners $ Map.fromList $
      zip (filter (flip Map.notMember corners) $ grid [C0, C2 .. C6])
         fixedSymbols

randomSymbolMap :: (Rnd.RandomGen g) => MS.State g SymbolMap
randomSymbolMap =
   Map.fromList <$>
   MS.evalStateT
      (mapM (\symbol -> flip (,) symbol <$> randomChoose) moveableSymbols)
      (filter (flip Map.notMember fixedSymbolMap) $ grid [C0 .. C6])

removeFromMap :: (Ord k) => k -> Map k a -> (Maybe a, Map k a)
removeFromMap = Map.updateLookupWithKey (\ _k _a -> Nothing)

insertMaybeMap :: (Ord k) => k -> Maybe a -> Map k a -> Map k a
insertMaybeMap k = maybe id (Map.insert k)

shiftRowLeftMap :: Coord -> (Maybe Char, SymbolMap) -> (Maybe Char, SymbolMap)
shiftRowLeftMap rowPos (extra, m) =
   let (row, remainder) = Map.partitionWithKey (\(r,_c) _ -> r == rowPos) m
       (x,xs) = removeFromMap (rowPos,minBound) row
   in (x,
       remainder <>
         insertMaybeMap (rowPos,maxBound) extra
            (Map.mapKeysMonotonic (\(r,c) -> (r, pred c)) xs))

shiftRowRightMap :: Coord -> (Maybe Char, SymbolMap) -> (Maybe Char, SymbolMap)
shiftRowRightMap rowPos (extra, m) =
   let (row, remainder) = Map.partitionWithKey (\(r,_c) _ -> r == rowPos) m
       (x,xs) = removeFromMap (rowPos,maxBound) row
   in (x,
       remainder <>
         insertMaybeMap (rowPos,minBound) extra
            (Map.mapKeysMonotonic (\(r,c) -> (r, succ c)) xs))

shiftColumnUpMap ::
   Coord -> (Maybe Char, SymbolMap) -> (Maybe Char, SymbolMap)
shiftColumnUpMap columnPos (extra, m) =
   let (column, remainder) =
         Map.partitionWithKey (\(_r,c) _ -> c == columnPos) m
       (x,xs) = removeFromMap (minBound,columnPos) column
   in (x,
       remainder <>
         insertMaybeMap (maxBound,columnPos) extra
            (Map.mapKeysMonotonic (\(r,c) -> (pred r, c)) xs))

shiftColumnDownMap ::
   Coord -> (Maybe Char, SymbolMap) -> (Maybe Char, SymbolMap)
shiftColumnDownMap columnPos (extra, m) =
   let (column, remainder) =
         Map.partitionWithKey (\(_r,c) _ -> c == columnPos) m
       (x,xs) = removeFromMap (maxBound,columnPos) column
   in (x,
       remainder <>
         insertMaybeMap (minBound,columnPos) extra
            (Map.mapKeysMonotonic (\(r,c) -> (succ r, c)) xs))


formatWithSymbols :: BoardState -> Board -> Box
formatWithSymbols s reachable =
   boxTable $
   zipWith4
      (zipWith4
         (\pos connects reach ->
            formatCell
               (EnumMap.keysSet $ EnumMap.filter (pos==) $ statePlayerMap s)
               connects
               (Map.findWithDefault (boolChar wayChar reach) pos $
                stateSymbolMap s)))
      (ListHT.outerProduct (,) allEnums allEnums)
      (transposeDirections $ fmap listsFromBoard $
       connectReachable (stateBoard s) reachable)
      (listsFromBoard reachable)
      (transposeDirections $ fmap listsFromBoard $ stateBoard s)


type PlayerMap = EnumMap Player (Coord,Coord)

shiftPlayersGen ::
   Eq a => (b -> a) -> (b -> b) -> a -> EnumMap k b -> EnumMap k b
shiftPlayersGen select update coord =
   EnumMap.map (\pos -> if select pos == coord then update pos else pos)

shiftRowLeftPlayers, shiftRowRightPlayers,
   shiftColumnUpPlayers, shiftColumnDownPlayers ::
      Coord -> PlayerMap -> PlayerMap
shiftRowLeftPlayers    = shiftPlayersGen fst (mapSnd cyclicPred)
shiftRowRightPlayers   = shiftPlayersGen fst (mapSnd cyclicSucc)
shiftColumnUpPlayers   = shiftPlayersGen snd (mapFst cyclicPred)
shiftColumnDownPlayers = shiftPlayersGen snd (mapFst cyclicSucc)


data BoardState =
   BoardState {
      stateBoard :: Directions Board,
      stateSymbolMap :: SymbolMap,
      statePlayerMap :: PlayerMap
   }

type Tile = (Directions Bool, Maybe Char)

cyclicPred :: (Eq a, Enum a, Bounded a) => a -> a
cyclicPred x = if x==minBound then maxBound else pred x

cyclicSucc :: (Eq a, Enum a, Bounded a) => a -> a
cyclicSucc x = if x==maxBound then minBound else succ x

shiftStateGen ::
   (Coord -> (Bool, Board) -> (Bool, Board)) ->
   (Coord -> (Maybe Char, SymbolMap) -> (Maybe Char, SymbolMap)) ->
   (Coord -> PlayerMap -> PlayerMap) ->
   Coord -> (Tile, BoardState) -> (Tile, BoardState)
shiftStateGen shiftBoard shiftMap shiftPlayers rowPos ((tile,symbol), state) =
   let (newExtra, newSymbolMap) =
         shiftMap rowPos (symbol, stateSymbolMap state)
       (newTile, newBoard) =
         FuncHT.unzip $ fmap (shiftBoard rowPos) $
         App.lift2 (,) tile (stateBoard state)
       newPlayerMap = shiftPlayers rowPos $ statePlayerMap state
   in ((newTile, newExtra),
       BoardState {
         stateBoard = newBoard,
         stateSymbolMap = newSymbolMap,
         statePlayerMap = newPlayerMap
       })

data Border = North | West | South | East
   deriving (Eq, Ord, Enum, Bounded, Show)

borderSet :: Directions Border
borderSet = Directions {north = North, west = West, south = South, east = East}

singleBorder :: Border -> a -> Directions a -> Directions a
singleBorder b a m =
   case b of
      North -> m{north = a}
      South -> m{south = a}
      East  -> m{east = a}
      West  -> m{west = a}

lookupBorder :: Border -> Directions a -> a
lookupBorder b =
   case b of
      North -> north
      South -> south
      East  -> east
      West  -> west

flipBorder :: Border -> Border
flipBorder b =
   case b of
      North -> South
      South -> North
      East -> West
      West -> East

shiftState :: Border -> Coord -> (Tile, BoardState) -> (Tile, BoardState)
shiftState b =
   case b of
      West -> shiftStateGen shiftRowRight shiftRowRightMap shiftRowRightPlayers
      East -> shiftStateGen shiftRowLeft shiftRowLeftMap shiftRowLeftPlayers
      South -> shiftStateGen shiftColumnUp shiftColumnUpMap shiftColumnUpPlayers
      North ->
         shiftStateGen shiftColumnDown shiftColumnDownMap shiftColumnDownPlayers


reachableFromPlayer :: Player -> BoardState -> Board
reachableFromPlayer player state =
   bfs (stateBoard state) $ singleCell $
   EnumMap.findWithDefault (error "player not in playerMap") player $
   statePlayerMap state

shapeRotations :: Eq a => Directions a -> [(Rotation, Directions a)]
shapeRotations shape =
   zip (if rotateDir180 shape == shape then [R0,R1] else allEnums) $
   iterate rotateDir90 shape

reachable1Single :: Board -> (Border, Coord) -> Tile -> BoardState -> Bool
reachable1Single cloud0 (b,pos) tile state0 =
   let state1 = snd $ shiftState b pos (tile, state0)
       cloud1 = bfs (stateBoard state1) $ cycleStripe b pos cloud0
   in any (boardGet cloud1) $ Map.keys $ stateSymbolMap state1

reachable1 :: (Tile, BoardState) -> [((Border, Coord), ([Rotation], Bool))]
reachable1 ((shape,symbol),state) =
   filter (not . null . fst . snd) $
   flip map (App.lift2 (,) allEnums [C1,C3,C5]) $ \shift ->
      (shift,
       mapPair (map fst, null) $
       ListHT.partition
          (\(_rot,rotShape) ->
             reachable1Single
                (Fold.foldMap singleCell $ statePlayerMap state)
                shift (rotShape,symbol) state) $
       shapeRotations shape)

reachable2Single :: Board -> (Border, Coord) -> Tile -> BoardState -> Int
reachable2Single cloud0 (b,pos) tile state0 =
   let (tile1,state1) = shiftState b pos (tile, state0)
       cloud1 = bfs (stateBoard state1) $ cycleStripe b pos cloud0
   in length $
      filter
         (\shift ->
            reachable1Single cloud1 shift (pure True, snd tile1) state1) $
      filter ((flipBorder b, pos) /=) $
      App.lift2 (,) allEnums [C1,C3,C5]

reachable2 :: (Tile, BoardState) -> [(((Border, Coord), Rotation), Int)]
reachable2 ((shape,symbol),state) =
   map
      (\(shift, (rot,rotShape)) ->
         ((shift, rot),
          reachable2Single
            (Fold.foldMap singleCell $ statePlayerMap state)
            shift (rotShape,symbol) state)) $
   App.lift2 (,)
      (App.lift2 (,) allEnums [C1,C3,C5])
      (shapeRotations shape)


formatRotation :: Rotation -> String
formatRotation = show . fromEnum

formatShift :: ((Border, Coord), Rotation) -> String
formatShift ((b, pos), rot) =
   Map.findWithDefault (error "forbidden coordinate")
      pos (lookupBorder b shiftCharMap)
   :
   formatRotation rot

formatShifts :: ((Border, Coord), ([Rotation], Bool)) -> String
formatShifts ((b, pos), (rot,complete)) =
   Map.findWithDefault (error "forbidden coordinate")
      pos (lookupBorder b shiftCharMap)
   :
   if complete then "*" else concatMap formatRotation rot

parseShift ::
   Maybe (Border, Coord) -> String ->
   ME.Exceptional String ((Border, Coord), Rotation)
parseShift forbidden input =
   case input of
      [arrowChar, rotation] ->
         App.lift2 (,)
            (do
               pos <-
                  ME.fromMaybe "not an arrow letter" $
                  Map.lookup (Char.toUpper arrowChar) arrowMap
               if Just pos == forbidden
                  then ME.throw "reversing the last shift is forbidden"
                  else return pos)
            (case rotation of
               '0' -> return R0
               '1' -> return R1
               '2' -> return R2
               '3' -> return R3
               _ -> ME.throw "not a rotation number from 0-3")
      _ -> ME.throw "need two input characters"

parsePosition :: Board -> String -> ME.Exceptional String (Coord, Coord)
parsePosition reachable input =
   case input of
      [rowChar, columnChar] -> do
         pos <-
            App.lift2 (,)
               (ME.fromMaybe "row letter out of range" $
                Map.lookup (Char.toUpper rowChar) rowMap)
               (ME.fromMaybe "column number out of range" $
                Map.lookup columnChar columnMap)
         if boardGet reachable pos
            then return pos
            else ME.throw "position unreachable"
      _ -> ME.throw "need two input characters"

inputLoop :: (String -> ME.Exceptional String a) -> IO a
inputLoop parse =
   ME.switch (\msg -> putStrLn msg >> inputLoop parse) return . parse
      =<< getLine

playerCharSet :: EnumSet Char
playerCharSet = EnumSet.fromList $ map (playerChar True) allEnums

inversePlayerChar :: Char -> String
inversePlayerChar c =
   if EnumSet.member c playerCharSet
      then ANSI.esc "" (ANSI.reverse []) "m" ++ c : ANSI.reset
      else [c]

printBox :: Box -> IO ()
printBox = putStr . concatMap inversePlayerChar . Box.render

main :: IO ()
main = do
   let gameLoop forbiddenShift (shape0, msymbol0) state0
            (NonEmpty.Cons (player,targets) remPlayers) = do
         let rotatedShapes =
               EnumMap.fromList $ zip allEnums $ iterate rotateDir90 shape0
         let symbol0 = maybe ' ' id msymbol0
         printBox $ Box.hsep 5 Box.top $
            arrowFrame forbiddenShift (formatWithSymbols state0 mempty)
            :
            (Box.vcat Box.center1 $ Fold.fold $
             EnumMap.mapWithKey
               (\k sh ->
                  [Box.text $ formatRotation k,
                   formatCell EnumSet.empty (pure False) symbol0 sh,
                   Box.emptyBox 1 0])
               rotatedShapes)
            :
            []
         let NonEmpty.Cons target remTargets = targets
         let state0Target =
              state0{
                 stateSymbolMap =
                    Map.filter (target==) $ stateSymbolMap state0,
                 statePlayerMap =
                    EnumMap.intersectionWith const (statePlayerMap state0)
                       (EnumMap.singleton player ())
              }
         let msymbol0Target = mfilter (target==) msymbol0
         let isAllowed (shift,_) = forbiddenShift /= Just shift
         case splitAt 5 $ filter isAllowed $
              reachable1 ((shape0,msymbol0Target),state0Target) of
            ([], _) -> do
               let xs =
                    take 5 $ List.sortBy (flip $ comparing snd) $
                    filter (isAllowed . fst) $
                    reachable2 ((shape0,msymbol0Target),state0Target)
               putStrLn $ "promising: " ++
                  List.intercalate ", "
                     (map (\(x,l) -> printf "%s(%d)" (formatShift x) l) xs
                        ++ ["..."])
            (xs, ys) ->
               putStrLn $ "hint: " ++
               List.intercalate ", "
                  (map formatShifts xs ++ take 1 (map (const "...") ys))
         let goal = playerChar True player : '\x2192' : target : []
         putStr $ goal ++ " Enter position and rotation of inserted piece: "
         ((border,coord), rotation) <- inputLoop $ parseShift forbiddenShift
         let (shape1,state1) =
               shiftState border coord
                  ((rotatedShapes EnumMap.! rotation, msymbol0), state0)
         let reachable = reachableFromPlayer player state1
         printBox $ coordinateFrame $ formatWithSymbols state1 reachable
         putStrLn ""
         putStr $ goal ++ " Enter coordinates of new player position: "
         pos <- inputLoop $ parsePosition reachable
         mNewPlayers <-
            if Just target /= Map.lookup pos (stateSymbolMap state1)
               then return $ Just $ NonEmpty.snoc remPlayers (player,targets)
               else do
                  putStrLn $ "found target: " ++ target : ""
                  case NonEmpty.fetch remTargets of
                     Nothing -> do
                        putStrLn $ "All targets found!"
                        return $ NonEmpty.fetch remPlayers
                     Just remTargetsNE@(NonEmpty.Cons newTarget _) -> do
                        putStrLn $
                           "new target: " ++ newTarget :
                           ", remaining: " ++ show (length remTargets)
                        return $ Just $
                           NonEmpty.snoc remPlayers (player, remTargetsNE)
         case mNewPlayers of
            Nothing -> putStrLn "All players found all their targets!"
            Just newPlayers ->
               gameLoop (Just (flipBorder border, coord)) shape1
                  state1{statePlayerMap =
                     EnumMap.insert player pos $ statePlayerMap state1}
                  newPlayers

   let (((shape,msymbol),(board,symbolMap)), shuffledTargets) =
         MS.evalState (App.lift2 (,) randomBoard shuffleTargets) $
         Rnd.mkStdGen 42
   let targets = associateTargets (Player1 !: Player2 : []) shuffledTargets
   let playerMap =
         EnumMap.intersectionWith (const id)
            (EnumMap.fromList $ Fold.toList targets) $
         EnumMap.fromList $ zip allEnums $ grid [C0,C6]
   gameLoop Nothing (dirsFromShape shape, msymbol)
      (BoardState board (fixedSymbolMap <> symbolMap) playerMap)
      targets

{-
let board = snd $ MS.evalState randomBoard $ Rnd.mkStdGen 42
printBox $ formatWays board $ bfs board $ singleCell (C1,C6)
-}