----------------------------------------------------------------------- -- Tic Tac Toe for SciTE -- Kein-Hong Man 20040727 -- This program is hereby placed into PUBLIC DOMAIN ----------------------------------------------------------------------- -- Best installed as a shortcut key, e.g. Ctrl-1 with the following in -- your user properties file, usually SciTEUser.properties: -- command.name.1.*=Tic Tac Toe -- command.subsystem.1.*=3 -- command.1.*=SciTE_TicTacToe -- command.save.before.1.*=2 -- This Lua function itself should be in SciTEStartup.lua, assuming it -- is properly set up. Consult SciTEDoc.html if you are unsure of what -- to do. You can also keep it in a separate file and load it using: -- require(props["SciteUserHome"].."/SciTE_TicTacToe.lua") -- Tested on SciTE 1.61+ (CVS 20040718) on Win98SE. Be careful when -- tweaking it, you *may* be able to crash SciTE. You have been warned! ----------------------------------------------------------------------- -- To start, run the shortcut once. Note that the computer player and -- the player human are fixed at 'O' and 'X', respectively. Use F8 to -- show the output window to see messages. If no boards are present, -- the computer creates one, otherwise it tries to move. You move by -- putting in an 'x' or 'X' into the appropriate location. Extra spaces -- are ignored. After a game ends, the board is deleted so that a new -- game can be restarted when the shortcut is pressed the next time. -- By default, the computer autoplays if you do not make a move. To -- take back a move, use the usual undo shortcut (Ctrl-Z). Game -- settings can be customized by editing the script. -- (Never mind about minimax eval, this is meant to be beatable :-)) ----------------------------------------------------------------------- SciTE_TicTacToe = function() -- game settings here local verbose = true -- writes some messages to the output window local autoplay = true -- plays against itself, otherwise prompts you local autoreset = true -- clears board at the end of the game -- default constants and messages local O, X = 1, 10 local msg = { default1 = "SciTE: O", default2 = "Human: X", win1 = "I win", win2 = "You lose", lose1 = "I lose", lose2 = "You win", draw1 = "Stalemate!", draw2 = "Ha ha ha!", scite_win = "It appears that you have lost...\n", human_win = "I will get you the next time...\n", draw = "Next time, there will be no mercy...\n", new_board = "\nNo tic tac toe board found, creating a new one\n", delete_board = "Game completed, board deleted\n", unidentified = "Doesn't look like a tic tac toe board to me\n", inconsistent = "Something strange on the board, I can't proceed\n", corrupted = "That's not right, I'd rather not proceed\n", ask_user = "I believe it's your turn, mate\n", auto_play = "Here, take this!\n", computer = "There, take that!\n", } -- variables local t = {} local line = editor:LineFromPosition(editor.CurrentPos) local linemax = editor.LineCount - 1 local pstart = editor:PositionFromLine(line) local lmin, lmax = line, line -- say something in output window local telluser = function(msg) output:DocumentEnd(); output:AddText(msg) end -- EOL char bypass editor:NewLine, avoids indentation local EOLLookup = function() local eol = editor.EOLMode if eol == SC_EOL_CR then return "\r" elseif eol == SC_EOL_CRLF then return "\r\n" else return "\n" --SC_EOL_LF end end -- replace line local replace = function(ln, text) editor.TargetStart = editor:PositionFromLine(ln) editor.TargetEnd = editor.LineEndPosition[ln] editor:ReplaceTarget(text) end -- classify TTT line local TTTLineType = function(ln) local text, len = editor:GetLine(ln) if text == nil then return 0 elseif string.find(text, "^%s*%w*%s*|%s*%w*%s*|%s*%w*%s*") then return 1 elseif string.find(text, "^%s*%-%-%-%-%-%-%-%-%-%s*") then return 2 else return 0 end end -- classify pieces local IsXOrO = function(c) 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 -- extract data from a line local TTTData = function(ln) local text, len = editor:GetLine(ln) local mstart, mend, c1, c2, c3 = string.find(text, "^%s*(%w*)%s*|%s*(%w*)%s*|%s*(%w*)%s*") return IsXOrO(c1), IsXOrO(c2), IsXOrO(c3) end -- make pieces local p = function(i) if t[i] == O then return "O" elseif t[i] == X then return "X" else return " " end end -- see who wins local TTTWin = function(player) local wins = player * 3 if t[1] + t[5] + t[9] == wins then return true end if 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 then return true end if t[j-2] + t[j-1] + t[j] == wins then return true end end return false end -- update board in editor local refresh = function(msg1, msg2) if msg1 == nil then msg1, msg2 = msg["default1"], msg["default2"] end editor:GotoPos(pstart) editor:BeginUndoAction() replace(lmin , p(1).." | "..p(2).." | "..p(3)) replace(lmin+1, "--------- "..msg1) replace(lmin+2, p(4).." | "..p(5).." | "..p(6)) replace(lmin+3, "--------- "..msg2) replace(lmin+4, p(7).." | "..p(8).." | "..p(9)) editor:EndUndoAction() end -- if nothing found, start a new board by inserting lines if not (TTTLineType(line) > 0) then if verbose then telluser(msg["new_board"]) output:DocumentEnd() end editor:GotoPos(pstart) editor:BeginUndoAction() editor:ReplaceSel(string.rep(EOLLookup(), 5)) editor:GotoPos(pstart) refresh() editor:EndUndoAction() return end -- find bounds of board while (lmin > 0) and (TTTLineType(lmin - 1) > 0) do lmin = lmin - 1 end while (lmax < linemax) and (TTTLineType(lmax + 1) > 0) do lmax = lmax + 1 end -- identify as a board local id = "" for i = lmin,lmax do id = id .. tostring(TTTLineType(i)) end if id ~= "12121" then if verbose then telluser(msg["unidentified"]) end return end -- convert pieces to data t[1], t[2], t[3] = TTTData(lmin) t[4], t[5], t[6] = TTTData(lmin+2) t[7], t[8], t[9] = TTTData(lmax) -- look for pieces on the board and check consistency local diff, TotalE = 0, 0 for i = 1,9 do if t[i] == -1 then if verbose then telluser(msg["inconsistent"]) end return elseif t[i] == O then diff = diff - 1 elseif t[i] == X then diff = diff + 1 else TotalE = TotalE + 1 end end if math.abs(diff) > 1 then if verbose then telluser(msg["corrupted"]) end return end -- not-bad movement (minimax can be easily made too perfect) local move_random = function(player, opponent) local mv for i = 1,9 do -- picks the obvious if t[i] == 0 then t[i] = player if TTTWin(player) then t[i] = player return end t[i] = 0 end end for i = 1,9 do -- blocks the obvious if t[i] == 0 then t[i] = opponent if TTTWin(opponent) then t[i] = player return end t[i] = 0 end end repeat -- otherwise pick randomly mv = math.random(1,9) until t[mv] == 0 t[mv] = player end local move = move_random -- select evaluator -- test if outcome already reported local game_done = function() local text, len = editor:GetLine(lmin+1) if string.find(text, msg["win1"]) or string.find(text, msg["lose1"]) or string.find(text, msg["draw1"]) then return true end return false end -- test for win local win_report = function() if game_done() then -- normally don't repeat completion message if autoreset then -- delete the board local lstart = editor:PositionFromLine(lmin) local lend = editor:PositionFromLine(lmax+1) editor:GotoPos(lstart) editor.TargetStart = lstart editor.TargetEnd = lend editor:ReplaceTarget("") if verbose then telluser(msg["delete_board"]) end end return true end -- test for win, lose or draw if TTTWin(O) then refresh(msg["win1"], msg["win2"]) if verbose then telluser(msg["scite_win"]) end return true elseif TTTWin(X) then refresh(msg["lose1"], msg["lose2"]) if verbose then telluser(msg["human_win"]) end return true elseif TotalE == 0 then refresh(msg["draw1"], msg["draw2"]) if verbose then telluser(msg["draw"]) end return true end return false end -- determine who moves (doesn't do it exactly right...) if win_report() then return end if TotalE == 9 then -- computer starts (fallthrough) elseif diff == 0 then -- O==X -- ambiguous, computer move (fallthrough) elseif diff == 1 then -- X>O -- computer's turn (fallthrough) elseif diff == -1 then -- O>X -- user's turn if autoplay then move(X, O) refresh() if verbose then telluser(msg["auto_play"]) end else if verbose then telluser(msg["ask_user"]) end end return end -- computer moves move(O, X) if win_report() then return end refresh() if verbose then telluser(msg["computer"]) end end -- end of script