Skip to content

Instantly share code, notes, and snippets.

@EvanHahn
Created August 10, 2011 01:46
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save EvanHahn/1135851 to your computer and use it in GitHub Desktop.
Save EvanHahn/1135851 to your computer and use it in GitHub Desktop.
Tic Tac Toe in Lua. Free license.
--[[
TIC-TAC-TOE
by Evan Hahn (http://www.evanhahn.com/)
This is a program that allows you to play tic-tac-toe against the
computer.
You may change the configuration (below) to make the board more than 3x3,
or play with "white" and "black" instead of "x" and "o", and change how
the board is displayed.
How it works:
The board is represented by a 2D table of spaces. They are filled with
nil to start. When you play, you put an "x" or an "o" into the table. The
board is used to keep track of piece locations and to display them. It
does not calculate wins.
The board also has regions. A region is a place where a player may win
(horizontal, vertical, or diagonal). It holds pointers to the board table.
Player 1 is represented by +, and Player 2 by -. Each piece in the region
increments or decrements the checking of the region. Basically, two X's
returns as 2. Two O's returns as -2. 3 or -3 is a winning region.
Some notes:
- There are other ways to program this, but I did not elect to use them.
One alternate way: Generate a table that holds string representations
of all the winning boards. Check for wins against that table instead.
- I am unfamiliar with accepted style and best practices of Lua, and
my code may reflect that.
--]]
----------------------------------------------
-- Configuration (change this if you wish!) --
----------------------------------------------
-- Are they playable by human or computer-controlled?
PLAYER_1_HUMAN = true
PLAYER_2_HUMAN = false
-- Board size
BOARD_RANK = 3 -- The board will be this in both dimensions.
-- Display stuff
PLAYER_1 = "x" -- Player 1 is represented by this. Player 1 goes first.
PLAYER_2 = "o" -- Player 2 is represented by this.
EMPTY_SPACE = " " -- An empty space is displayed like this.
DISPLAY_HORIZONTAL_SEPARATOR = "-" -- Horizontal lines look like this.
DISPLAY_VERTICAL_SEPARATOR = " | " -- Vertical lines look like this
--[[ ###################################################################
#### Don't mess with things below here unless you are brave ####
################################################################### --]]
------------------------
-- More configuration --
------------------------
MAX_BOARD_RANK = 100 -- Won't run above this number. Prevents crashes.
-------------------------------------------------------
-- Don't run if the board is larger than the maximum --
-------------------------------------------------------
if BOARD_RANK > MAX_BOARD_RANK then os.exit(0) end
-----------------------------
-- Create board (2D table) --
-----------------------------
space = {}
for i = 0, (BOARD_RANK - 1) do
space[i] = {}
for j = 0, (BOARD_RANK - 1) do
space[i][j] = nil -- start each space with nil
end
end
---------------------
-- Board functions --
---------------------
-- get the piece at a given spot
function getPiece(x, y)
return space[x][y]
end
-- get the piece at a given spot; if nil, return " "
-- this is useful for output.
function getPieceNoNil(x, y)
if getPiece(x, y) ~= nil then
return getPiece(x, y)
else
return EMPTY_SPACE
end
end
-- is that space empty?
function isEmpty(x, y)
if getPiece(x, y) == nil then
return true
else
return false
end
end
-- place a piece there, but make sure nothing is there already.
-- if you can't play there, return false.
function placePiece(x, y, piece)
if isEmpty(x, y) == true then
space[x][y] = piece
return true
else
return false
end
end
-- is the game over?
function isGameOver()
if checkWin() == false then -- if there is no win...
for i = 0, (BOARD_RANK - 1) do -- is the board empty?
for j = 0, (BOARD_RANK - 1) do
if isEmpty(i, j) == true then return false end
end
end
return true
else -- there is a win; the game is over
return true
end
end
-- create a string made up of a certain number of smaller strings
-- this is useful for the display.
function repeatString(to_repeat, amount)
if amount <= 0 then return "" end
local to_return = ""
for i = 1, amount do
to_return = to_return .. to_repeat
end
return to_return
end
-- display the board.
-- this uses the configuration file pretty much entirely.
function displayBoard()
-- find the widest player
local widest_piece = math.max(string.len(PLAYER_1), string.len(PLAYER_2), string.len(EMPTY_SPACE))
-- display board, top to bottom
io.write("\n") -- make sure it starts on a new line
for i = (BOARD_RANK - 1), 0, -1 do
local row = "" -- start with an empty row
for j = 0, (BOARD_RANK - 1) do -- generate that row
local piece = getPieceNoNil(j, i)
row = row .. piece
row = row .. repeatString(" ", widest_piece - string.len(piece))
if j ~= (BOARD_RANK - 1) then
row = row .. DISPLAY_VERTICAL_SEPARATOR
end
end
io.write(row) -- output row
if i ~= 0 then -- output horizontal line as long as the row
io.write("\n")
local repeats = math.ceil(string.len(row) / string.len(DISPLAY_HORIZONTAL_SEPARATOR))
io.write(repeatString(DISPLAY_HORIZONTAL_SEPARATOR, repeats))
io.write("\n")
end
end
-- finish off with a line break
io.write("\n")
end
-------------------------------------------------
-- Create regions (I admit this is a bit ugly) --
-------------------------------------------------
-- declare region and a number to increment
region = {}
region_number = 0
-- vertical
for i = 0, (BOARD_RANK - 1) do
region[region_number] = {}
for j = 0, (BOARD_RANK - 1) do
region[region_number][j] = {}
region[region_number][j]["x"] = i
region[region_number][j]["y"] = j
end
region_number = region_number + 1
end
-- horizontal
for i = 0, (BOARD_RANK - 1) do
region[region_number] = {}
for j = 0, (BOARD_RANK - 1) do
region[region_number][j] = {}
region[region_number][j]["x"] = j
region[region_number][j]["y"] = i
end
region_number = region_number + 1
end
-- diagonal, bottom-left to top-right
region[region_number] = {}
for i = 0, (BOARD_RANK - 1) do
region[region_number][i] = {}
region[region_number][i]["x"] = i
region[region_number][i]["y"] = i
end
region_number = region_number + 1
-- diagonal, top-left to bottom-right
region[region_number] = {}
for i = (BOARD_RANK - 1), 0, -1 do
region[region_number][i] = {}
region[region_number][i]["x"] = BOARD_RANK - i - 1
region[region_number][i]["y"] = i
end
region_number = region_number + 1
----------------------
-- Region functions --
----------------------
-- get a region
function getRegion(number)
return region[number]
end
-- check for a win in a particular region.
-- returns a number representation of the region. occurrences of player 1
-- add 1, occurrences of player 2 subtract 1. so if there are two X pieces,
-- it will return 2. one O will return -1.
function checkWinInRegion(number)
local to_return = 0
for i, v in pairs(getRegion(number)) do
local piece = getPiece(v["x"], v["y"])
if piece == PLAYER_1 then to_return = to_return + 1 end
if piece == PLAYER_2 then to_return = to_return - 1 end
end
return to_return
end
-- check for a win in every region.
-- returns false if no winner.
-- returns the winner if there is one.
function checkWin()
for i in pairs(region) do
local win = checkWinInRegion(i)
if math.abs(win) == BOARD_RANK then
if win == math.abs(win) then
return PLAYER_1
else
return PLAYER_2
end
end
end
return false
end
------------------
-- UI Functions --
------------------
-- human play
function humanPlay(piece)
io.write(piece .. ", here's the board:\n")
displayBoard()
local placed = false
while placed == false do -- loop until they play correctly
io.write("\nWhere would you like to play your " .. piece .. "?\n")
io.write("Give the X-coordinate (starting with 0). ")
local x = tonumber(io.read())
io.write("Now give the Y-coordinate (starting with 0). ")
local y = tonumber(io.read())
placed = placePiece(x, y, piece)
if placed == false then
io.write("I'm afraid you can't play there!")
end
end
displayBoard()
io.write("\n")
end
-- AI play
function AIPlay(piece)
-- am I negative or positive?
local me = 0
if piece == PLAYER_1 then me = 1 end
if piece == PLAYER_2 then me = -1 end
-- look for a region in which I can win
for i in pairs(region) do
local win = checkWinInRegion(i)
if win == ((BOARD_RANK - 1) * me) then
for j, v in pairs(getRegion(i)) do
if isEmpty(v["x"], v["y"]) == true then
placePiece(v["x"], v["y"], piece)
return
end
end
end
end
-- look for a region in which I can block
for i in pairs(region) do
local win = checkWinInRegion(i)
if win == ((BOARD_RANK - 1) * (me * -1)) then
for j, v in pairs(getRegion(i)) do
if isEmpty(v["x"], v["y"]) == true then
placePiece(v["x"], v["y"], piece)
return
end
end
end
end
-- play first empty space, if no better option
for i = 0, (BOARD_RANK - 1) do
for j = 0, (BOARD_RANK - 1) do
if placePiece(i, j, piece) ~= false then return end
end
end
end
----------
-- Main --
----------
-- welcome!
io.write("Welcome to Tic-Tac-Toe!\n\n")
-- play the game until someone wins
while true do
-- break if the game is won
if isGameOver() == true then break end
-- player 1
if PLAYER_1_HUMAN == true then humanPlay(PLAYER_1)
else AIPlay(PLAYER_1) end
-- break if the game is won
if isGameOver() == true then break end
-- player 2
if PLAYER_2_HUMAN == true then humanPlay(PLAYER_2)
else AIPlay(PLAYER_2) end
end
-- show the final board
io.write("The final board:\n")
displayBoard()
io.write("\n")
-- write who won, or if there is a tie
win = checkWin()
if win == false then
io.write("Tie game!\n")
else
io.write(win)
io.write(" wins!\n")
end
@jony-g06
Copy link

jony-g06 commented Oct 4, 2020

I can´t run this on my computer, it says that on line 281 a number was expected on argument 2, but got a string, can you please help?

@EvanHahn
Copy link
Author

EvanHahn commented Oct 8, 2020

I wrote this a long time ago and don't have time to help debug this, sadly. Perhaps I wrote this on a different version of Lua?

@mattwidmann
Copy link

Line 281? That's a bit odd. io.read acts like io.input:read() and shouldn't be returning a second return value. It returns either a string or nil. You can pull the values out into variables and print them to see what might be going on:

local line, extra = io.read()
print(line, extra)

The better way to do this would probably to use the n format for :read which just reads a whitespace-delimited number from the string and converts it automatically.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment