z0w0/helm

Add more extensive examples

z0w0 opened this issue · 7 comments

z0w0 commented

There's currently only a flappy bird clone example. I'd like to see at least three more, such as:

  • Shooter
  • Platformer
  • Physics playground

Want to back this issue? Post a bounty on it! We accept bounties via Bountysource.

Though it doesn't strictly fit into any of the categories above, would there be interest in creating a sort of Minesweeper clone?

z0w0 commented

Yep. The categories were just examples, anything that's a game would be very welcome. Even if it's very simplistic

Okay, cool. I've started on the minesweeper example and it should be up and running soon.

@TheLostLambda hey, do you need any help with minesweeper? In terms os questions / answers etc.

Hmm, to be entirely honest, I recently started school up again so I haven't made too much progress on the minesweeper front. It looks like I got a decent ways with it. It currently allows you to reveal tiles and once revealed they will show as red if there is a mine there and white if there is not. Pressing the R key resets the game.

The bits that aren't finished are: placing flags, displaying the number of surrounding mines, auto-revealing fields, and actually being able to win or lose the game.

Now that you have brought it back to my attention, there is a decent chance that I will actually pick it up and finish it, but that isn't a guarantee — particularly with school filling my time. If I don't get around to finishing it soon, feel free to check out the code I have so far:

