Scite Tic Tac Toe

lua-users home
wiki

An improved tic tac toe game. This script acts like a self-contained Lua "application" in SciTE; it opens a new buffer to play the game and does not affect other buffers. It hooks to handlers in a well-behaved manner, and is compatible with extman (SciteExtMan). Finally, it demonstrates a simple application that interacts by having the user double-click on "buttons" or press certain keys. The buffer window is marked read-only so that display refreshes can be better controlled. Coded and tested on SciTE 1.71. Now obsolete old version can be found [here].

Note: If you use proportional fonts, grab SciteMakeMonospace and then add MakeMonospace() at the end of the initialization function, TicTacToe().


Sample output:


SciTE Tic Tac Toe

-----------------

Status: No more moves to make, draw



+---+---+---+

| O | O | X | SciTE: O

+---+---+---+ Human: X

| X | X | O |

+---+---+---+

| O | X | X |

+---+---+---+



+----------+----------+

| New Game | Autoplay |

+----------+----------+



For best results, please use a monospace font (press Ctrl+F11 for

monospace font mode.) Double-click boxes to play, or press keys

1 through 9 to make a move. Key positions correspond to the usual

keypad arrangement. For a new game, you can press the N key or

double-click the "NewGame" box. To autoplay, you can press [Space]

or double-click the "Autoplay" box.


-----------------------------------------------------------------------

-- Tic Tac Toe for SciTE Version 2.2

-- Kein-Hong Man <khman@users.sf.net> 20060905

-- This program is hereby placed into PUBLIC DOMAIN

-----------------------------------------------------------------------

-- This script can be installed to a shortcut using properties:

--     command.name.8.*=Tic Tac Toe

--     command.subsystem.8.*=3

--     command.8.*=TicTacToe

--     command.save.before.8.*=2

-- If you use extman, you can do it in Lua like this:

--     scite_Command('Tic Tac Toe|TicTacToe|Ctrl+8')

-----------------------------------------------------------------------

-- * This is a demonstration of a (hopefully) well-behaved Lua-based

--   "application" in SciTE that hooks to handlers, is compatible

--   with extman, and uses mouse doubleclicks as the user interface.

-- * TicTacToe is the main function. It opens a new buffer and the

--   game is played by double-clicking on boxed areas, or by pressing

--   the number keys 1 through 9.

-- * Note that the computer player and the player human are fixed

--   at 'O' and 'X', respectively.

-- * If you play using digit keys, do not change buffer from read-only.

-----------------------------------------------------------------------



------------------------------------------------------------------------

-- constants and primitives

------------------------------------------------------------------------

local string = string

local O, X = 1, 10

local STR = {                           -- various strings

  Sig = "SciTE_TicTacToe2",

  Prompt = ">SciTE_TicTacToe2: ",

  Horiz = "+---+---+---+",

  HorizRegex = "^%+%-%-%-%+%-%-%-%+%-%-%-%+",

  TrioRegex = "^|%s*(%w*)%s*|%s*(%w*)%s*|%s*(%w*)%s*|",

  BoardPat = "12121",

  ToolBar = [[

+----------+----------+

| New Game | Autoplay |

+----------+----------+

]],

}

local MSG = {                           -- game messages

  Title = "SciTE Tic Tac Toe",

  Conflict = "There is an OnDoubleClick conflict, please use extman",

  BadBoard = "Board not recognized, computer cannot continue",

  BadPieces = "Something strange on the board, cannot continue",

  IllegalMove = "Illegal move",

  Borked = "Evaluator borked",

  Key1 = "SciTE: O",

  Key2 = "Human: X",

  Start1 = "Human starts",

  Start2 = "Computer starts",

  AlreadyEnd = "Game has already ended",

  NoMoves = "No more moves to make, draw",

  HumanWin = "Human wins this round",

  ComputerWin = "Computer wins this round",

  Help = [[

For best results, please use a monospace font (press Ctrl+F11 for

monospace font mode.) Double-click boxes to play, or press keys

1 through 9 to make a move. Key positions correspond to the usual

keypad arrangement. For a new game, you can press the N key or

double-click the "NewGame" box. To autoplay, you can press [Space]

or double-click the "Autoplay" box.

]]

}

local BUT = {                           -- fixed button set

  [5] = {{2,4,"7"},{6,8,"8"},{10,12,"9"},},

  [7] = {{2,4,"4"},{6,8,"5"},{10,12,"6"},},

  [9] = {{2,4,"1"},{6,8,"2"},{10,12,"3"},},

  [13] = {{2,11,"NewGame"},{13,22,"Autoplay"},},

}

