2

I am trying to implement a game with multiple levels in Haskell. Each level has different but generalized game state. We can define the game state like the following-

type Player = (Float, Float) -- Player coordinates
data State a = State Player a Bool

Here, a is the environment. In one level it is defined as String and in another level it is defined as ([Int], [Int]). And the Bool determines if level is complete or not.

I have two different functions implemented that uses play from Graphics.Gloss.Interface.Pure.Game. I can inject any of this functions to the main and play them individually. For example-

lv1 = play window black 90 (State (0,0) "Hello" False) drawLv1 handleLv1 updateLv1
lv2 = play window black 90 (State (0,0) ([1,2,3,4], [0,0,0,0]) False) drawLv2 handleLv2 updateLv2

main :: IO()
main = lv1
-- main = lv2

However, I want to go from lv1 to lv2 somehow. I know that Haskell functions can be passed through as values and there is obviously a way to change the main function from lv1 to lv2 but I cannot figure that out.

A solution or maybe an idea on how the problem can be generalized and handled properly, would be very nice.

andrewJames
  • 19,570
  • 8
  • 19
  • 51
pptx704
  • 160
  • 10
  • It's not completely clear to me what you're asking. If you just want to play one level followed by the other, then it's as simple as sequencing the IO actions: `main = do { lvl1; levl2; }`. However if you want a more realistic "game" where you proceed to level 2 only if you "beat" level 1, then that will take a lot more logic. From having looked at the documentation for Gloss - which I'm not familiar with - it seems like you'll have to build more complex "draw", "handle" and "update" functions from the ones you have to handle progressions from one level to another. – Robin Zigmond Jul 02 '22 at 10:32
  • @RobinZigmond I have the functions defined. In the code snippet they are `drawLv1`, `handleLv1` and so on. Also, yes, I need to proceed to lv2 only when I pass lv1. So it has been tricky because I cannot find a way to change the functions on the go. – pptx704 Jul 02 '22 at 14:57

2 Answers2

1

When you say that the levels can have different types, somehow your game code that plays the levels must be interpreting and modifying this state value. So there must be functions from a String to some types in your game, and equivalent function from ([Int], [Int]) to the same types.

If so then you need to put these functions in a class:

class MyGameState a where
   someFunction :: a -> Int
   someOtherFunction :: a -> Thing
   updateGameState :: Int -> a -> a

instance MyGameState String where
   someFunction str = .....
   someOtherFunction str = ......
   updateGameState n oldState = ......

And likewise for instance MyGameState ([Int],[Int]). Then you can define your game play function using someFunction and someOtherFunction.

Paul Johnson
  • 17,438
  • 3
  • 42
  • 59
  • Thanks for your idea. Whilst this is almost a correct solution, this arises a new problem. For example, my `lv3` also has String as the type. But the handler and update state functions are not the same. – pptx704 Jul 02 '22 at 14:59
  • 1
    Then wrap your `String` up in a `newtype` and give that a suitable instance. – Paul Johnson Jul 02 '22 at 20:46
1

I decided to write how I finally solved the problem since others might face the same problem as me-

First of all, I changed the State data to make a generalized system for better transition between different levels. So instead of current State a data, I now have State but with a new parameter Level

type Player = (Float, Float) -- Player coordinates
-- data State a = State Player a Bool
data State = State {
    getPlayer :: Player,
    getLevel :: Level,
    levelCompleted :: Bool
}
data Level = Lv1 String | Lv2 ([Int], [Int])

Then I needed to fix the level specific functions accordingly. For example-

updateLv1 :: Float -> State -> State
-- If com is true then level is complete
updateLv1 _ (State player (Lv1 st) com) =
    if com then State (0,0) ([1,2,3,4], [0,0,0,0]) False
    else
        ...
        ...

updateLv1 time state = commonUpdater time state

Same goes for drawing and event handlers.

Finally I had to fix the common handlers-

commonUpdater time state = case getLevel state of
    Lv1 _ = updateLv1 time state
    Lv2 _ = updateLv2 time state

I had to implement the commonHandler and commonDrawer too. The process is almost the same.

Finally the play function was easy. I just had to initialize the Lv1 state and rest were to be handled by common functions-

game :: IO()
game = play window black 90 (State (0,0) (Lv2 "Hello") False) commonDrawer commonHandler commonUpdater

main :: IO()
main = game

That's all!

pptx704
  • 160
  • 10