Skip to content
October 9, 2011 / cdsmith

Just for fun: Hangman with gloss-web

So this will be brief.  Ivan Miljenovic pointed me to a discussion about writing games with kids, and the question came up about how clearly one could write a hangman game.  To I took that a challenge, and I present to you: Hangman in gloss-web!

Edit: I’ve now added a random number generator to the signature of the initial worlds, so it even chooses words at random.

The code:

import Graphics.Gloss
import Graphics.Gloss.Interface.Game

import System.Random

import Data.Char

possibleWords = [
    "cat", "hat", "zebra", "delicious", "water",
    "elephant", "granite", "sunset", "dragon"
    ]

data World = World {
    word    :: String,
    strikes :: Int,
    guesses :: [Char]
    }

initial g = World (possibleWords !! i) 0 []
  where (i, _) = randomR (0, length possibleWords - 1) g

lost world = strikes world == 6
won  world = all (`elem` guesses world) (word world)
over world = won world || lost world

event (EventKey (Char c) Down _ _) world
  | not (isLetter c)                     = world
  | over world                           = world
  | toLower c `elem` guesses world       = world
  | toLower c `elem` word world          = world { guesses = c : guesses world }
  | otherwise                            = world { strikes = strikes world + 1,
                                                   guesses = c : guesses world }
event _                            world = world

step time world = world

draw world = pictures [ frame, hangee world, answer world ]

frame = pictures [
    line [ (100, -50), (-100, -50), (-100, 200), (0, 200), (0, 150) ],
    line [ (-75, -50), (-100, -25) ]
    ]

hangee world = pictures (take (strikes world) parts)
  where parts = [ hangeeHead, hangeeBody, leftArm, rightArm, leftLeg, rightLeg ]

hangeeHead = translate 0 125 (circle 25)
hangeeBody = line [ (0, 100), (  0, 30) ]
leftArm    = line [ (0,  90), (-30, 60) ]
rightArm   = line [ (0,  90), ( 30, 60) ]
leftLeg    = line [ (0,  30), (-30,  0) ]
rightLeg   = line [ (0,  30), ( 30,  0) ]

answer world = translate (-50) (-200)
             $ scale 0.3 0.3
             $ color (wordColor world)
             $ text (letters world)

wordColor world
  | won  world = dark green
  | lost world = red
  | otherwise  = black

letters world = concat [
    if c `elem` guesses world then [c, ' '] else "_ "
      | c <- word world
    ]
And, here's what it looks like:

To run the code, visit http://dac4.designacourse.com:8000, choose Game, and copy and paste the code above into the editor area.  Or visit this link to just play the game.  Have fun!

8 Comments