local function Error(msg) _ALERT(STR.Prompt..msg) end       -- error msg



------------------------------------------------------------------------

-- simple check for extman, partially emulate if okay to do so

------------------------------------------------------------------------

if (OnDoubleClick or OnChar) and not scite_Command then

  Error(MSG.Conflict)

else

  -- simple way to add a handler only, can't remove like extman does

  if not scite_OnDoubleClick then

    local _OnDC

    scite_OnDoubleClick = function(f) _OnDC = f end

    OnDoubleClick = function(c) if _OnDC then return _OnDC(c) end end

  end

  if not scite_OnChar then

    local _OnCh

    scite_OnChar = function(f) _OnCh = f end

    OnChar = function(c) if _OnCh then return _OnCh(c) end end

  end

end



------------------------------------------------------------------------

-- tic tac toe functions (implicitly uses O as computer, X as human)

------------------------------------------------------------------------



local function CheckForWin(t, player)   -- see who wins

  local wins = player * 3

  if t[1]+t[5]+t[9] == wins or

     t[3]+t[5]+t[7] == wins then return true end

  for i = 1,3 do

    local j = i * 3

    if t[i]+t[i+3]+t[i+6] == wins or

       t[j-2]+t[j-1]+t[j] == wins then return true end

  end

  return false

end



local function AnyWin(t)                -- see if somebody won

  return CheckForWin(t, X) or CheckForWin(t, O)

end



local function MoveCount(t)             -- counts the number of moves

  local n = 0

  for i = 1, 9 do if t[i] == O or t[i] == X then n = n + 1 end end

  return n

end



-- not-bad movement evaluator (minimax can be easily made perfect)

-- (1) picks the obvious

-- (2) blocks the obvious

-- (3) otherwise pick randomly

local function MoveSimple(t, player)

  local mv, opponent

  if player == X then opponent = O else opponent = X end

  for i = 1, 9 do -- (1)

    if t[i] == 0 then

      t[i] = player

      if CheckForWin(t, player) then t[i] = player return end

      t[i] = 0

    end

  end

  for i = 1, 9 do -- (2)

    if t[i] == 0 then

      t[i] = opponent

      if CheckForWin(t, opponent) then t[i] = player return end

      t[i] = 0

    end

  end

  if MoveCount(t) == 9 then Error(MSG.Borked) return end

  repeat mv = math.random(1, 9) until t[mv] == 0 -- (3)

  t[mv] = player

end

local Evaluate = MoveSimple             -- select evaluator



local function ComputerStart(t)         -- computer may start

  if math.random(1, 10) > 5 then

    t[math.random(1, 9)] = O

    return MSG.Start2

  end

  return MSG.Start1

end



------------------------------------------------------------------------

-- redraws the screen (complete redraw for simplicity)

------------------------------------------------------------------------

local function DrawBoard(t)

  if not t then t = {} end

  local p = function(i)

    if not t[i] then return "   "

    elseif t[i] == O then return " O "

    elseif t[i] == X then return " X "

    else return "   "

    end

  end

  editor:AddText(

    STR.Horiz.."\n"..

    "|"..p(7).."|"..p(8).."|"..p(9).."| "..MSG.Key1.."\n"..

    STR.Horiz.." "..MSG.Key2.."\n"..

    "|"..p(4).."|"..p(5).."|"..p(6).."|\n"..

    STR.Horiz.."\n"..

    "|"..p(1).."|"..p(2).."|"..p(3).."|\n"..

    STR.Horiz.."\n"

  )

end



local function Refresh(t, msg)

  local function Underline(s) return string.rep("-", string.len(s)) end

  msg = msg or ""

  editor.ReadOnly = false

  editor:ClearAll()

  editor:AddText(MSG.Title.."\n"..Underline(MSG.Title).."\n")

  editor:AddText("Status: "..msg.."\n\n")

  DrawBoard(t)

  editor:AddText("\n"..STR.ToolBar.."\n"..MSG.Help)

  editor.ReadOnly = true

end



------------------------------------------------------------------------

-- main routine, processes double-clicks

------------------------------------------------------------------------

