<- Back

Tic-Tac-Toe Machine

Dec. 24, 2015 - cheapie

This article assumes you have the mesecons, digilines, rgblightstone, and digibutton mods installed. It may still be possible to build this without some or all of these mods, but replacing them with fancy wiring is an exercise left to the reader.

That aside, this project is actually not too complex, and it is quite possible to complete it with only minimal prior experience. The reason for the high difficulty rating is more because the code on the Luacontroller (there's only one!) is rather complex, and it is strongly recommended to understand what is going on in there in order to get the most from this article.


So, what is this thing? Well, if you're here, odds are you've already seen some variant of Uberi's Tic-Tac-Toe machine. It's rather impressive, but I found that it's not only quite large, but the 31 Luacontrollers required made me think that there must be a better way to do it. The two main obstacles, however, were that every pixel on the display took a port on a Luacontroller (81 in total), and so did every button (10 of those).

To overcome these limitations, I figured that the best course of action would be to make digilines-connectable buttons and lightstone. That's exactly what digibutton and rgblightstone do. After making those, I took the opportunity to rewrite the Luacontroller program from scratch, adding many new features in the process such as crossing out the winning symbols and making the colors configurable.


Step 1: Display

Start with the backplane for the display. Don't let the name scare you - it's just an 11x11 square of digimese.

Now place a block of RGB Lightstone in the top left corner.

This one should be on channel "ttt", X address 1, and Y address 1.

Next, extend this all the way to the right, incrementing the X address each time. The far right one should be at X address 11.

Now place a row below it, and punch each one in turn. They should auto-fill with the same values as the one above them, but with the next Y address. Continue doing this one row at a time until the front of the digimese is covered. The bottom-right corner will have X and Y addresses of 11 if you did it right.


Step 2: Indicators

There's not really much to this step. Simply place 4 RGB Lightstones, and set them to the channels "xwin" (X wins), "owin" (O wins), "xturn" (X's turn), and "oturn" (O's turn). They should all be connected via digilines or similar to the display's digimese. While optional, a sign is recommended to remind you what they each do.


Step 3: Keypad

Moves will be entered on the keypad. Place 9 digilines buttons in a 3x3 grid on some digimese. Set the channel on all of them to "keypad", and the message to the number in the chart below corresponding to where they are on the grid.

11 21 31
12 22 32
13 23 33

Off to the side (or right next to them) place another digilines button on the channel "reset". Any message works for this one. This button will be used to start a new game.

Connect the digimese behind the keypad, as well as the reset button, to the display with digilines.


Step 4: Controller

This may very well be the simplest step of them all. First, place a Luacontroller anywhere along the digilines or digimese.

Then, program it with the following program:

--Tic-Tac-Toe Machine APU
--By Advanced Mesecons Devices, a division of Cheapie Systems
--WTFPL

if event.type == "program" then
--Settings
	mem.ledoffcolor = "darkgray"
	mem.xcolor = "darkgreen"
	mem.ocolor = "darkred"
	mem.winlinecolor = "darkgray"
	mem.darkxcolor = "darkblue"
	mem.darkocolor = "brown"
	mem.bgcolor = "white"
	mem.gridcolor = "gray"
	mem.darkgridcolor = "black"
end

function checkwin(b)
	if b[1][1] == "X" and b[2][1] == "X" and b[3][1] == "X" then return({win="X",dir=2,offset=2}) end
	if b[1][2] == "X" and b[2][2] == "X" and b[3][2] == "X" then return({win="X",dir=2,offset=6}) end
	if b[1][3] == "X" and b[2][3] == "X" and b[3][3] == "X" then return({win="X",dir=2,offset=10}) end
	if b[1][1] == "X" and b[1][2] == "X" and b[1][3] == "X" then return({win="X",dir=1,offset=2}) end
	if b[2][1] == "X" and b[2][2] == "X" and b[2][3] == "X" then return({win="X",dir=1,offset=6}) end
	if b[3][1] == "X" and b[3][2] == "X" and b[3][3] == "X" then return({win="X",dir=1,offset=10}) end
	if b[1][1] == "X" and b[2][2] == "X" and b[3][3] == "X" then return({win="X",dir=3,offset=nil}) end
	if b[3][1] == "X" and b[2][2] == "X" and b[1][3] == "X" then return({win="X",dir=4,offset=nil}) end

	if b[1][1] == "O" and b[2][1] == "O" and b[3][1] == "O" then return({win="O",dir=2,offset=2}) end
	if b[1][2] == "O" and b[2][2] == "O" and b[3][2] == "O" then return({win="O",dir=2,offset=6}) end
	if b[1][3] == "O" and b[2][3] == "O" and b[3][3] == "O" then return({win="O",dir=2,offset=10}) end
	if b[1][1] == "O" and b[1][2] == "O" and b[1][3] == "O" then return({win="O",dir=1,offset=2}) end
	if b[2][1] == "O" and b[2][2] == "O" and b[2][3] == "O" then return({win="O",dir=1,offset=6}) end
	if b[3][1] == "O" and b[3][2] == "O" and b[3][3] == "O" then return({win="O",dir=1,offset=10}) end
	if b[1][1] == "O" and b[2][2] == "O" and b[3][3] == "O" then return({win="O",dir=3,offset=nil}) end
	if b[3][1] == "O" and b[2][2] == "O" and b[1][3] == "O" then return({win="O",dir=4,offset=nil}) end

	return({win="none",dir=nil,offset=nil})
end

function coverpixel(y,x)
	if mem.buffer[y][x] == mem.bgcolor then
		mem.buffer[y][x] = mem.winlinecolor
	elseif mem.buffer[y][x] == mem.xcolor then
		mem.buffer[y][x] = mem.darkxcolor
	elseif mem.buffer[y][x] == mem.ocolor then
		mem.buffer[y][x] = mem.darkocolor
	elseif mem.buffer[y][x] == mem.gridcolor then
		mem.buffer[y][x] = mem.darkgridcolor
	end
end

function drawline(win)
	if win.dir==1 then
		coverpixel(win.offset,1)
		coverpixel(win.offset,2)
		coverpixel(win.offset,3)
		coverpixel(win.offset,4)
		coverpixel(win.offset,5)
		coverpixel(win.offset,6)
		coverpixel(win.offset,7)
		coverpixel(win.offset,8)
		coverpixel(win.offset,9)
		coverpixel(win.offset,10)
		coverpixel(win.offset,11)
	elseif win.dir==2 then
		coverpixel(1,win.offset)
		coverpixel(2,win.offset)
		coverpixel(3,win.offset)
		coverpixel(4,win.offset)
		coverpixel(5,win.offset)
		coverpixel(6,win.offset)
		coverpixel(7,win.offset)
		coverpixel(8,win.offset)
		coverpixel(9,win.offset)
		coverpixel(10,win.offset)
		coverpixel(11,win.offset)
	elseif win.dir==3 then
		coverpixel(1,1)
		coverpixel(2,2)
		coverpixel(3,3)
		coverpixel(4,4)
		coverpixel(5,5)
		coverpixel(6,6)
		coverpixel(7,7)
		coverpixel(8,8)
		coverpixel(9,9)
		coverpixel(10,10)
		coverpixel(11,11)
	elseif win.dir==4 then
		coverpixel(1,11)
		coverpixel(2,10)
		coverpixel(3,9)
		coverpixel(4,8)
		coverpixel(5,7)
		coverpixel(6,6)
		coverpixel(7,5)
		coverpixel(8,4)
		coverpixel(9,3)
		coverpixel(10,2)
		coverpixel(11,1)
	end
	digiline_send("ttt",mem.buffer)
end


function rendersymbol(sym)
if sym.val == "X" then
mem.buffer[sym.yp][sym.xp] = mem.xcolor
mem.buffer[sym.yp][sym.xp+1] = mem.bgcolor
mem.buffer[sym.yp][sym.xp+2] = mem.xcolor
mem.buffer[sym.yp+1][sym.xp] = mem.bgcolor
mem.buffer[sym.yp+1][sym.xp+1] = mem.xcolor
mem.buffer[sym.yp+1][sym.xp+2] = mem.bgcolor
mem.buffer[sym.yp+2][sym.xp] = mem.xcolor
mem.buffer[sym.yp+2][sym.xp+1] = mem.bgcolor
mem.buffer[sym.yp+2][sym.xp+2] = mem.xcolor
elseif sym.val == "O" then
mem.buffer[sym.yp][sym.xp] = mem.bgcolor
mem.buffer[sym.yp][sym.xp+1] = mem.ocolor
mem.buffer[sym.yp][sym.xp+2] = mem.bgcolor
mem.buffer[sym.yp+1][sym.xp] = mem.ocolor
mem.buffer[sym.yp+1][sym.xp+1] = mem.bgcolor
mem.buffer[sym.yp+1][sym.xp+2] = mem.ocolor
mem.buffer[sym.yp+2][sym.xp] = mem.bgcolor
mem.buffer[sym.yp+2][sym.xp+1] = mem.ocolor
mem.buffer[sym.yp+2][sym.xp+2] = mem.bgcolor
else
mem.buffer[sym.yp][sym.xp] = mem.bgcolor
mem.buffer[sym.yp][sym.xp+1] = mem.bgcolor
mem.buffer[sym.yp][sym.xp+2] = mem.bgcolor
mem.buffer[sym.yp+1][sym.xp] = mem.bgcolor
mem.buffer[sym.yp+1][sym.xp+1] = mem.bgcolor
mem.buffer[sym.yp+1][sym.xp+2] = mem.bgcolor
mem.buffer[sym.yp+2][sym.xp] = mem.bgcolor
mem.buffer[sym.yp+2][sym.xp+1] = mem.bgcolor
mem.buffer[sym.yp+2][sym.xp+2] = mem.bgcolor
end
digiline_send("ttt",mem.buffer)
end

if event.type=="program" or (event.type=="digiline" and event.channel=="reset") then
	mem.board = {{"blank","blank","blank"},{"blank","blank","blank"},{"blank","blank","blank"}}
	mem.win={win="none",dir=nil,offset=nil}
	mem.turn=false
	port.a=mem.turn
	digiline_send("xwin",mem.ledoffcolor)
	digiline_send("owin",mem.ledoffcolor)
	digiline_send("xturn",mem.xcolor)
	digiline_send("oturn",mem.ledoffcolor)
	mem.buffer = {
	{mem.bgcolor,mem.bgcolor,mem.bgcolor,mem.gridcolor,mem.bgcolor,mem.bgcolor,mem.bgcolor,mem.gridcolor,mem.bgcolor,mem.bgcolor,mem.bgcolor},
	{mem.bgcolor,mem.bgcolor,mem.bgcolor,mem.gridcolor,mem.bgcolor,mem.bgcolor,mem.bgcolor,mem.gridcolor,mem.bgcolor,mem.bgcolor,mem.bgcolor},
	{mem.bgcolor,mem.bgcolor,mem.bgcolor,mem.gridcolor,mem.bgcolor,mem.bgcolor,mem.bgcolor,mem.gridcolor,mem.bgcolor,mem.bgcolor,mem.bgcolor},
	{mem.gridcolor,mem.gridcolor,mem.gridcolor,mem.gridcolor,mem.gridcolor,mem.gridcolor,mem.gridcolor,mem.gridcolor,mem.gridcolor,mem.gridcolor,mem.gridcolor},
	{mem.bgcolor,mem.bgcolor,mem.bgcolor,mem.gridcolor,mem.bgcolor,mem.bgcolor,mem.bgcolor,mem.gridcolor,mem.bgcolor,mem.bgcolor,mem.bgcolor},
	{mem.bgcolor,mem.bgcolor,mem.bgcolor,mem.gridcolor,mem.bgcolor,mem.bgcolor,mem.bgcolor,mem.gridcolor,mem.bgcolor,mem.bgcolor,mem.bgcolor},
	{mem.bgcolor,mem.bgcolor,mem.bgcolor,mem.gridcolor,mem.bgcolor,mem.bgcolor,mem.bgcolor,mem.gridcolor,mem.bgcolor,mem.bgcolor,mem.bgcolor},
	{mem.gridcolor,mem.gridcolor,mem.gridcolor,mem.gridcolor,mem.gridcolor,mem.gridcolor,mem.gridcolor,mem.gridcolor,mem.gridcolor,mem.gridcolor,mem.gridcolor},
	{mem.bgcolor,mem.bgcolor,mem.bgcolor,mem.gridcolor,mem.bgcolor,mem.bgcolor,mem.bgcolor,mem.gridcolor,mem.bgcolor,mem.bgcolor,mem.bgcolor},
	{mem.bgcolor,mem.bgcolor,mem.bgcolor,mem.gridcolor,mem.bgcolor,mem.bgcolor,mem.bgcolor,mem.gridcolor,mem.bgcolor,mem.bgcolor,mem.bgcolor},
	{mem.bgcolor,mem.bgcolor,mem.bgcolor,mem.gridcolor,mem.bgcolor,mem.bgcolor,mem.bgcolor,mem.gridcolor,mem.bgcolor,mem.bgcolor,mem.bgcolor}
	}
	digiline_send("ttt",mem.buffer)
end

if event.type=="digiline" and event.channel=="keypad" and mem.win.win == "none" then
	x = tonumber(string.sub(event.msg,1,1))
	y = tonumber(string.sub(event.msg,2,2))
	if mem.board[y][x] == "blank" then
		mem.board[y][x] = mem.turn and "O" or "X"
		temptable = {}
		temptable.xp = 1+((x-1)*4)
		temptable.yp = 1+((y-1)*4)
		temptable.val = mem.board[y][x]
		rendersymbol(temptable)
		mem.win = checkwin(mem.board)
		if mem.win.win == "X" then
			digiline_send("xwin",mem.xcolor)
			drawline(mem.win)
		elseif mem.win.win == "O" then
			digiline_send("owin",mem.ocolor)
			drawline(mem.win)
		else
			mem.turn = not mem.turn
			if mem.turn then
				digiline_send("oturn",mem.ocolor)
				digiline_send("xturn",mem.ledoffcolor)
			else
				digiline_send("xturn",mem.xcolor)
				digiline_send("oturn",mem.ledoffcolor)
			end
		end
	end
end

Assuming you did everything right (let's hope so!) the machine should spring to life and be ready to play as soon as you press "Execute".

Now all that remains to be done is for the circuitry to be moved closer together (to make it more compact) and for a case to be made for it, and you're all done!