in which four pieces are placed in a row

The other day my son and I were at a friend's house, and we were just on our way home. As we were leaving he saw they had the game Connect 4 and asked if we could play. Since we were on our way I told him, "We can't play the game now, but when we get home, we can program the game, and then play that." I wasn't sure exactly how this would work out, but I thought we'd have some fun on the way.

This isn't the first time I've adapted a physical game to a program with my kids. But since then I've done most of my games using LÖVE, the 2D Lua game framework along with Polywell, a text editor and development tool that runs in it. Polywell is roughly a port of Emacs, and I've found that the foundation it provides of buffers, modes, and keymaps is useful for all kinds of games. As a bonus, you can use the text editing features of Polywell to code the game from within the game itself, which makes experimentation and reloading seamless.

My son and I sat down and knocked out an implementation of Connect 4 pretty quickly using Polywell, and I thought it would be interesting to step through how it works since it can serve as a very succinct explanation for how to use Polywell.

State and Drawing

local e = require("polywell")

local board = { {}, {}, {}, {}, {}, {}, {} }
local colors = {red={255,50,50},yellow={255,238,0}}
local turn = "yellow"

We start out by loading "polywell" and putting it in the e local (e for editor). Most of the game state is in the board table[1], which has an empty table for each column in it. The Connect 4 board has seven columns in which pieces can be dropped. It's a bit unusual, but we represent columns as lists of pieces from the bottom up, because the tokens are subject to gravity and fall to the bottom of the column they're placed in. Finally we set up colors which maps each player's color name to an RGB triplet and store the final bit of state (the current turn) in the turn local. So far so good!

local draw = function()
   for col=1,7 do
      for n,color in ipairs(board[col]) do
         local x,y = col * 75, 800 - n*75[color])"fill", x, y, 30)

Our draw function is very natural once you understand the unusual structure of the board; we simply loop over each column with an inner loop over each piece in the column. The piece is represented by n, its numeric position within the list of pieces, and its color. We calculate x and y from the col and n respectively and draw a colored circle for each piece from the bottom of the column upwards. This is basically the only place we use the LÖVE framework directly.

Modes and Bindings

e.define_mode("connect4", nil, {draw=draw, read_only=true})

Using Polywell's define_mode function we create a "connect4" mode which will contain all the key bindings for the game. Modes in Polywell are assumed to be textual unless otherwise specified, but since our game is graphical we pass nil as the second argument because our mode does not inherit from any existing mode. For our third argument, we pass in our previously-defined draw function as the mode's draw property, overriding the default draw which simply displays the current mode's text. We also mark it as read_only to avoid accidentally inserting any text into the buffer.

e.bind("connect4", "escape", function() e.change_buffer("*console*") end)
e.bind("connect4", "backspace", function()
          for i=1,7 do lume.clear(board[i]) end

Polywell's bind function allows us to attach a function to be called when a specific keystroke is pressed in a specific mode. In this case we say that escape will switch back to the Lua console while backspace will just clear each column in the board.

for key=1,7 do
   e.bind("connect4", tostring(key), function()
             local column = board[key]
             if(#column >= 6) then return end
             table.insert(column, turn)
             turn = turn == "red" and "yellow" or "red"

Almost done! Here's where the meat of the game is. We loop from 1 to 7, which is the number of columns in the game. For each column, we bind that number key to a function which grabs the corresponding column table from the board. It checks to make sure the column isn't full (each one can only hold 6 pieces) and if not it inserts the color of the current player into the column with table.insert. Then it changes the turn to the next player., "*connect4*", "connect4")

Finally it uses the open function to create a new buffer named "*connect4*" with "connect4" mode active. The first argument is nil because this buffer is not attached to the filesystem; it's a free-floating thing that doesn't get loaded or saved. You could leave this line out and Polywell would simply boot to a Lua console where you could invoke connect4 mode manually from there.

And that's it! 27 lines is all it took, and me and my son were off to the races playing the game. While we were writing it I kept him involved by asking each step of the way what we should do next. Once I wrote the draw function we were able to test it out by editing the board table directly using Lua code in the console. Our first pass of the number key function simply called table.insert, so once we tried it out he was able to point out which features were still missing, and I could ask leading questions which helped him piece together roughly what was needed to address those things.

Of course there's a lot more that Polywell can do, but it doesn't take much code to get a simple game going. Try it for yourself; you might have a lot of fun.

[1] Lua tables can be a bit confusing since they're a single data structure that can act both sequentially (as with board here which is basically used as a vector/array) or associatively (as with colors which acts like a map). The thing to remember is that the sequential/associative property is not inherent in the table but rather part of how it's used.

« older | 2017-02-12T23:15:07Z | newer »