local function TicTacClick(ch)

  local BEG = 4                         -- first line of board

  if not buffer[STR.Sig] then return end-- verify buffer signature

  --------------------------------------------------------------------

  -- check appearance of board

  --------------------------------------------------------------------

  local tln = editor:GetLine(0) or ""   -- verify title signature

  if string.sub(tln, 1, string.len(MSG.Title)) ~= MSG.Title then

    Error(MSG.BadBoard) return true

  end

  local LineType = function(ln)         -- classify TTT line

    local text = editor:GetLine(ln)

    if text == nil then return 0

    elseif string.find(text, STR.HorizRegex) then return 1

    elseif string.find(text, STR.TrioRegex) then return 2

    else return 0 end

  end

  local id = ""                         -- verify board pattern

  for i = BEG, BEG+4 do id = id..tostring(LineType(i)) end

  if id ~= STR.BoardPat then Error(MSG.BadBoard) return true end

  --------------------------------------------------------------------

  -- extract board information

  --------------------------------------------------------------------

  local IsXOrO = function(c)            -- classify pieces

    if c == nil or c == "" then return 0

    elseif string.upper(c) == "O" then return O

    elseif string.upper(c) == "X" then return X

    else return -1

    end

  end

  local GetData = function(ln)          -- extract data from a line

    local text = editor:GetLine(ln)

    local _, _, c1, c2, c3 = string.find(text, STR.TrioRegex)

    return IsXOrO(c1), IsXOrO(c2), IsXOrO(c3)

  end

  local t = {}                          -- convert pieces to data

  t[7], t[8], t[9] = GetData(BEG+1)

  t[4], t[5], t[6] = GetData(BEG+3)

  t[1], t[2], t[3] = GetData(BEG+5)

  local delta = 0

  for i = 1,9 do                        -- verify board contents

    if t[i] == -1 then Error(MSG.BadPieces) return true

    elseif t[i] == O then delta = delta - 1

    elseif t[i] == X then delta = delta + 1

    end

  end

  if math.abs(delta) > 1 then Error(MSG.BadPieces) return true end

  --------------------------------------------------------------------

  -- decode user-clicked position or keypresses

  --------------------------------------------------------------------

  if ch == "click" then                 -- mouse double-click event

    local pos = editor.CurrentPos

    local ln = editor:LineFromPosition(pos)

    local col = editor.Column[pos]

    local bln = editor:GetLine(ln) or ""

    tln, id = BUT[ln], nil              -- check for button click

    if not tln then return end

    for i,b in ipairs(tln) do

      if col >= b[1] and col <= b[2] then id = b[3] end

    end

    if not id then return true end      -- nothing happen if no button

  else                                  -- keypress event

    id = string.find("123456789 nN", ch, 1, 1)

    if not id then return true end

    if id == 10 then id = "Autoplay"

    elseif id >= 11 then id = "NewGame"

    end

  end

  --------------------------------------------------------------------

  -- interactive game logic, takes id and t as state inputs

  --------------------------------------------------------------------

  local msg

  if id == "NewGame" then                               -- new game

    t = {}

    msg = ComputerStart(t)

  elseif AnyWin(t) then msg = MSG.AlreadyEnd            -- already won

  elseif MoveCount(t) == 9 then msg = MSG.NoMoves       -- draw

  else

    if id == "Autoplay" then                            -- auto play

      Evaluate(t, X)

    else                                                -- human play

      id = id+0

      if t[id] ~= 0 then Refresh(t, MSG.IllegalMove) return true end

      t[id] = X

    end

    if CheckForWin(t, X) then msg = MSG.HumanWin        -- human moves

    elseif MoveCount(t) == 9 then msg = MSG.NoMoves

    else

      Evaluate(t, O)                                    -- computer moves

      if CheckForWin(t, O) then msg = MSG.ComputerWin

      elseif MoveCount(t) == 9 then msg = MSG.NoMoves

      end

    end

  end

  Refresh(t, msg)                                       -- redraw screen

  return true

end



-- handle incoming events

local function HandleClick() return TicTacClick("click") end

local function HandleChar(c) return TicTacClick(c) end



------------------------------------------------------------------------

-- game initialization (opens a new file and set up handlers)

------------------------------------------------------------------------

function TicTacToe()

  scite_OnDoubleClick(HandleClick)

  scite_OnChar(HandleChar)

  scite.Open("")

  buffer[STR.Sig] = true;

  local t = {}

  Refresh(t, ComputerStart(t))

end



-- end of script


RecentChanges · preferences
edit · history
Last edited November 15, 2012 6:50 am GMT (diff)