Hi Corona Community,
I’ve been working on a chess app which employs a standard chess clock. To give you a very crude idea of the app…
a. There is a button in the middle that, when clicked, prompts the computer to generate a move (the computer plays both White and Black).
b. The time of the white and black players are displayed at the bottom and top of the screen, respectively.
c. The is no chess board interface. Instead, the board state is printed in the terminal after each move is made.
d. The app uses timer.performWithDelay to subtract 1 second from the initially allocated time for each player every second when it’s that player’s turn.
Everything seems to work fine…except for one problem: when the computer is thinking, timer.performWithDelay decides to just take a break and stops working! That is, while the computer is calculating its next move, the timer is not ticking and the remaining time stays constant.
This behavior is surprising to me, since the app uses completely separate and independent logic and code for the chess AI and the timer, or chess clock.
Obviously, this is not the behavior I want, since a chess clock that doesn’t tick while the player is thinking sort of defeats the whole purpose having a chess clock in the first place.
How can I modify the code so that the timer properly “ticks”, even when the computer is thinking? Any advice is much appreciated.
Thanks in advance!
KC
--=========================== User Interface ===========================-- local clock local whiteTime, blackTime local player1TimeText, player2TimeText local activePlayerColor = "white" function toggle() if (activePlayerColor == "white") then activePlayerColor = "black" return else activePlayerColor = "white" end end function initClock() player1Time, blackTime = 10\*60, 10\*60 player1TimeText = display.newText("White Time : 10:00", display.contentCenterX, 1000, native.systemFont, 60) player2TimeText = display.newText("Black Time : 10:00", display.contentCenterX, 200, native.systemFont, 60) clock = timer.performWithDelay(1000, updateTime, 0) end function updateTime() if (activePlayerColor == "white") then if (player1Time \> 0) then player1Time = player1Time - 1 local minutes = math.floor(player1Time/60) local seconds = player1Time % 60 player1TimeText.text = string.format("White Time : %02d:%02d", minutes, seconds) else timer.cancel(clock) end else if (blackTime \> 0) then blackTime = blackTime - 1 local minutes = math.floor(blackTime/60) local seconds = blackTime % 60 player2TimeText.text = string.format("Black TIme : %02d:%02d", minutes, seconds) else timer.cancel(clock) end end end --============================== Fields ================================-- local Position = {} local A1, A8, H8 = 91, 21, 28 local TABLE\_SIZE = 1e6 local NODES\_SEARCHED = 1e4 local MATE\_VALUE = 30000 local pos local N, E, S, W = -10, 1, 10, -1 local directions = { P = {N, 2\*N, N+W, N+E}, N = {2\*N+E, N+2\*E, S+2\*E, 2\*S+E, 2\*S+W, S+2\*W, N+2\*W, 2\*N+W}, B = {N+E, S+E, S+W, N+W}, R = {N, E, S, W}, Q = {N, E, S, W, N+E, S+E, S+W, N+W}, K = {N, E, S, W, N+E, S+E, S+W, N+W} } local initial = ' \n' .. ' \n' .. ' rnbqkbnr\n' .. ' pppppppp\n' .. ' ........\n' .. ' ........\n' .. ' ........\n' .. ' ........\n' .. ' PPPPPPPP\n' .. ' RNBQKBNR\n' .. ' \n' .. ' ' --===================== Move and evaluation tables ======================-- local pst = { P = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 198, 198, 198, 198, 198, 198, 198, 198, 0, 0, 178, 198, 198, 198, 198, 198, 198, 178, 0, 0, 178, 198, 198, 198, 198, 198, 198, 178, 0, 0, 178, 198, 208, 218, 218, 208, 198, 178, 0, 0, 178, 198, 218, 238, 238, 218, 198, 178, 0, 0, 178, 198, 208, 218, 218, 208, 198, 178, 0, 0, 178, 198, 198, 198, 198, 198, 198, 178, 0, 0, 198, 198, 198, 198, 198, 198, 198, 198, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, B = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 797, 824, 817, 808, 808, 817, 824, 797, 0, 0, 814, 841, 834, 825, 825, 834, 841, 814, 0, 0, 818, 845, 838, 829, 829, 838, 845, 818, 0, 0, 824, 851, 844, 835, 835, 844, 851, 824, 0, 0, 827, 854, 847, 838, 838, 847, 854, 827, 0, 0, 826, 853, 846, 837, 837, 846, 853, 826, 0, 0, 817, 844, 837, 828, 828, 837, 844, 817, 0, 0, 792, 819, 812, 803, 803, 812, 819, 792, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, N = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 627, 762, 786, 798, 798, 786, 762, 627, 0, 0, 763, 798, 822, 834, 834, 822, 798, 763, 0, 0, 817, 852, 876, 888, 888, 876, 852, 817, 0, 0, 797, 832, 856, 868, 868, 856, 832, 797, 0, 0, 799, 834, 858, 870, 870, 858, 834, 799, 0, 0, 758, 793, 817, 829, 829, 817, 793, 758, 0, 0, 739, 774, 798, 810, 810, 798, 774, 739, 0, 0, 683, 718, 742, 754, 754, 742, 718, 683, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, R = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1258, 1263, 1268, 1272, 1272, 1268, 1263, 1258, 0, 0, 1258, 1263, 1268, 1272, 1272, 1268, 1263, 1258, 0, 0, 1258, 1263, 1268, 1272, 1272, 1268, 1263, 1258, 0, 0, 1258, 1263, 1268, 1272, 1272, 1268, 1263, 1258, 0, 0, 1258, 1263, 1268, 1272, 1272, 1268, 1263, 1258, 0, 0, 1258, 1263, 1268, 1272, 1272, 1268, 1263, 1258, 0, 0, 1258, 1263, 1268, 1272, 1272, 1268, 1263, 1258, 0, 0, 1258, 1263, 1268, 1272, 1272, 1268, 1263, 1258, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, Q = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 0, 0, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 0, 0, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 0, 0, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 0, 0, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 0, 0, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 0, 0, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 0, 0, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 2529, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, K = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 60098, 60132, 60073, 60025, 60025, 60073, 60132, 60098, 0, 0, 60119, 60153, 60094, 60046, 60046, 60094, 60153, 60119, 0, 0, 60146, 60180, 60121, 60073, 60073, 60121, 60180, 60146, 0, 0, 60173, 60207, 60148, 60100, 60100, 60148, 60207, 60173, 0, 0, 60196, 60230, 60171, 60123, 60123, 60171, 60230, 60196, 0, 0, 60224, 60258, 60199, 60151, 60151, 60199, 60258, 60224, 0, 0, 60287, 60321, 60262, 60214, 60214, 60262, 60321, 60287, 0, 0, 60298, 60332, 60273, 60225, 60225, 60273, 60332, 60298, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } --=========================== Helper methods ===========================-- local function isupper(s) local special = '. \n' if special:find(s) then return false end return s == s:upper() end local function islower(s) local special = '. \n' if special:find(s) then return false end return s == s:lower() end --====================== Position related methods ======================-- function Position.new(board, score, ep) local self = {} self.board = board self.score = score self.ep = ep for k,v in pairs(Position) do self[k] = v end return self end function Position:genMoves() local moves = {} for i = 1, #self.board do local p = self.board:sub(i, i) if isupper(p) and directions[p] then for \_, d in ipairs(directions[p]) do local limit = (i + d) + (10000) \* d for j = i + d - 1, limit, d do local q = self.board:sub(j + 1, j + 1) if q == ' ' or q == '\n' then break end if isupper(q) then break end if (p == 'P') then if ((d == N + W or d == N + E) and q == '.' and j ~= self.ep) then break end if ((d == N or d == 2\*N) and q ~= '.') then break end if (d == 2\*N and (i \< A1 + N or self.board:sub(i + N, i + N) ~= '.')) then break end end table.insert(moves, {i - 1, j}) if p == 'P' or p == 'N' or p == 'K' then break end if islower(q) then break end end end end end return moves end function Position:rotate() local function swapcase(s) local s2 = '' for i = 1, #s do local c = s:sub(i, i) if islower(c) then s2 = s2 .. c:upper() else s2 = s2 .. c:lower() end end return s2 end return self.new(swapcase(self.board:reverse()), -self.score, 119 - self.ep) end function Position:move(move) assert(move) local i, j = move[1], move[2] local p, q = self.board:sub(i + 1, i + 1), self.board:sub(j + 1, j + 1) local function put(board, i, p) return board:sub(1, i - 1) .. p .. board:sub(i + 1) end local board = self.board local ep = 0 local score = self.score + self:value(move) board = put(board, j + 1, board:sub(i + 1, i + 1)) board = put(board, i + 1, '.') if p == 'P' then if A8 \<= j and j \<= H8 then board = put(board, j + 1, 'Q') end if j - i == 2\*N then ep = i + N end if ((j - i) == N + W or (j - i) == N + E) and q == '.' then board = put(board, j + S + 1, '.') end end return self.new(board, score, ep):rotate() end function Position:value(move) local i, j = move[1], move[2] local p, q = self.board:sub(i + 1, i + 1), self.board:sub(j + 1, j + 1) local score = pst[p][j + 1] - pst[p][i + 1] if islower(q) then score = score + pst[q:upper()][j + 1] end if p == 'P' then if A8 \<= j and j \<= H8 then score = score + pst['Q'][j + 1] - pst['P'][j + 1] end if j == self.ep then score = score + pst['P'][j + S + 1] end end return score end --================= Transposition table related methods ================-- local tp = {} local tp\_index = {} local tp\_count = 0 local function tp\_set(pos, val) local hash = pos.board..';'..pos.score..';true;true;true;true;'..pos.ep..';' tp[hash] = val tp\_index[#tp\_index + 1] = hash tp\_count = tp\_count + 1 end local function tp\_get(pos) local hash = pos.board..';'..pos.score..';true;true;true;true;'..pos.ep..';' return tp[hash] end --=========================== Search Logic ============================-- local nodes = 0 local function bound(pos, gamma, depth) nodes = nodes + 1 local entry = tp\_get(pos) if entry and entry.depth \>= depth and ( entry.score \< entry.gamma and entry.score \< gamma or entry.score \>= entry.gamma and entry.score \>= gamma) then return entry.score end local nullscore = depth \> 0 and -bound(pos:rotate(), 1 - gamma, depth - 3) or pos.score if nullscore \>= gamma then return nullscore end local best, bmove = -3\*MATE\_VALUE, nil local moves = pos:genMoves() local function sorter(a, b) local va = pos:value(a) local vb = pos:value(b) if va ~= vb then return va \> vb else if a[1] == b[1] then return a[2] \> b[2] else return a[1] \< b[1] end end end table.sort(moves, sorter) for \_,move in ipairs(moves) do if depth \<= 0 and pos:value(move) \< 150 then break end local score = -bound(pos:move(move), 1 - gamma, depth - 1) if score \> best then best = score bmove = move end if score \>= gamma then break end end if depth \<= 0 and best \< nullscore then return nullscore end if depth \> 0 and (best \<= -MATE\_VALUE) and nullscore \> -MATE\_VALUE then best = 0 end if entry == nil or depth \>= entry.depth and best \>= gamma then tp\_set(pos, {depth = depth, score = best, gamma = gamma, move = bmove}) if tp\_count \> TABLE\_SIZE then tp[tp\_index[#tp\_index]] = nil tp\_index[#tp\_index] = nil tp\_count = tp\_count - 1 end end return best end local function search(pos) nodes = 0 local score for depth = 1, 98 do local lower, upper = -3\*MATE\_VALUE, 3\*MATE\_VALUE while lower \< upper - 3 do local gamma = math.floor((lower + upper + 1)/2) score = bound(pos, gamma, depth) if score \>= gamma then lower = score end if score \< gamma then upper = score end end if nodes \>= NODES\_SEARCHED then break end end local entry = tp\_get(pos) if entry ~= nil then return entry.move end return nil end --====================== Graphical Representation =======================-- local function parse(c) if not c then return nil end local p, v = c:sub(1,1), c:sub(2,2) if not (p and v and tonumber(v)) then return nil end local file, rank = string.byte(p) - string.byte('a'), tonumber(v) - 1 return A1 + file - 10\*rank end local function render(i) local rank, file = math.floor((i - A1) / 10), (i - A1) % 10 return string.char(file + string.byte('a')) .. tostring(-rank + 1) end local function printboard(board) local function strsplit(a) local out = {} while true do local pos, \_ = a:find('\n') if pos then out[#out + 1] = a:sub(1, pos - 1) a = a:sub(pos+1) else out[#out + 1] = a break end end return out end local l = strsplit(board, '\n') for k,v in ipairs(l) do for i = 1,#v do io.write(v:sub(i, i)) io.write(' ') end io.write('\n') end end function generateMove() if (not pos) then pos = Position.new(initial, 0, 0) end local optimalMove = search(pos) pos = pos:move(optimalMove) printboard(pos.board) toggle() end local moveButton = display.newRoundedRect(display.contentCenterX, display.contentCenterY, 400, 200, 30) moveButton:setFillColor(0, 0, 1) moveButton:addEventListener("tap", generateMove) local msg = display.newText("Generate Move!", display.contentCenterX, display.contentCenterY, native.systemFont, 50) initClock() printboard(Position.new(initial, 0, 0).board)