As is traditional, I’ll close with a postmortem. But first, a little story about why this victory means so much to me:
My experience at Hacker School was very enjoyable, but toward the end there was a single somewhat unpleasant event. On almost the last day, there was a “jobs dinner”, at which people from a bunch of different Hacker School-partnered companies came over to talk about where they work and meet Hacker Schoolers. At this point I was already pretty confident that I had a job, but I went anyway because it seemed a prudent thing to do. And because I was working on a type theory thing at the time, whenever someone asked me what I was working on I mentioned types.
Now, probably part of the blame for this falls on me, because I was bringing up all the theoretical stuff I’m interested in to a bunch of people with very practical jobs. So it would have been reasonable for them to not revise their estimates of my capability upward. Being enthusiastic about theory does not necessarily imply competence in practice. And maybe that was the extent of their response, just behaving reasonably and treating it as fun-to-hear but not really relevant, but… the impression I got was a bit more like
And an evening of this elicited a pretty strong reaction from me. First of all, I felt insulted, as if these people were under the impression that I’m only allowed to be good at one thing, and if I picked types, well… have fun in your little ivory tower while we do all the real work! And faced with all these people in industry saying that a good type system just gets in the way of real work, I started to feel uncertain. I’ve always maintained that a good type system just helps you tell the compiler what you were already thinking, and that once you know the language moderately well it hardly gets in your way at all. But what if these people were right, and types were in some ways strictly inferior to dynamic languages?
And here I am, with a finished product written in Idris (a language with one of the most powerful theoretical type systems I know of) in 48 hours. My confidence is restored.
Anyway, enough soapboxing. Here is the actual postmortem.
In terms of feature completeness, I had more than enough time to include almost everything I dared dream. Parameters like paddle size and the bouncing behavior of the ball are adjustable; I have a two-stage score card after every round that shows (by ticking their score up) who won; I have an AI that is actually kind of fun to play against and watch; there’s even an attract mode that pits two AIs against each other with randomized parameters. I think the only thing I even contemplated including that I didn’t get around to was sound.
I could have done this in Idris if I had wanted to, but I prefer a different approach to software design that makes minimal use of mutability or global variables. The way to imitate a game loop with mutable global state in a functional programming language is to have a function that recursively calls itself and store the “mutable” state as that function’s argument. But this doesn’t immediately lend itself to a callback-based approach: callbacks have no way of passing the updated state between each other.
After some worrying, frustrated hours of thought, I found a solution. At any given time, I’d have exactly one time-based callback waiting to resolve. This callback would reference a particular game state, simulate one frame and draw the results, and then register another time-based callback to do the same with the updated gamestate. This was frustratingly similar to standard recursion except that a lot of annoying boilerplate was involved. I think this could have been made a lot easier with a Continuation monad or a functional reactive programming system or an abstraction relating this infinite series of callbacks with a codata structure, or… something, but in 48 hours I had no time to find the elegant underlying pattern and crystallize it into a useful abstraction. Maybe some other time!
What should a Pong AI look like?
This is a question I struggled with for a while. It turns out it’s really simple to design an unbeatable AI player: just have it always, exactly follow the ball. In code-speak, always update the center of its paddle to have the same y-coordinate as the center of the ball. And indeed, this is what I did in the early stages of development, because I desperately needed to have the ball bouncing back at me and this unbeatable AI is perhaps the simplest Pong AI possible (other than “don’t move, ever”).
Once I had the basic architecture of the game loop ironed out, and had turned my attention to making the game fun to play, I had some questions to answer. How do I cripple this perfect AI but leave it competent enough that it’s still fun to play against and can beat a human on occasion?
I ended up deciding to give it a speed limit. This speed limit is actually not quite constant: there is a “maximum speed” which the AI reaches when its y coordinate is very far from the ball’s; the closer together they are, the slower the paddle follows the ball. The AI also gets extra lazy if the ball is far away in the horizontal direction. This gives it a nice feel where the AI paddle will kind of sedately move in the direction of the ball when it’s halfway across the table, followed by a quick repositioning when it gets closer. I’m sure there are better behaviors to give (maybe an AI that tries to return to the center of the table after hitting the ball, like a human ping pong player returning to bodily equilibrium), but all the same I really like what I came up with.
Attract mode: functional programming to the rescue!
For attract mode, I knew I’d want to have a pair of AIs playing against each other. Luckily, I already had the AI tracking logic factored out into a separate function, and this required only a minor adjustment to calculate the desired position of a paddle on the human side, so figuring out what the left paddle should do was pretty simple.
The issue was my
step function, which advances a game state by one frame. Since you can’t advance the game state by a frame without knowing where the player’s paddle should be moving in that frame, that meant my
step function had to reference player input, which meant I couldn’t reuse it for attract mode where the left paddle should be AI-controlled.
Oh, wait. I’m writing in a functional language, and
step is a pure function from input objects to outcomes (an outcome is either “the next frame of the simulation”, or “the player won this round”, or “the AI won this round”, or “the player pressed Escape, so we should return to the main menu”). An input object is a simple bundle of data containing a mouse position and a boolean representing whether the player has pressed “escape” — the normal source of such objects is from an impure IO action that reads the player’s input, but there’s nothing saying that has to be the only source. So all I had to do was construct an input object stating that the player had moved the mouse to the position that the AI would have moved to, and we have a virtual player!
(For bonus points, I later realized that it would be nice to be able to press escape to quit from attract mode back to the main menu. This, too, was a simple adjustment — instead of constructing an input object from whole cloth, I procured one from the normal impure IO action and just updated its “mouse position” field, leaving the “escape” boolean faithful.)
In conclusion: I am super happy with the outcome of this project and you should totally humor me by playing for a few moments! (And watch attract mode, because I am so proud that I managed to make it work.)
If you’re interested in the source, here it is one more time: https://github.com/trillioneyes/idris-pong.