Another short day since I had several phone interviews. Thanks to the folks who left comments!
I got a little further today; I feel like I'm starting to understand Haskell's data handling a little bit better. It's a cliché but I think the hard part is un-learning, and understanding what something like this doesn't do. So here's where it stands now -- not finished by any means, but coming along, with painful slowness as I continue to learn:
data Dir = North | East | South | West deriving (Show, Eq) data Pos y x = Pos Int Int deriving (Show, Eq) -- N.B.: capitalization of initial letters in posY, posX is -- semantically important! posY ( Pos y x ) = y posX ( Pos y x ) = x data Tile = Empty | Tree | Mountain | House | Ice_Block | Bomb | Heart | Edge deriving (Show, Eq) -- Different types of tiles have different properties in -- different interaction contexts: -- The penguin can walk through empty tiles or trees (forest) walkable :: Tile -> Bool walkable t = ( t == Empty ) || ( t == Tree ) -- But everything except empty tiles will block sliding objects blocking :: Tile -> Bool blocking t = ( t /= Empty ) -- A subset of tiles are movable (and will slide until blocked) movable :: Tile -> Bool movable t = ( t == Bomb ) || ( t == Heart ) || ( t == Ice_Block ) -- A subset of tiles aren't movable; note that this set -- overlaps blocking and that Tree is both walkable and fixed fixed :: Tile -> Bool fixed t = ( t == House ) || ( t == Mountain ) || ( t == Edge )
That all should be fairly non-controversial, I think. The predicate approach to classifying tiles in different contexts may actually make more sense in Haskell, given that I can then use these predicates as guards. The replacement for a simple struct, Pos, still feels awkward -- I haven't really dug into whether it could be improved with record syntax, or some other technique. For now it's there because it works.
All the beginner tutorials say "don't use arrays, don't use arrays, don't use arrays!" At least not until I reach the stage where I need to optimize the implementation. So I'll try that. Let's try a list, and I'll extract "slices" from it, lists starting at a given Pos going in one of four different directions. Eventually I want the slice function to terminate the slices with Edge tiles that aren't actually stored in the list. So... I have to think about this some more, but here's a single case, sort of taken care of:
type Board = [[Tile]] slice :: Board -> Pos y x -> Dir -> [Tile] slice board pos East = drop ( posX pos ) $ head $ drop ( posY pos ) board slice _ _ _ = error "slice: not handled yet!"
I don't have slide finished, but here's a version of collide that works, at least a little:
collide :: [Tile] -> [Tile] collide (t:(Empty:ts)) | movable t = [Empty] ++ collide (t:ts) collide (Bomb:(Mountain:ts)) = [Empty, Empty] ++ ts collide (Heart:House:ts) = [Empty, House] ++ ts collide (_) = error "collide: unexpected case!"
The nested pattern (Bomb:(Mountain:ts)) was sort of a flash of inspiration -- but it appears that maybe both this version and the (Heart:House:ts) version work the same -- I think -- so perhaps it's kind of pointless. It seemed to go along with the "destructure it the way you would structure it" idea, although I would normally not build a list out of cons cells unless it was irregular in some way.
Here's the penguin step function, returning True if the penguin can move onto the tile at the head of the list:
step :: [Tile] -> ( Bool, [Tile] ) step  = error "step: empty list!" step ts = if walkable (head ts) then ( True, ts ) else ( False, collide ts )
And there's a move, which "absorbs" the case where the penguin is turned to face a different direction. It's not really done; the idea is that it will give back the board, basically generating a new world. For now we kind of punt on the question of how to rebuild the board out of the existing board and the modified "slice" -- and so the I just return a list as the first element of the tuple. In the first case where the penguin hasn't moved, that doesn't actually make sense, but it satisfies GHC for now (wow, she's kind of a harsh mistress, but you've got to love those thigh-high black leather boots!)
move :: Board -> Pos y x -> Dir -> Dir -> ( [Tile], Pos y x, Dir, Dir ) move board pos move_dir penguin_dir = if move_dir /= penguin_dir then ( head board, pos, move_dir, move_dir ) else ( collide $ slice board (Pos 1 0) penguin_dir, pos, penguin_dir, penguin_dir )
Boy, that's tuple-icious... not sure I like it, but it's a start. So:
*Main> walkable Tree True *Main> :t Pos Pos :: Int -> Int -> Pos y x *Main> let slice = [Heart, House] *Main> collide slice [Empty,House] *Main> let slice = [Bomb, Empty, Mountain] *Main> collide slice [Empty,House] *Main> let board = [[Empty, Tree, Empty, Edge], [Bomb, Empty, Mountain, Edge]] *Main> move board (Pos 1 0) West East ([Empty,Tree,Empty,Edge],Pos 1 0,West,West) *Main> move board (Pos 1 0) East East ([Empty,Empty,Empty,Edge],Pos 1 0,East,East)
More tomorrow if I can manage it! Oh, and it's here, such as it is: https://github.com/paulrpotts/arctic-slide-haskell