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)