Leave a Comment
  1. matthias / Oct 9 2011 9:50 am

    In our ‘World’ and ‘Universe’ setting (Racket, ProgramByDesign, see ICFP ’09), there are several solutions to the random word problem. The first one would be to write a single function, hangman, which maps a word to a game process. In this setting, one player would launch the program and another one would play. Alternatively, your library would provide a higher-order function, say provide-random-word, and the kids could launch it like this: (provide-random-word hangman). The second solution would be to equip each callback function with an optional random letter or integer or word or whatever. In this setting, the hangman game comes with one extra handler, which uses the first or second or whatever random word from the handler to set the World state. And voila, you remain purely functional.

    I thought a lot about the random function issue because our Racket-inspired teaching languages are purely functional. In the end, however, I decided to go with plain random in our teaching languages. I am not fanatical enough to push pure functional programming to the extreme in serious programming courses. In reality most kids will and should also program in imperative languages.

    • cdsmith / Oct 9 2011 10:36 am

      Absolutely, I agree that kids who go on to do computer programming will learn imperative languages. Ideological purity isn’t the issue here! Rather, it’s that I don’t see a good interface for it. Because the assumption of purity is built in to Haskell more deeply than Scheme, there would be some structural changes needed. I really do think StdGen is the cleanest interface here. Without that, I’d have an unappealing choice between hand-waving “ignore this, it’s just magic” stuff, teaching a fairly complex operational model, or moving all the code into a monad. All of those are non-starters for me. They’d also require that I build my own library, and so far I haven’t written any of my own libraries for this environment at all. I’m offering a thin top-level interface between the environment and the program (just “implement these names with these types”), but I like the idea of telling the students that the library they are using is one that’s out there and used by lots of people all over the world.

      • matthias / Oct 9 2011 11:17 am

        You already wave your hands because your framework picks up certain names from the kids’ files/programs. (Beyond that you provide take and map and other ‘magic’ functions). In our World/Universe library, they write something like

        onKeyDo: key-event-handler
        onClockTickDo: clock-event-handler
        onMouseClickDo: mouse-event-handler

        All of this is collected into a ‘big-bang’ construct for ‘Worlds’ so that kids can write functions that look like

        fun hangman(initial-world)
        bigbang initial-world onKeyDo ….

        Our kids now know that this declarative description of the world asks the computer to run certain handlers when certain events happen.

        Now read my reply from before and you will see that provide-random-word(hangman) introduces no magic, does not need to make the monad explicit, and looks like all other functional stuff to the kids. Best of all, kids can run the function on their own with their chosen word.

        (BTW, this is exactly what I meant with my very first post: no monads needed.)

      • cdsmith / Oct 9 2011 12:44 pm

        Matthias, I’m not sure I understand your suggestion here. Certainly I could make things work where the framework provides with a random word, but then I’m writing code specific to hangman in the framework, which of course isn’t a reasonable design. So I’d guess I’m missing something about your suggestion.

        The closest equivalent seems to be what I mentioned in the post: defining initial to receive an StdGen. Then initial would still be responsible for using it to pick a random word, but that’s at least possible given an already-initialized pseudo-random number generator. And other programs could choose to store it, and thread it through other computations. Then I’d agree that a good design might be for a student to write a function with a word as a parameter, and a higher-order function from (String -> World) to (StdGen -> World), but that’s part of the student’s work, not the framework.

        Of course, reading /usr/share/dict/words is also impossible, but I’m less concerned about that. The program will just have to get its list of words elsewhere.

  2. camaiocco / Oct 9 2011 10:24 am

    Funny I was thinking about it just now, googled “gloss haskell random” and got here.
    Aside that, we can read the mouse position to select a word from a dictionary.
    It would be possible also to pass a random stream along time parameter in animation function.
    Something like : animate’ :: Time -> Noise -> Picture.

    • cdsmith / Oct 9 2011 10:38 am

      That’s basically the same idea as StdGen, which would act like your Noise type. I think I’d leave it out of the animation mode, and introduce it into simulations and games, where you can either choose to store it in your world type, or not.

      For example, the specialized type of randomR is

      randomR :: Random a => (a, a) -> StdGen -> (a, StdGen)

      You give it a range and a noise source, and it returns a value and an updated noise source.

  3. matthias / Oct 9 2011 4:45 pm

    Here is how we would implement the second posters idea:

    
    #lang racket
    
    (require 2htdp/universe 2htdp/image)
    
    (define words (file->list "/usr/share/dict/words"))
    (define word# (length words))
    
    (define (hangman)
      (big-bang "" 
                (on-mouse (lambda (world-state x y mouse-event)
                            (cond
                              [(and (string=? world-state "") (mouse=? mouse-event "button-down"))
                               (define pick (modulo (* x y) word#))
                               (symbol->string (list-ref words pick))]
                              [else world-state])))
                (to-draw (lambda (w) (text w 22 "red"))   200 30)))
    

    I think providing a (lazy) list of all words in /usr/share/dict/words from Gloss should not be considered a hangman-specific idea. There are other games and animations that will need a dictionary. Then you could initialize the state to the empty word and use the first button-down event from the mouse to choose a word.

    • cdsmith / Oct 9 2011 5:05 pm

      I think what will be most consistent with the design of this environment will be: (a) providing a pre-initialized pseudo random number generator to the initial world, and (b) handling this words issue outside of gloss, for example by exporting it as a symbol from a different package, which can contain reasonable logic across various environments, such as reading the file if it exists on the operating system, using a built-in list if not, etc.

      I’ve pretty much decided already to provide a random number generator to initial, which is the starting state of the world (similar to the first parameter to your big-bang). I just need to implement it, and then hunt down and then deal with the resulting incompatibility.

Leave a reply to matthias Cancel reply