{-# LANGUAGE RecordWildCards #-}
import           Data.Foldable   (minimum)
import           Data.List       (nub)
import           Data.Maybe      (isJust, fromJust)

import           Linear.V2       (V2 (V2))
import qualified System.Random   as Rand

import           Helm
import qualified Helm.Cmd        as Cmd
import           Helm.Color
import           Helm.Engine.SDL (SDLEngine)
import qualified Helm.Engine.SDL as SDL
import           Helm.Graphics2D
import qualified Helm.Keyboard   as Key
import qualified Helm.Mouse      as Mouse
import qualified Helm.Sub        as Sub
import qualified Helm.Window     as Win

screenSize :: V2 Int
screenSize = V2 600 600

padding :: Int
padding = 2

gameSize :: Int
gameSize = 15

mineCount :: Int
mineCount = 30

data Action = Idle | Init Rand.StdGen | Restart | Resize (V2 Int) | Click Mouse.MouseButton (V2 Int)

type Position = (Int, Int)

data Model = Model
  { revealed :: [Position]
  , flags    :: [Position]
  , mines    :: [Position]
  , size     :: V2 Int
  , state    :: Int
  }

initial :: V2 Int -> (Model, Cmd SDLEngine Action)
initial s = ( Model
    { revealed   = []
    , flags      = []
    , mines      = []
    , size       = s
    , state      = 0
    }
  , Cmd.execute Rand.newStdGen Init
  )

update :: Model -> Action -> (Model, Cmd SDLEngine Action)
update model Idle = (model, Cmd.none)
update model@Model{..} (Click Mouse.LeftButton pos)
  | isJust clickIndex = (model { revealed = nub $ fromJust clickIndex : revealed }, Cmd.none)
  where clickIndex = decodeClick model pos
update model (Click _ _) = (model, Cmd.none)
update Model{..} Restart = initial size
update model (Resize s) = (model { size = s }, Cmd.none)
update model (Init rng) = (model { mines = take mineCount . nub $ zip (randomLst xGen) (randomLst yGen)}, Cmd.none)
  where (xGen, yGen) = Rand.split rng
        randomLst = Rand.randomRs (0, gameSize - 1)

decodeClick :: Model -> V2 Int -> Maybe (Int,Int)
decodeClick model (V2 mx my)
  | null tile = Nothing
  | otherwise = Just (head tile)
    where positions = map (\(i,pos) -> (i, fmap round pos)) $ tilePositions model
          tileRadius = round $ tileSize model / 2
          ranges = map (\(i, V2 x y) -> (i, ((y + tileRadius, y - tileRadius)
                                        ,(x + tileRadius, x - tileRadius)))) positions
          tile = map fst $ filter (\(_,((yu, yl), (xu, xl))) -> (my < yu) && (my > yl)
                                                             && (mx < xu) && (mx > xl)) ranges

tileSize :: Model -> Double
tileSize Model{..} = fromIntegral (minimum size - padding * (gameSize + 1)) / fromIntegral gameSize

tilePositions :: Model -> [(Position, V2 Double)]
tilePositions model =
  [ ((rn - 1, cn - 1), V2 (toPos rn) (toPos cn))| rn <- [1..gameSize], cn <- [1..gameSize]
  , let toPos n = fromIntegral n * (tileSize model + fromIntegral padding) - tileSize model / 2]

subscriptions :: Sub SDLEngine Action
subscriptions = Sub.batch [ Win.resizes Resize
                          , Key.ups (\k -> case k of Key.RKey -> Restart
                                                     _        -> Idle)
                          , Mouse.ups Click
                          ]

view :: Model -> Graphics SDLEngine
view model@Model{..} = Graphics2D . collage . map toTile $ tilePositions model
  where toTile (index, pos) = move pos $ filled (tileColor model index) $ square (tileSize model)

tileColor :: Model -> Position -> Color
tileColor Model{..} pos
  | pos `elem` revealed && pos `elem` mines = rgb 1 0 0
  | pos `elem` revealed = rgb 1 1 1
  | otherwise = rgb 0.5 0.5 0.5

main :: IO ()
main = do
  engine <- SDL.startupWith $ SDL.defaultConfig
    { SDL.windowDimensions = screenSize }

  run engine GameConfig
    { initialFn       = initial screenSize
    , updateFn        = update
    , subscriptionsFn = subscriptions
    , viewFn          = view
    }

As for specific issues I've been having, there is nothing other than my lack of time that has set back the completion of minesweeper, but one little thing that has been bothering me is not being able to lock window resizing to a particular aspect ratio. I currently have support for nicely scaling the window's contents, but if it is wider than it is tall, for example, there is just a black empty space to the right of the actual minesweeper grid.

It would be nice if there was a way to allow resizing, but also force the width and height of the window to remain the same. Again, this is mearly a little annoyance that I had experienced, and isn't particularly hindering progress, but if you have a solution for the issue, that would be great!

Let me know if you have any questions and I will keep you all posted if I make any progress!
Brooks J Rady

@TheLostLambda interesting problem. Set is no exposed through Helm yet, so implementation is a little bit tacky and works directly with SDLEngine. Here is how I implemented aspect fit:

module Main where

import Helm
import Helm.Engine.SDL.Engine
import Helm.Graphics2D

import qualified Helm.Cmd as Cmd
import qualified Helm.Window as Window
import qualified Helm.Engine.SDL as SDL

import Data.StateVar (($=))
import Linear.V2 (V2(V2))
import qualified SDL.Video as Video

data Action = Idle | FitWindow Video.Window (V2 Int)
data Model = Model

initial :: (Model, Cmd SDLEngine Action)
initial = (Model, Cmd.none)

fit :: (V2 Int) -> (V2 Int)
fit (V2 width height) = V2 dimension dimension
  where dimension = min width height

update :: Model -> Action -> (Model, Cmd SDLEngine Action)
update model Idle = (model, Cmd.none)
update model (FitWindow window size) = (model, resizeCommand)
  where resizeCommand = Cmd.execute (Video.windowSize window $= fromIntegral <$> fit size) $ const Idle

subscriptions :: Video.Window -> Sub SDLEngine Action
subscriptions window = Window.resizes $ FitWindow window

view :: Model -> Graphics SDLEngine
view _ = Graphics2D $ collage []

main :: IO ()
main = do
  engine@SDLEngine { window } <- SDL.startup
  run engine GameConfig
    { initialFn       = initial
    , updateFn        = update
    , subscriptionsFn = subscriptions window
    , viewFn          = view
    }

I added #121 to expose window size through Engine, although could be a smaller priority in compare with other tasks.

Does anyone wants to port Mario from Elm examples? We are kinda Elm inspired, it will makes sense to have game examples that they have :) http://debug.elm-lang.org/edit/Mario.elm Also it will be the first example with at least some sprites involved.