use markdown syntax for images
[teliva.git] / life.tlv
blob8b3cc6741f5b09a91e53887cf16bd853c98eca5d
1 # .tlv file generated by https://github.com/akkartik/teliva
2 # You may edit it if you are careful; however, you may see cryptic errors if you
3 # violate Teliva's assumptions.
5 # .tlv files are representations of Teliva programs. Teliva programs consist of
6 # sequences of definitions. Each definition is a table of key/value pairs. Keys
7 # and values are both strings.
9 # Lines in .tlv files always follow exactly one of the following forms:
10 # - comment lines at the top of the file starting with '#' at column 0
11 # - beginnings of definitions starting with '- ' at column 0, followed by a
12 #   key/value pair
13 # - key/value pairs consisting of '  ' at column 0, containing either a
14 #   spaceless value on the same line, or a multi-line value
15 # - multiline values indented by more than 2 spaces, starting with a '>'
17 # If these constraints are violated, Teliva may unceremoniously crash. Please
18 # report bugs at http://akkartik.name/contact
19 - __teliva_timestamp: original
20   str_helpers:
21     >-- some string helpers from http://lua-users.org/wiki/StringIndexing
22     >
23     >-- index characters using []
24     >getmetatable('').__index = function(str,i)
25     >  if type(i) == 'number' then
26     >    return str:sub(i,i)
27     >  else
28     >    return string[i]
29     >  end
30     >end
31     >
32     >-- ranges using (), selected bytes using {}
33     >getmetatable('').__call = function(str,i,j)
34     >  if type(i)~='table' then
35     >    return str:sub(i,j)
36     >  else
37     >    local t={}
38     >    for k,v in ipairs(i) do
39     >      t[k]=str:sub(v,v)
40     >    end
41     >    return table.concat(t)
42     >  end
43     >end
44     >
45     >-- iterate over an ordered sequence
46     >function q(x)
47     >  if type(x) == 'string' then
48     >    return x:gmatch('.')
49     >  else
50     >    return ipairs(x)
51     >  end
52     >end
53     >
54     >-- insert within string
55     >function string.insert(str1, str2, pos)
56     >  return str1:sub(1,pos)..str2..str1:sub(pos+1)
57     >end
58     >
59     >function string.remove(s, pos)
60     >  return s:sub(1,pos-1)..s:sub(pos+1)
61     >end
62     >
63     >function string.pos(s, sub)
64     >  return string.find(s, sub, 1, true)  -- plain=true to disable regular expressions
65     >end
66     >
67     >-- TODO: backport utf-8 support from Lua 5.3
68 - __teliva_timestamp: original
69   debugy:
70     >debugy = 5
71 - __teliva_timestamp: original
72   dbg:
73     >-- helper for debug by print; overlay debug information towards the right
74     >-- reset debugy every time you refresh screen
75     >function dbg(window, s)
76     >  local oldy = 0
77     >  local oldx = 0
78     >  oldy, oldx = window:getyx()
79     >  window:mvaddstr(debugy, 60, s)
80     >  debugy = debugy+1
81     >  window:mvaddstr(oldy, oldx, '')
82     >end
83 - __teliva_timestamp: original
84   check:
85     >function check(x, msg)
86     >  if x then
87     >    Window:addch('.')
88     >  else
89     >    print('F - '..msg)
90     >    print('  '..str(x)..' is false/nil')
91     >    teliva_num_test_failures = teliva_num_test_failures + 1
92     >    -- overlay first test failure on editors
93     >    if teliva_first_failure == nil then
94     >      teliva_first_failure = msg
95     >    end
96     >  end
97     >end
98 - __teliva_timestamp: original
99   check_eq:
100     >function check_eq(x, expected, msg)
101     >  if eq(x, expected) then
102     >    Window:addch('.')
103     >  else
104     >    print('F - '..msg)
105     >    print('  expected '..str(expected)..' but got '..str(x))
106     >    teliva_num_test_failures = teliva_num_test_failures + 1
107     >    -- overlay first test failure on editors
108     >    if teliva_first_failure == nil then
109     >      teliva_first_failure = msg
110     >    end
111     >  end
112     >end
113 - __teliva_timestamp: original
114   eq:
115     >function eq(a, b)
116     >  if type(a) ~= type(b) then return false end
117     >  if type(a) == 'table' then
118     >    if #a ~= #b then return false end
119     >    for k, v in pairs(a) do
120     >      if b[k] ~= v then
121     >        return false
122     >      end
123     >    end
124     >    for k, v in pairs(b) do
125     >      if a[k] ~= v then
126     >        return false
127     >      end
128     >    end
129     >    return true
130     >  end
131     >  return a == b
132     >end
133 - __teliva_timestamp: original
134   str:
135     >-- smarter tostring
136     >-- slow; used only for debugging
137     >function str(x)
138     >  if type(x) == 'table' then
139     >    local result = ''
140     >    result = result..#x..'{'
141     >    for k, v in pairs(x) do
142     >      result = result..str(k)..'='..str(v)..', '
143     >    end
144     >    result = result..'}'
145     >    return result
146     >  elseif type(x) == 'string' then
147     >    return '"'..x..'"'
148     >  end
149     >  return tostring(x)
150     >end
151 - __teliva_timestamp: original
152   menu:
153     >-- To show app-specific hotkeys in the menu bar, add hotkey/command
154     >-- arrays of strings to the menu array.
155     >menu = {}
156 - __teliva_timestamp: original
157   window:
158     >-- constructor for fake screen and window
159     >-- call it like this:
160     >--   local w = window{
161     >--     kbd=kbd('abc'),
162     >--     scr=scr{h=5, w=4},
163     >--   }
164     >-- eventually it'll do everything a real ncurses window can
165     >function window(h)
166     >  h.__index = h
167     >  setmetatable(h, h)
168     >  h.__index = function(table, key)
169     >    return rawget(h, key)
170     >  end
171     >  h.attrset = function(self, x)
172     >    self.scr.attrs = x
173     >  end
174     >  h.attron = function(self, x)
175     >    -- currently same as attrset since Lua 5.1 doesn't have bitwise operators
176     >    -- doesn't support multiple attrs at once
177     >--    local old = self.scr.attrs
178     >--    self.scr.attrs = old|x
179     >    self.scr.attrs = x
180     >  end
181     >  h.attroff = function(self, x)
182     >    -- currently borked since Lua 5.1 doesn't have bitwise operators
183     >    -- doesn't support multiple attrs at once
184     >--    local old = self.scr.attrs
185     >--    self.scr.attrs = old & (~x)
186     >    self.scr.attrs = curses.A_NORMAL
187     >  end
188     >  h.getch = function(self)
189     >    local c = table.remove(h.kbd, 1)
190     >    if c == nil then return c end
191     >    return string.byte(c)  -- for verisimilitude with ncurses
192     >  end
193     >  h.addch = function(self, c)
194     >    local scr = self.scr
195     >    if c == '\n' then
196     >      scr.cursy = scr.cursy+1
197     >      scr.cursx = 0
198     >      return
199     >    end
200     >    if scr.cursy <= scr.h then
201     >      scr[scr.cursy][scr.cursx] = {data=c, attrs=scr.attrs}
202     >      scr.cursx = scr.cursx+1
203     >      if scr.cursx > scr.w then
204     >        scr.cursy = scr.cursy+1
205     >        scr.cursx = 1
206     >      end
207     >    end
208     >  end
209     >  h.addstr = function(self, s)
210     >    for i=1,s:len() do
211     >      self:addch(s[i])
212     >    end
213     >  end
214     >  h.mvaddch = function(self, y, x, c)
215     >    self.scr.cursy = y
216     >    self.scr.cursx = x
217     >    self:addch(c)
218     >  end
219     >  h.mvaddstr = function(self, y, x, s)
220     >    self.scr.cursy = y
221     >    self.scr.cursx = x
222     >    self:addstr(s)
223     >  end
224     >  h.clear = function(self)
225     >    clear_scr(self.scr)
226     >  end
227     >  h.refresh = function(self)
228     >    -- nothing
229     >  end
230     >  return h
231     >end
232 - __teliva_timestamp: original
233   kbd:
234     >function kbd(keys)
235     >  local result = {}
236     >  for i=1,keys:len() do
237     >    table.insert(result, keys[i])
238     >  end
239     >  return result
240     >end
241 - __teliva_timestamp: original
242   scr:
243     >function scr(props)
244     >  props.cursx = 1
245     >  props.cursy = 1
246     >  clear_scr(props)
247     >  return props
248     >end
249 - __teliva_timestamp: original
250   clear_scr:
251     >function clear_scr(props)
252     >  props.cursy = 1
253     >  props.cursx = 1
254     >  for y=1,props.h do
255     >    props[y] = {}
256     >    for x=1,props.w do
257     >      props[y][x] = {data=' ', attrs=curses.A_NORMAL}
258     >    end
259     >  end
260     >  return props
261     >end
262 - __teliva_timestamp: original
263   check_screen:
264     >function check_screen(window, contents, message)
265     >  local x, y = 1, 1
266     >  for i=1,contents:len() do
267     >    check_eq(window.scr[y][x].data, contents[i], message..'/'..y..','..x)
268     >    x = x+1
269     >    if x > window.scr.w then
270     >      y = y+1
271     >      x = 1
272     >    end
273     >  end
274     >end
275     >
276     >-- putting it all together, an example test of both keyboard and screen
277     >function test_check_screen()
278     >  local lines = {
279     >    c='123',
280     >    d='234',
281     >    a='345',
282     >    b='456',
283     >  }
284     >  local w = window{
285     >    kbd=kbd('abc'),
286     >    scr=scr{h=3, w=5},
287     >  }
288     >  local y = 1
289     >  while true do
290     >    local b = w:getch()
291     >    if b == nil then break end
292     >    w:mvaddstr(y, 1, lines[string.char(b)])
293     >    y = y+1
294     >  end
295     >  check_screen(w, '345  '..
296     >                  '456  '..
297     >                  '123  ',
298     >              'test_check_screen')
299     >end
300 - __teliva_timestamp: original
301   grid:
302     >-- main data structure
303     >grid = {}
304     >for i=1,lines*4 do
305     >  grid[i] = {}
306     >  for j=1,cols*2 do
307     >    grid[i][j] = 0
308     >  end
309     >end
310 - __teliva_timestamp: original
311   Window:
312     >Window = curses.stdscr()
313     >-- animation-based app
314     >Window:nodelay(true)
315     >curses.curs_set(0)
316     >lines, cols = Window:getmaxyx()
317 - __teliva_timestamp: original
318   grid_char:
319     >-- grab a 4x2 chunk of grid
320     >function grid_char(line, col)
321     >  result = {}
322     >  for l, row in ipairs({unpack(grid, (line-1)*4+1, line*4)}) do
323     >    result[l] = {unpack(row, (col-1)*2+1, col*2)}
324     >  end
325     >  return result
326     >end
327 - __teliva_timestamp: original
328   print_grid_char:
329     >function print_grid_char(window, x)
330     >  result = {}
331     >  for l, row in ipairs(x) do
332     >    for c, val in ipairs(row) do
333     >      window:mvaddstr(l, c, val)
334     >    end
335     >  end
336     >  return result
337     >end
338 - __teliva_timestamp: original
339   glyph:
340     >-- look up the braille pattern corresponding to a 4x2 chunk of grid
341     >-- https://en.wikipedia.org/wiki/Braille_Patterns
342     >-- not obviously programmatic because Unicode added 4x2 after 3x2
343     >glyph = {
344     >  0x2800, 0x2801, 0x2802, 0x2803, 0x2804, 0x2805, 0x2806, 0x2807,   0x2840, 0x2841, 0x2842, 0x2843, 0x2844, 0x2845, 0x2846, 0x2847,
345     >  0x2808, 0x2809, 0x280a, 0x280b, 0x280c, 0x280d, 0x280e, 0x280f,   0x2848, 0x2849, 0x284a, 0x284b, 0x284c, 0x284d, 0x284e, 0x284f,
346     >  0x2810, 0x2811, 0x2812, 0x2813, 0x2814, 0x2815, 0x2816, 0x2817,   0x2850, 0x2851, 0x2852, 0x2853, 0x2854, 0x2855, 0x2856, 0x2857,
347     >  0x2818, 0x2819, 0x281a, 0x281b, 0x281c, 0x281d, 0x281e, 0x281f,   0x2858, 0x2859, 0x285a, 0x285b, 0x285c, 0x285d, 0x285e, 0x285f,
348     >  0x2820, 0x2821, 0x2822, 0x2823, 0x2824, 0x2825, 0x2826, 0x2827,   0x2860, 0x2861, 0x2862, 0x2863, 0x2864, 0x2865, 0x2866, 0x2867,
349     >  0x2828, 0x2829, 0x282a, 0x282b, 0x282c, 0x282d, 0x282e, 0x282f,   0x2868, 0x2869, 0x286a, 0x286b, 0x286c, 0x286d, 0x286e, 0x286f,
350     >  0x2830, 0x2831, 0x2832, 0x2833, 0x2834, 0x2835, 0x2836, 0x2837,   0x2870, 0x2871, 0x2872, 0x2873, 0x2874, 0x2875, 0x2876, 0x2877,
351     >  0x2838, 0x2839, 0x283a, 0x283b, 0x283c, 0x283d, 0x283e, 0x283f,   0x2878, 0x2879, 0x287a, 0x287b, 0x287c, 0x287d, 0x287e, 0x287f,
352     >
353     >  0x2880, 0x2881, 0x2882, 0x2883, 0x2884, 0x2885, 0x2886, 0x2887,   0x28c0, 0x28c1, 0x28c2, 0x28c3, 0x28c4, 0x28c5, 0x28c6, 0x28c7,
354     >  0x2888, 0x2889, 0x288a, 0x288b, 0x288c, 0x288d, 0x288e, 0x288f,   0x28c8, 0x28c9, 0x28ca, 0x28cb, 0x28cc, 0x28cd, 0x28ce, 0x28cf,
355     >  0x2890, 0x2891, 0x2892, 0x2893, 0x2894, 0x2895, 0x2896, 0x2897,   0x28d0, 0x28d1, 0x28d2, 0x28d3, 0x28d4, 0x28d5, 0x28d6, 0x28d7,
356     >  0x2898, 0x2899, 0x289a, 0x289b, 0x289c, 0x289d, 0x289e, 0x289f,   0x28d8, 0x28d9, 0x28da, 0x28db, 0x28dc, 0x28dd, 0x28de, 0x28df,
357     >  0x28a0, 0x28a1, 0x28a2, 0x28a3, 0x28a4, 0x28a5, 0x28a6, 0x28a7,   0x28e0, 0x28e1, 0x28e2, 0x28e3, 0x28e4, 0x28e5, 0x28e6, 0x28e7,
358     >  0x28a8, 0x28a9, 0x28aa, 0x28ab, 0x28ac, 0x28ad, 0x28ae, 0x28af,   0x28e8, 0x28e9, 0x28ea, 0x28eb, 0x28ec, 0x28ed, 0x28ee, 0x28ef,
359     >  0x28b0, 0x28b1, 0x28b2, 0x28b3, 0x28b4, 0x28b5, 0x28b6, 0x28b7,   0x28f0, 0x28f1, 0x28f2, 0x28f3, 0x28f4, 0x28f5, 0x28f6, 0x28f7,
360     >  0x28b8, 0x28b9, 0x28ba, 0x28bb, 0x28bc, 0x28bd, 0x28be, 0x28bf,   0x28f8, 0x28f9, 0x28fa, 0x28fb, 0x28fc, 0x28fd, 0x28fe, 0x28ff,
361     >}
362 - __teliva_timestamp: original
363   utf8:
364     >-- https://stackoverflow.com/questions/7983574/how-to-write-a-unicode-symbol-in-lua
365     >function utf8(decimal)
366     >  local bytemarkers = { {0x7FF,192}, {0xFFFF,224}, {0x1FFFFF,240} }
367     >  if decimal<128 then return string.char(decimal) end
368     >  local charbytes = {}
369     >  for bytes,vals in ipairs(bytemarkers) do
370     >    if decimal<=vals[1] then
371     >      for b=bytes+1,2,-1 do
372     >        local mod = decimal%64
373     >        decimal = (decimal-mod)/64
374     >        charbytes[b] = string.char(128+mod)
375     >      end
376     >      charbytes[1] = string.char(vals[2]+decimal)
377     >      break
378     >    end
379     >  end
380     >  return table.concat(charbytes)
381     >end
382 - __teliva_timestamp: original
383   grid_char_to_glyph_index:
384     >-- convert a chunk of grid into a number
385     >function grid_char_to_glyph_index(g)
386     >  return g[1][1]    + g[2][1]*2  + g[3][1]*4  + g[4][1]*8 +
387     >         g[1][2]*16 + g[2][2]*32 + g[3][2]*64 + g[4][2]*128 +
388     >         1  -- 1-indexing
389     >end
390 - __teliva_timestamp: original
391   render:
392     >function render(window)
393     >  window:clear()
394     >  window:attron(curses.color_pair(1))
395     >  for line=1,lines do
396     >    for col=1,cols do
397     >      window:addstr(utf8(glyph[grid_char_to_glyph_index(grid_char(line, col))]))
398     >    end
399     >  end
400     >  window:attroff(curses.color_pair(1))
401     >  window:refresh()
402     >end
403 - __teliva_timestamp: original
404   state:
405     >function state(line, col)
406     >  if line < 1 or line > #grid or col < 1 or col > #grid[1] then
407     >    return 0
408     >  end
409     >  return grid[line][col]
410     >end
411 - __teliva_timestamp: original
412   num_live_neighbors:
413     >function num_live_neighbors(line, col)
414     >  return state(line-1, col-1) + state(line-1, col) + state(line-1, col+1) +
415     >         state(line,   col-1) +                      state(line,   col+1) +
416     >         state(line+1, col-1) + state(line+1, col) + state(line+1, col+1)
417     >end
418 - __teliva_timestamp: original
419   step:
420     >function step()
421     >  local new_grid = {}
422     >  for line=1,#grid do
423     >    new_grid[line] = {}
424     >    for col=1,#grid[1] do
425     >      local n = num_live_neighbors(line, col)
426     >      if n == 3 then
427     >        new_grid[line][col] = 1
428     >      elseif n == 2 then
429     >        new_grid[line][col] = grid[line][col]
430     >      else
431     >        new_grid[line][col] = 0
432     >      end
433     >    end
434     >  end
435     >  grid = new_grid
436     >end
437 - __teliva_timestamp: original
438   sleep:
439     >function sleep(a)
440     >    local sec = tonumber(os.clock() + a);
441     >    while (os.clock() < sec) do
442     >    end
443     >end
444 - __teliva_timestamp: original
445   load_file:
446     >function load_file(window, fs, filename)
447     >  local infile = start_reading(fs, filename)
448     >  if infile == nil then return end
449     >  local line_index = lines  -- quarter of the way down in pixels
450     >  while true do
451     >    local line = infile.read()
452     >    if line == nil then break end
453     >    if line:sub(1,1) ~= '!' then  -- comment; plaintext files can't have whitespace before comments
454     >      local col_index = cols
455     >      for c in line:gmatch(".") do
456     >        if c == '\r' then break end  -- DOS line ending
457     >        if c == '.' then
458     >          grid[line_index][col_index] = 0
459     >        else
460     >          grid[line_index][col_index] = 1
461     >        end
462     >        col_index = col_index+1
463     >      end
464     >      line_index = line_index+1
465     >    end
466     >  end
467     >end
468 - __teliva_timestamp: original
469   update:
470     >menu = {{"arrow", "pan"}}
471     >
472     >function update(window, c)
473     >  if c == curses.KEY_LEFT then
474     >    for i=1,lines*4 do
475     >      for j=2,cols*2 do
476     >        grid[i][j-1] = grid[i][j]
477     >      end
478     >      grid[i][cols*2] = 0
479     >    end
480     >  elseif c == curses.KEY_DOWN then
481     >    for i=lines*4-1,1,-1 do
482     >      for j=1,cols*2 do
483     >        grid[i+1][j] = grid[i][j]
484     >      end
485     >    end
486     >    for j=1,cols*2 do
487     >      grid[1][j] = 0
488     >    end
489     >  elseif c == curses.KEY_UP then
490     >    for i=2,lines*4 do
491     >      for j=1,cols*2 do
492     >        grid[i-1][j] = grid[i][j]
493     >      end
494     >    end
495     >    for j=1,cols*2 do
496     >      grid[lines*4][j] = 0
497     >    end
498     >  elseif c == curses.KEY_RIGHT then
499     >    for i=1,lines*4 do
500     >      for j=cols*2-1,1,-1 do
501     >        grid[i][j+1] = grid[i][j]
502     >      end
503     >      grid[i][1] = 0
504     >    end
505     >  end
506     >end
507 - __teliva_timestamp: original
508   main:
509     >function main()
510     >  curses.init_pair(1, 22, 189)
511     >
512     >  -- initialize grid based on commandline args
513     >  if (#arg == 0) then
514     >    -- by default, start from a deterministically random state
515     >    for i=1,lines*4 do
516     >      for j=1,cols*2 do
517     >        grid[i][j] = math.random(0, 1)
518     >      end
519     >    end
520     >  elseif arg[1] == "random" then
521     >    -- start from a non-deterministically random start state
522     >    math.randomseed(os.time())
523     >    for i=1,lines*4 do
524     >      for j=1,cols*2 do
525     >        grid[i][j] = math.random(0, 1)
526     >      end
527     >    end
528     >  -- shortcuts for some common patterns
529     >  elseif arg[1] == "pentomino" then
530     >    -- https://www.conwaylife.com/wiki/Pentomino
531     >    grid[83][172] = 1
532     >    grid[83][173] = 1
533     >    grid[84][173] = 1
534     >    grid[84][174] = 1
535     >    grid[85][173] = 1
536     >  elseif arg[1] == "glider" then
537     >    -- https://www.conwaylife.com/wiki/Glider
538     >    grid[5][4] = 1
539     >    grid[6][5] = 1
540     >    grid[7][3] = 1
541     >    grid[7][4] = 1
542     >    grid[7][5] = 1
543     >  elseif arg[1] == "blinker" then
544     >    -- https://www.conwaylife.com/wiki/Blinker
545     >    grid[7][3] = 1
546     >    grid[7][4] = 1
547     >    grid[7][5] = 1
548     >  elseif arg[1] == "block" then
549     >    -- https://www.conwaylife.com/wiki/Block
550     >    grid[5][4] = 1
551     >    grid[5][5] = 1
552     >    grid[6][4] = 1
553     >    grid[6][5] = 1
554     >  elseif arg[1] == "loaf" then
555     >    -- https://www.conwaylife.com/wiki/Loaf
556     >    grid[5][4] = 1
557     >    grid[5][5] = 1
558     >    grid[6][6] = 1
559     >    grid[7][6] = 1
560     >    grid[8][5] = 1
561     >    grid[7][4] = 1
562     >    grid[6][3] = 1
563     >  else
564     >    -- Load a file in the standard "plaintext" format: https://www.conwaylife.com/wiki/Plaintext
565     >    --
566     >    -- Each pattern page at https://www.conwaylife.com/wiki provides its
567     >    -- plaintext representation in a block called "Pattern Files" on the right.
568     >    --
569     >    -- For example, check out the list of Important Patterns at
570     >    -- https://www.conwaylife.com/wiki/Category:Patterns_with_Catagolue_frequency_class_0
571     >    load_file(Window, nil, arg[1])
572     >  end
573     >
574     >  -- main loop
575     >  while true do
576     >    render(Window)
577     >    c = Window:getch()
578     >    update(Window, c)
579     >    step()
580     >  end
581     >end
582 - __teliva_timestamp:
583     >Thu Feb 17 19:58:19 2022
584   doc:blurb:
585     >Conway's Game of Life
586     >
587     >To get around limitations of text mode we use the braille character set to render 8 cells per character.
588     >
589     >By default it initializes the space with a random state of cells. You can also start it up with .cells files from https://conwaylife.com/wiki, or with a few special names:
590     >  $ src/teliva life.tlv block
591     >  $ src/teliva life.tlv loaf
592     >  $ src/teliva life.tlv blinker
593     >  $ src/teliva life.tlv glider
594     >  $ src/teliva life.tlv pentomino