360 lines
12 KiB
Lua
360 lines
12 KiB
Lua
# .tlv file generated by https://github.com/akkartik/teliva
|
|
# You may edit it if you are careful; however, you may see cryptic errors if you
|
|
# violate Teliva's assumptions.
|
|
#
|
|
# .tlv files are representations of Teliva programs. Teliva programs consist of
|
|
# sequences of definitions. Each definition is a table of key/value pairs. Keys
|
|
# and values are both strings.
|
|
#
|
|
# Lines in .tlv files always follow exactly one of the following forms:
|
|
# - comment lines at the top of the file starting with '#' at column 0
|
|
# - beginnings of definitions starting with '- ' at column 0, followed by a
|
|
# key/value pair
|
|
# - key/value pairs consisting of ' ' at column 0, containing either a
|
|
# spaceless value on the same line, or a multi-line value
|
|
# - multiline values indented by more than 2 spaces, starting with a '>'
|
|
#
|
|
# If these constraints are violated, Teliva may unceremoniously crash. Please
|
|
# report bugs at http://akkartik.name/contact
|
|
- __teliva_timestamp: original
|
|
check:
|
|
>function check(x, msg)
|
|
> if x then
|
|
> Window:addch('.')
|
|
> else
|
|
> print('F - '..msg)
|
|
> print(' '..str(x)..' is false/nil')
|
|
> teliva_num_test_failures = teliva_num_test_failures + 1
|
|
> -- overlay first test failure on editors
|
|
> if teliva_first_failure == nil then
|
|
> teliva_first_failure = msg
|
|
> end
|
|
> end
|
|
>end
|
|
- __teliva_timestamp: original
|
|
check_eq:
|
|
>function check_eq(x, expected, msg)
|
|
> if eq(x, expected) then
|
|
> Window:addch('.')
|
|
> else
|
|
> print('F - '..msg)
|
|
> print(' expected '..str(expected)..' but got '..str(x))
|
|
> teliva_num_test_failures = teliva_num_test_failures + 1
|
|
> -- overlay first test failure on editors
|
|
> if teliva_first_failure == nil then
|
|
> teliva_first_failure = msg
|
|
> end
|
|
> end
|
|
>end
|
|
- __teliva_timestamp: original
|
|
eq:
|
|
>function eq(a, b)
|
|
> if type(a) ~= type(b) then return false end
|
|
> if type(a) == 'table' then
|
|
> if #a ~= #b then return false end
|
|
> for k, v in pairs(a) do
|
|
> if b[k] ~= v then
|
|
> return false
|
|
> end
|
|
> end
|
|
> for k, v in pairs(b) do
|
|
> if a[k] ~= v then
|
|
> return false
|
|
> end
|
|
> end
|
|
> return true
|
|
> end
|
|
> return a == b
|
|
>end
|
|
- __teliva_timestamp: original
|
|
str:
|
|
>-- smarter tostring
|
|
>-- slow; used only for debugging
|
|
>function str(x)
|
|
> if type(x) == 'table' then
|
|
> local result = ''
|
|
> result = result..#x..'{'
|
|
> for k, v in pairs(x) do
|
|
> result = result..str(k)..'='..str(v)..', '
|
|
> end
|
|
> result = result..'}'
|
|
> return result
|
|
> elseif type(x) == 'string' then
|
|
> return '"'..x..'"'
|
|
> end
|
|
> return tostring(x)
|
|
>end
|
|
- __teliva_timestamp: original
|
|
Window:
|
|
>Window = curses.stdscr()
|
|
>-- animation-based app
|
|
>Window:nodelay(true)
|
|
>lines, cols = Window:getmaxyx()
|
|
- __teliva_timestamp: original
|
|
current_game:
|
|
>current_game = {}
|
|
- __teliva_timestamp: original
|
|
piece_glyph:
|
|
>piece_glyph = {
|
|
> -- for legibility, white pieces also use unicode glyphs for black pieces
|
|
> -- we rely on colors to distinguish them
|
|
> K = 0x265a,
|
|
> Q = 0x265b,
|
|
> R = 0x265c,
|
|
> B = 0x265d,
|
|
> N = 0x265e,
|
|
> P = 0x265f,
|
|
> k = 0x265a,
|
|
> q = 0x265b,
|
|
> r = 0x265c,
|
|
> b = 0x265d,
|
|
> n = 0x265e,
|
|
> p = 0x265f,
|
|
>}
|
|
- __teliva_timestamp: original
|
|
top_player:
|
|
>function top_player(current_game)
|
|
> if current_game.players[1].color == "black" then
|
|
> return current_game.players[1]
|
|
> end
|
|
> return current_game.players[2]
|
|
>end
|
|
- __teliva_timestamp: original
|
|
bottom_player:
|
|
>function bottom_player(current_game)
|
|
> if current_game.players[1].color == "white" then
|
|
> return current_game.players[1]
|
|
> end
|
|
> return current_game.players[2]
|
|
>end
|
|
- __teliva_timestamp: original
|
|
render_player:
|
|
>function render_player(y, x, player)
|
|
> Window:mvaddstr(y, x, player.user.name)
|
|
> Window:addstr(" · ")
|
|
> Window:addstr(tostring(player.rating))
|
|
>end
|
|
- __teliva_timestamp: original
|
|
render_square:
|
|
>function render_square(current_game, rank, file, highlighted_squares)
|
|
> -- decide whether to highlight
|
|
> local hl = 0
|
|
> if (rank == highlighted_squares.from.rank and file == highlighted_squares.from.file)
|
|
> or (rank == highlighted_squares.to.rank and file == highlighted_squares.to.file) then
|
|
> hl = 4
|
|
> end
|
|
> if (rank+file)%2 == 1 then
|
|
> -- light square
|
|
> Window:attrset(curses.color_pair(1+hl))
|
|
> else
|
|
> -- dark square
|
|
> Window:attrset(curses.color_pair(3+hl))
|
|
> end
|
|
> Window:mvaddstr((8 - rank + 1)*3, file*5, " ")
|
|
> Window:mvaddstr((8 - rank + 1)*3+1, file*5, " ")
|
|
> Window:mvaddstr((8 - rank + 1)*3+2, file*5, " ")
|
|
> Window:attrset(curses.A_NORMAL)
|
|
>end
|
|
- __teliva_timestamp: original
|
|
render_fen_rank:
|
|
>function render_fen_rank(rank, fen_rank, highlighted_squares)
|
|
> local file = 1
|
|
> for x in fen_rank:gmatch(".") do
|
|
> if x:match("%d") then
|
|
> file = file + tonumber(x)
|
|
> else
|
|
> -- decide whether to highlight
|
|
> local hl = 0
|
|
> if (rank == highlighted_squares.from.rank and file == highlighted_squares.from.file)
|
|
> or (rank == highlighted_squares.to.rank and file == highlighted_squares.to.file) then
|
|
> hl = 4
|
|
> end
|
|
> if (rank+file)%2 == 1 then
|
|
> if x < 'Z' then
|
|
> -- white piece on light square
|
|
> Window:attrset(curses.color_pair(1+hl))
|
|
> else
|
|
> -- black piece on light square
|
|
> Window:attrset(curses.color_pair(2+hl))
|
|
> end
|
|
> else
|
|
> if x < 'Z' then
|
|
> -- white piece on dark square
|
|
> Window:attrset(curses.color_pair(3+hl))
|
|
> else
|
|
> -- black piece on dark square
|
|
> Window:attrset(curses.color_pair(4+hl))
|
|
> end
|
|
> end
|
|
> Window:mvaddstr((8 - rank + 1)*3+1, file*5+2, utf8(piece_glyph[x]))
|
|
> Window:attrset(curses.A_NORMAL)
|
|
> file = file + 1
|
|
> end
|
|
> end
|
|
>end
|
|
- __teliva_timestamp: original
|
|
render_time:
|
|
>function render_time(y, x, seconds)
|
|
> if seconds == nil then return end
|
|
> Window:mvaddstr(y, x, tostring(math.floor(seconds/60)))
|
|
> Window:addstr(string.format(":%02d", seconds%60))
|
|
>end
|
|
- __teliva_timestamp: original
|
|
render_board:
|
|
>function render_board(current_game)
|
|
>--? Window:mvaddstr(1, 50, dump(current_game.fen))
|
|
>--? Window:mvaddstr(6, 50, dump(current_game.previously_moved_squares))
|
|
> render_player(2, 5, top_player(current_game))
|
|
> render_time(2, 35, current_game.bc)
|
|
> for rank=8,1,-1 do
|
|
> for file=1,8 do
|
|
> render_square(current_game, rank, file, current_game.previously_moved_squares)
|
|
> end
|
|
> render_fen_rank(rank, current_game.fen_rank[8-rank+1], current_game.previously_moved_squares)
|
|
> end
|
|
> render_player(27, 5, bottom_player(current_game))
|
|
> render_time(27, 35, current_game.wc)
|
|
>end
|
|
- __teliva_timestamp: original
|
|
parse_lm:
|
|
>function parse_lm(move)
|
|
>--? Window:mvaddstr(4, 50, move)
|
|
> local file1 = string.byte(move:sub(1, 1)) - 96 -- 'a'-1
|
|
> local rank1 = string.byte(move:sub(2, 2)) - 48 -- '0'
|
|
> local file2 = string.byte(move:sub(3, 3)) - 96 -- 'a'-1
|
|
> local rank2 = string.byte(move:sub(4, 4)) - 48 -- '0'
|
|
>--? Window:mvaddstr(5, 50, dump({{rank1, file1}, {rank2, file2}}))
|
|
> return {from={rank=rank1, file=file1}, to={rank=rank2, file=file2}}
|
|
>end
|
|
- __teliva_timestamp: original
|
|
render:
|
|
>function render(chunk)
|
|
> local o = json.decode(chunk)
|
|
> if o.t == "featured" then
|
|
> current_game = o.d
|
|
>--? current_game.lm = "__"
|
|
> current_game.previously_moved_squares = {from={rank=0, file=0}, to={rank=0, file=0}} -- no highlight
|
|
> else
|
|
> current_game.fen = o.d.fen
|
|
> current_game.wc = o.d.wc
|
|
> current_game.bc = o.d.bc
|
|
>--? current_game.lm = o.d.lm
|
|
> current_game.previously_moved_squares = parse_lm(o.d.lm)
|
|
>--? Window:nodelay(false)
|
|
>--? Window:mvaddstr(3, 50, "paused")
|
|
> end
|
|
> current_game.fen_rank = split(current_game.fen, "%w+")
|
|
> render_board(current_game)
|
|
> Window:refresh()
|
|
>end
|
|
- __teliva_timestamp: original
|
|
init_colors:
|
|
>function init_colors()
|
|
> -- colors
|
|
> local light_piece = 1
|
|
> local dark_piece = 0
|
|
> local light_square = 252
|
|
> local dark_square = 242
|
|
> local light_last_moved_square = 229
|
|
> local dark_last_moved_square = 226
|
|
> -- initialize colors
|
|
> curses.init_pair(1, light_piece, light_square)
|
|
> curses.init_pair(2, dark_piece, light_square)
|
|
> curses.init_pair(3, light_piece, dark_square)
|
|
> curses.init_pair(4, dark_piece, dark_square)
|
|
> curses.init_pair(5, light_piece, light_last_moved_square)
|
|
> curses.init_pair(6, dark_piece, light_last_moved_square)
|
|
> curses.init_pair(7, light_piece, dark_last_moved_square)
|
|
> curses.init_pair(8, dark_piece, dark_last_moved_square)
|
|
>end
|
|
- __teliva_timestamp: original
|
|
main:
|
|
>function main()
|
|
> init_colors()
|
|
> local request = {
|
|
> url = "https://lichess.org/api/tv/feed",
|
|
> sink = function(chunk, err)
|
|
> if chunk then
|
|
> Window:clear()
|
|
> render(chunk)
|
|
> Window:getch()
|
|
> end
|
|
> return 1
|
|
> end,
|
|
> }
|
|
> http.request(request)
|
|
>end
|
|
- __teliva_timestamp: original
|
|
utf8:
|
|
>-- https://stackoverflow.com/questions/7983574/how-to-write-a-unicode-symbol-in-lua
|
|
>function utf8(decimal)
|
|
> local bytemarkers = { {0x7FF,192}, {0xFFFF,224}, {0x1FFFFF,240} }
|
|
> if decimal<128 then return string.char(decimal) end
|
|
> local charbytes = {}
|
|
> for bytes,vals in ipairs(bytemarkers) do
|
|
> if decimal<=vals[1] then
|
|
> for b=bytes+1,2,-1 do
|
|
> local mod = decimal%64
|
|
> decimal = (decimal-mod)/64
|
|
> charbytes[b] = string.char(128+mod)
|
|
> end
|
|
> charbytes[1] = string.char(vals[2]+decimal)
|
|
> break
|
|
> end
|
|
> end
|
|
> return table.concat(charbytes)
|
|
>end
|
|
- __teliva_timestamp: original
|
|
split:
|
|
>function split(s, pat)
|
|
> result = {}
|
|
> for x in s:gmatch(pat) do
|
|
> table.insert(result, x)
|
|
> end
|
|
> return result
|
|
>end
|
|
- __teliva_timestamp: original
|
|
dump:
|
|
>-- https://stackoverflow.com/questions/9168058/how-to-dump-a-table-to-console
|
|
>function dump(o)
|
|
> if type(o) == 'table' then
|
|
> local s = '{ '
|
|
> for k,v in pairs(o) do
|
|
> if type(k) ~= 'number' then k = '"'..k..'"' end
|
|
> s = s .. '['..k..'] = ' .. dump(v) .. ','
|
|
> end
|
|
> return s .. '} '
|
|
> else
|
|
> return tostring(o)
|
|
> end
|
|
>end
|
|
- __teliva_timestamp:
|
|
>Sat Feb 12 18:57:02 2022
|
|
__teliva_note:
|
|
>better UX when app isn't permitted to access the network
|
|
main:
|
|
>function main()
|
|
> init_colors()
|
|
> local request = {
|
|
> url = "https://lichess.org/api/tv/feed",
|
|
> sink = function(chunk, err)
|
|
> if chunk then
|
|
> Window:clear()
|
|
> -- main event loop is here
|
|
> render(chunk)
|
|
> Window:getch()
|
|
> end
|
|
> return 1
|
|
> end,
|
|
> }
|
|
> http.request(request)
|
|
> -- degenerate event loop just to show errors in sandboxing, etc.
|
|
> while true do Window:getch(); end
|
|
>end
|
|
- __teliva_timestamp:
|
|
>Thu Feb 17 20:00:23 2022
|
|
doc:blurb:
|
|
>A demo of Teliva's support for networking: watch the current live chess game from LiChessTV.
|
|
>
|
|
>Compare with https://lichess.org/tv on a browser.
|