191 lines
4.6 KiB
Lua
191 lines
4.6 KiB
Lua
#!/usr/bin/env luajit
|
||
ffi=require"ffi"
|
||
local function printf(fmt,...) io.write(string.format(fmt, ...)) end
|
||
local board="123456789" -- board
|
||
local pval={1, -1} -- player 1=1 2=-1 for negamax
|
||
local pnum={} for k,v in ipairs(pval) do pnum[v]=k end
|
||
local symbol={'X','O'} -- default symbols X and O
|
||
local isymbol={} for k,v in pairs(symbol) do isymbol[v]=pval[k] end
|
||
math.randomseed(os.time()^5*os.clock()) -- time-seed the random gen
|
||
local random=math.random
|
||
-- usage of ffi variables give 20% speed
|
||
ffi.cdef[[
|
||
typedef struct{
|
||
char value;
|
||
char flag;
|
||
int depth;
|
||
}cData;
|
||
]]
|
||
-- draw the "board" in the way the numpad is organized
|
||
local function draw(board)
|
||
for i=7,1,-3 do
|
||
print(board:sub(i,i+2))
|
||
end
|
||
end
|
||
-- pattern for win situations
|
||
local check={"(.)...%1...%1","..(.).%1.%1..",
|
||
"(.)%1%1......","...(.)%1%1...","......(.)%1%1",
|
||
"(.)..%1..%1..",".(.)..%1..%1.","..(.)..%1..%1",
|
||
}
|
||
-- calculate a win situation for which player or draw
|
||
local function win(b)
|
||
local sub
|
||
for i=1,#check do
|
||
sub=b:match(check[i])
|
||
if sub then break end
|
||
end
|
||
sub=isymbol[sub]
|
||
return sub or 0
|
||
end
|
||
-- input only validate moves of not yet filled positions
|
||
local function input(b,player)
|
||
char=symbol[pnum[player]]
|
||
local inp
|
||
repeat
|
||
printf("Player %d (\"%s\") move: ",pnum[player],char)
|
||
inp=tonumber(io.read()) or 0
|
||
until inp>=1 and inp<=9 and b:find(inp)
|
||
b=b:gsub(inp,char)
|
||
return b,inp
|
||
end
|
||
-- ask how many human or AI players
|
||
local function playerselect()
|
||
local ai={}
|
||
local yn
|
||
for i=1,2 do
|
||
repeat
|
||
printf("Player %d human (Y/n)? ", i) yn=io.read():lower()
|
||
until yn:match("[yn]") or yn==''
|
||
if yn=='n' then
|
||
ai[pval[i]]=true
|
||
printf("Player %d is AI\n", i)
|
||
else
|
||
printf("Player %d is human\n", i)
|
||
end
|
||
end
|
||
return ai
|
||
end
|
||
local function endgame()
|
||
repeat
|
||
printf("\nEnd game? (y/n)? ", i) yn=io.read():lower()
|
||
until yn:match("[yn]")
|
||
if yn=='n' then
|
||
return false
|
||
else
|
||
printf("\nGOOD BYE PROFESSOR FALKEN.\n\nA STRANGE GAME.\nTHE ONLY WINNING MOVE IS\nNOT TO PLAY.\n\nHOW ABOUT A NICE GAME OF CHESS?\n")
|
||
return true
|
||
end
|
||
end
|
||
-- AI Routine
|
||
local function shuffle(t)
|
||
for i=#t,1,-1 do
|
||
local rnd=random(i)
|
||
t[i], t[rnd] = t[rnd], t[i]
|
||
end
|
||
return t
|
||
end
|
||
-- move generator
|
||
local function genmove(node, color)
|
||
return coroutine.wrap(function()
|
||
local moves={}
|
||
for m in node:gmatch("%d") do
|
||
moves[#moves+1]=m
|
||
end
|
||
shuffle(moves) -- to make it more interesting
|
||
for _,m in ipairs(moves) do
|
||
local child=node:gsub(m,symbol[pnum[color]])
|
||
coroutine.yield(child, m)
|
||
end
|
||
end)
|
||
end
|
||
--[[
|
||
Negamax with alpha-beta pruning and table caching
|
||
]]
|
||
local cache={}
|
||
local best, aimove, tDepth
|
||
local LOWERB,EXACT,UPPERB=-1,0,1 -- has somebody an idea how to make them real constants?
|
||
local function negamax(node, depth, color, α, β)
|
||
color=color or 1
|
||
α=α or -math.huge
|
||
β=β or math.huge
|
||
-- check for cached node
|
||
local αOrg=α
|
||
local cData=cache[node]
|
||
if cData and cData.depth>=depth and depth~=tDepth then
|
||
if cData.flag==EXACT then
|
||
return cData.value
|
||
elseif cData.flag==LOWERB then
|
||
α=math.max(α,cData.value)
|
||
elseif cData.flag==UPPERB then
|
||
β=math.min(β,cData.value)
|
||
end
|
||
if α>=β then
|
||
return cData.value
|
||
end
|
||
end
|
||
|
||
local winner=win(node)
|
||
if depth==0 or winner~=0 then
|
||
return winner*color
|
||
end
|
||
local value=-math.huge
|
||
for child,move in genmove(node, color) do
|
||
value=math.max(value, -negamax(child, depth-1, -color, -β, -α))
|
||
if value>α then
|
||
α=value
|
||
if depth==tDepth then
|
||
best=child
|
||
aimove=move
|
||
end
|
||
end
|
||
if α>=β then break end
|
||
end
|
||
-- cache known data
|
||
--cData={} -- if you want Lua tables instead of ffi you can switch the two lines here, costs 20% speed
|
||
cData=ffi.new("cData")
|
||
cData.value=value
|
||
if value<=αOrg then
|
||
cData.flag=UPPERB
|
||
elseif value>=β then
|
||
cData.flag=LOWERB
|
||
else
|
||
cData.flag=EXACT
|
||
end
|
||
cData.depth=depth
|
||
cache[node]=cData
|
||
return α
|
||
end
|
||
-- MAIN
|
||
do
|
||
local winner,value
|
||
local score={[-1]=0, [0]=0, [1]=0}
|
||
repeat
|
||
print("\n TIC-TAC-TOE\n")
|
||
local aiplayer=playerselect()
|
||
local player=1
|
||
board="123456789"
|
||
for i=1,#board do
|
||
draw(board)
|
||
tDepth=10-i
|
||
if aiplayer[player] then
|
||
negamax(board, tDepth, player, -math.huge, math.huge)
|
||
board=best
|
||
printf("AI %d moves %s\n", pnum[player], aimove)
|
||
else
|
||
board=input(board,player)
|
||
end
|
||
winner=win(board)
|
||
if winner~=0 then break end
|
||
player=-player
|
||
end
|
||
score[winner]=score[winner]+1
|
||
if winner and winner~=0 then
|
||
printf("*** Player %d (%s) has won\n", pnum[winner], symbol[pnum[winner]])
|
||
else
|
||
printf("*** No winner\n")
|
||
end
|
||
printf("Score Player 1: %d Player 2: %d Draw: %d\n",score[1],score[-1],score[0])
|
||
draw(board)
|
||
until endgame()
|
||
end
|