

A Clone of Chips Challenge in Haskell - egonschiele
https://github.com/egonSchiele/chips

======
egonschiele
Hi HN! Author here. I made this game because I wanted to see what it was like
to make video games with Haskell. It turned out to be a pretty good fit. If
you are interested, here's an explanation of the code:
[http://vimeo.com/109663514](http://vimeo.com/109663514)

You might also be interested in this framework if you want to make games of
your own:
[https://github.com/egonSchiele/actionkid](https://github.com/egonSchiele/actionkid)

~~~
freyrs3
This is extremely well done, thanks for taking the time to make a screencast.

------
misja
I'm learning Haskell and I find this project very helpful and educating.
There's one thing I'm wondering about, and that's the lack of (unit) tests.
I'm coming from the Java world and I'm used to write unit and integration
tests for most of the code I write. But I have noticed that in Haskell
projects this is not so common.

Why is that? Is it a cultural thing, or is Haskell's strict type system and
immutability so helpful that unit tests are not so necessary comparing to for
instance Java?

~~~
gamegoblin
You may have just looked at some poor projects. In fact, Haskell has a two
really great testing frameworks:

HUnit, directly based on JUnit:
[http://hackage.haskell.org/package/HUnit](http://hackage.haskell.org/package/HUnit)

Quickcheck, which uses randomized testing:
[http://hackage.haskell.org/package/QuickCheck](http://hackage.haskell.org/package/QuickCheck)

HUnit will be very familiar to you, Quickcheck is definitely more interesting.
You specify some invariants and the heuristic algorithm tries to come up with
random inputs to break your function. It's potentially less accurate as good
unit tests, but it's also a ton less work.

I can just say something like "This function takes an array of integers as
input, and I expect an output array of the same size, where all numbers are
even, and there are no duplicates" (in math-y form, of course), and quickcheck
will try however man random inputs I specify to try to find a case that breaks
that invariant.

Consider a well known Haskell library, attoparsec. It has extensive tests and
benchmarks:
[https://github.com/bos/attoparsec](https://github.com/bos/attoparsec)

That being said, there are those in the Haskell community who take Haskell's
referential transparency and equational reasoning to a whole new level and
don't write tests, but instead write _proofs_.

For an example, see the popular Pipes library:
[https://github.com/Gabriel439/Haskell-Pipes-
Library/blob/mas...](https://github.com/Gabriel439/Haskell-Pipes-
Library/blob/master/laws.md)

~~~
PieSquared
In addition to the libraries mentioned above, there's also some very good
testing _frameworks_ (which can run test suites composed of HUnit and
Quickcheck tests), such as HSpec and tasty (and various extensions such as
tasty-golden for golden tests and tasty-quickcheck for quickcheck tests). I've
used these in various projects of mine; as with other languages, sometimes TDD
is the right approach, sometimes you write a few tests afterwards to make sure
everything works, and sometimes you can get away with no testing at all.

That said, I do feel that the type of testing I do in Haskell is often quite
different from other languages. Due to the strict type system, some types of
errors can be eliminated at compile time, and thus do not need to be tested
for. Of course, as the parent pointed out, some people take this to a (very
awesome) extreme, but even in a less rigorous form the static guarantees can
eliminate many things that I would write unit tests for in other languages.

Finally, you may also be interested in SmallCheck, an alternative to
QuickCheck. From its documentation:

> The big difference [between SmallCheck and QuickCheck] is that instead of
> using a sample of randomly generated values, SmallCheck tests properties for
> all the finitely many values up to some depth, progressively increasing the
> depth used. For data values, depth means depth of construction. For
> functional values, it is a measure combining the depth to which arguments
> may be evaluated and the depth of possible results.

This means that SmallCheck will often find small and interesting
counterexamples to the properties you wish to affirm. (As an aside, note that
writing property-based tests is a skill, just like writing good unit tests.)

tl;dr: Testing can sometimes be less necessary, but is certainly not
unnecessary, and there are some very high quality and well-documented
libraries for testing in Haskell.

------
crimsonalucard
Honestly, is functional programming really better for this type of game? I
find the structure and nature of the game more fitting to an object oriented
approach. IMO Chips, blocks, player character all tend to be better described
as mutable objects, because that's what they actually are from the users
perspective....

~~~
jerf
In addition to the other fine replies: As a game gets bigger and more
complicated, you'll often find that the direct mutable OO approach becomes an
antipattern at scale.

Do you have anything that you want to do, but discover halfway through for
some reason it's not legal? Good luck undoing what was half done. Of course
you might think the simple answer is "don't do that", but as the game gets
bigger and responsibilities get spread out between all the objects it becomes
increasingly easy for some action to become possible that requires a lot of
checking, and if things are just willy-nilly mutating themselves the odds of
you encountering this sort of problem skyrocket. And that's just one
example... games by their nature resist the sort of decomposition that most
"real" software can have done on it, because as soon as you tell a designer
that X can't talk to Y, by golly they'll start thinking about how awesome it
would be if X _was_ talking to Y. You don't get a pretty tree of relationships
with occasional crosstalk, you get a nasty snarl of communication patterns,
irreducibly.

Many games end up functional under the hood anyhow, with a clearly-defined
loop:

    
    
        1. Ask all the objects in the world what they "want" to do.
        2. Validate and resolve conflicts.
        3. Render the new world state.
    

One of OO's big problems in general is lack of control over the profound,
often-overpowerful effects of mutation, and used in an undisciplined manner,
games often turn into a worst-case-scenario for OO. And most of using them in
a "disciplined" manner turns out to be using "FP" techniques to keep the
complexity under control.

Of course nothing stops you from doing that in OO, with discipline and care
unenforced by the compiler. But what it does mean is that suddenly the
"mismatch" that you're referring to disappears, and one starts wondering
whether perhaps FP is the better match for games and OO the accident of
history.

~~~
webjprgm
I might also point out that OO and FP are not strictly at odds. You can use
object libraries in FP languages to encapsulate the state and logic but avoid
the mutation of the state.

So if the two issues are orthogonal then you're both right. :-)

~~~
crimsonalucard
Good point. So if I use OO while maintaining immutability then am I doing
functional programming, OO or both?

~~~
jerf
At the risk of going really philosophical, in the end words are just words,
and your program is what it is. Given the vast, vast, _vast_ space of possible
programs it should come as no surprise that little tiny English words are
hardly up to the task of completely rigidly classifying them. _Especially_
programs, which are quite literally the most perverse mathematical constructs
known to man, courtesy of the Church-Turing thesis.

------
iwasanewt
Aha! A bug! [http://i.imgur.com/2vNiRzn.gif](http://i.imgur.com/2vNiRzn.gif)
(you can walk on ice if you have skates, but you can't collide with walls
without being "repelled")

Also, I completely forgot about this wonderful game. Thanks for the trip on
memory lane.

~~~
egonschiele
Ouch! I'll have to fix that. In the meantime you can skip to the next level by
pressing 'n'.

~~~
iwasanewt
oh, there's no need to skip the level. you can escape the bump by pressing
forward+left/right

------
alexchamberlain
Chips was one of the first applications I ever used as a kid. It was Windows
3.1, and I distinctly remember knowing how to start `windows` from the DOS
prompt.

------
CoffeeDregs
I gotta say that, after not having used it in about 4 years, I love looking at
Haskell again.

One question: could CurrentTile.hs be DRYed up?
[https://github.com/egonSchiele/chips/blob/master/src/Chips/C...](https://github.com/egonSchiele/chips/blob/master/src/Chips/CurrentTile.hs)

This repeating pattern seems ripe for DRYing:

    
    
        checkCurTile (Chip _) = do
          liftIO $ playSound (soundDir ++ "collect_chip.wav") False
          tileAt Current .= (Empty def)
        checkCurTile (Gate _) = do
          liftIO $ playSound (soundDir ++ "door.wav") False
          tileAt Current .= (Empty def)
        checkCurTile (KeyYellow _) = do
          yellowKeyCount += 1
          tileAt Current .= (Empty def)
        checkCurTile (KeyBlue _) = do
          blueKeyCount += 1
          tileAt Current .= (Empty def)
        [...]

~~~
codygman
Sorry for bad formatting and not compile checking the code, but this is an
improvement imo:

    
    
        checkCurTile b = do
          case b of
           Chip -> liftIO $ playSound (soundDir ++ "collect_chip.wav") False
           Gate -> liftIO $ playSound (soundDir ++ "door.wav") False
           (KeyYellow _) -> yellowKeyCount += 1
           (KeyBlue _) -> blueKeyCount += 1
          tileAt Current .= (Empty def)

