Logo for Tick5 Game

What's new
About
Download
License
Screenshots
Manual
Tutorial
sf.net page
Contact


SourceForge.net Logo

Image of Lua Logo

Image for wxWidgets Logo

1. Start goofing around

"No more goofy script!" you may shout.

Sure, we will eventually build a real thing.
However, building an AI player is by no means a one-shot event; we'll add components one by one. Like the old Chinese saying says, "A long journey starts with a single step," our program, first and foremost, must be able to make legal moves, so that we can add and test components without being kicked off by the application.

I
nstead of goofing around, there are alternatives, of course. For example, the program just scans the board from the upper left corner to the bottom right, and makes a move on the first empty spot encountered. However, the disadvantage of this approach is that we have to pay attention to its moves when we test the program by playing against it; otherwise, it would simply form a 5-in-a-row at its fifth move and the game wouldn't run any further.

In order to better serve us, our program preferably makes random moves before it can make real moves. You will see, we do need the program to goof around for a while, until it can make meaningful moves. As you already know, there is an example script, goofy2.wz, in Tick5 User's Manual, which makes legal and random moves. Why don't we start from it? 

The goal in this section is to refactor goofy2.wz by introducing in an AI class. As we add more components to the program, this class will take the role to coordinate other components to make moves; but it just makes random moves for now.

Let's open goofy2.wz with a text editor and save it as, say, boora-0.1.wz. In case that you may wonder what does boora mean, it is from two of my daughter's favorite cartoon figures, Dora and Boots.

Let's add a
do
-end block into the file. (see Listing 1-1)

function tick5think( board )
math.randomseed( os.time() )
local dim = board:dimension()
local bound = dim - 1

local x = math.random( 0, bound )
local y = math.random( 0, bound )

while not board:isempty( x, y ) do
x = math.random( 0, bound )
y = math.random( 0, bound )
end

return x, y
end

-----------------------
-- AI class
-----------------------
do
-- a method that makes a move
local function makemove( ai, board )
end

-- a table that lists methods of AI class
local methodtbl = { makemove = makemove }

-- a table that lists a meta method
local metatbl = { __index = methodtbl }

-- a method that creates an instance of AI class,
-- like a ctor or a factory method
function newAI()
return setmetatable( {}, metatbl )
end
end

Listing 1-1

Throughout this tutorial, we will use do-end blocks to define classes. The advantages are: 1) it creates a local scope for each class to avoid name collisions; 2) it makes easy to hide the details of a class, for example, private member functions and private data members.

In the 
do-end block, there's a local function makemove().  It is empty now, but we're going to fill it up in a minute. Then, comes a table named methodtbl, which has only one field that references to function makemove().  Follows another table metatble, which is a metatable and contains a metamethod __index that references to methodtbl. In the end is the global function newAI() which acts as a constructor or a factory method. It creates an empty table in this case and associates it with the metatable.

This
do-end block implements a class which has one public member function makemove(). An instance of the class can be created by calling newAI(), a move can be generated by calling makemove(), while all details are hidden in the do-end block.

When the function gets called up on an AI instance, for example
AI:makemove(board)
, the metamethod __index in the metatable is used to search for key makemove. In this implementation, metamethod __index is method table methodtbl, so methodtbl is searched for key makemove. methodtbl does have a field named makemove which references the local function makemove(), therefore, makemove() gets called, when AI:makemove(board) is called.

Let's cut all the lines in tick5think() and paste them into makemove(). Function tick5think() just creates an AI instance and call makemove()to get a move. (see Listing 1-2)

function tick5think( board )
AI = newAI()
return AI:makemove( board )
end

-----------------------
-- AI class
-----------------------
do
-- a method that makes a move
local function makemove( ai, board )
math.randomseed( os.time() )
local dim = board:dimension()
local bound = dim - 1

local x = math.random( 0, bound )
local y = math.random( 0, bound )

while not board:isempty( x, y ) do
x = math.random( 0, bound )
y = math.random( 0, bound )
end

return x, y
end

-- a table that lists methods of AI class
local methodtbl = { makemove = makemove }

-- a table that lists a meta method
local metatbl = { __index = methodtbl }

-- a method that creates an instance of AI class,
-- like a ctor or a factory method
function newAI()
return setmetatable( {}, metatbl )
end
end

Listing 1-2

Play against the program. That is, we play one color of the stones and the program plays the other. It works just as expected.  

Before we move on to the next section, let's clean up somehow to keep the code tidy.

Function tick5think() now is self-illustrative and looks much better. Function  makemove() has a redundant variable dim. Let's remove the line and modify the other that follows. (see Listing 1-3)

   local function makemove( ai, board )
math.randomseed( os.time() )
local dim = board:dimension()
local bound = dim board:dimension() - 1

local x = math.random( 0, bound )
local y = math.random( 0, bound )

while not board:isempty( x, y ) do
x = math.random( 0, bound )
y = math.random( 0, bound )
end

return x, y
end

Listing 1-3

Have another test: let the program play against itself.