commit 7cfe539edac0ba47d1dfa51aa19806b2a7795e76 Author: Luxferre Date: Tue Jan 23 10:29:28 2024 +0200 First upload diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..266843d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "engines/dale-8a"] + path = engines/dale-8a + url = git://git.luxferre.top/dale-8a.git +[submodule "games/nnfc"] + path = games/nnfc + url = git://git.luxferre.top/nnfc.git +[submodule "games/awlite"] + path = games/awlite + url = git://git.luxferre.top/awlite.git diff --git a/README b/README new file mode 100644 index 0000000..2b52e20 --- /dev/null +++ b/README @@ -0,0 +1,32 @@ +AWK Gold Collection: my best software created for POSIX AWK +----------------------------------------------------------- +In this repo, I decided to collect all useful software I have written in AWK +throughout the recent years. All of it is strictly POSIX-compliant. The list +is divided into three directories: engines, games and utils. + +Some software contains its own README files, please read them carefully. +Other AWK scripts have the usage described in their comment headers. + += Emulators, interpreters and game engines = + +* engines/awpix.awk: Pix64 game engine port (prototype) +* engines/dale-8a: CHIP-8 virtual machine port + (a submodule from git://git.luxferre.top/dale-8a.git) +* engines/lvtl.awk: VTL-2 programming language interpreter +* engines/subleq.awk: Subleq-16 VM port (can run .dec files) +* engines/tch.awk: TinyChoice game engine port + += Games = + +* games/nnfc: FreeCell solitaire game + (a submodule from git://git.luxferre.top/nnfc.git) +* games/awlite: Text Elite 1.5 port with further improvements and fixes + (a submodule from git://git.luxferre.top/awlite.git) + += Utilities = + +* utils/textereo.awk: ASCII art stereogram generator +* utils/tgl.awk: The Great Library of useful functions missing in POSIX AWK + + +--- Luxferre --- diff --git a/engines/awpix.awk b/engines/awpix.awk new file mode 100644 index 0000000..7af5734 --- /dev/null +++ b/engines/awpix.awk @@ -0,0 +1,633 @@ +#!/sbin/env awk -f +# AWPix - a prototype port of Pix64 console to POSIX AWK +# Requires png2ppm command (netpbm package) to decode PNG carts +# Usage: LANG=C awk -f awpix.awk cart.png[ cart_2.png] ... +# Controls: WASD - movement, R - reset, Esc - exit +# Created by Luxferre in 2023, released into public domain + +# fatal error reporting function +function trapout(msg) { + shutdown() + cmd = "cat 1>&2" + printf("Fatal: %s\n", msg) | cmd + close(cmd) + exit(1) +} + +# graceful shutdown function - restore the terminal state +function shutdown() {printf(SCR_CLR); altbufoff(); close(KEY_INPUT_STREAM); setterm(0)} + +# terminal control routines +function altbufon() {printf("\033[?47h")} +function altbufoff() {printf("\033[?47l")} +function setterm(mode, cmd) { + if(system("stty >/dev/null 2>&1")) return 0 # exit code 0 means we're in a tty + if(!TGL_TERMMODE) { # cache the original terminal input mode + (cmd = "stty -g") | getline TGL_TERMMODE + close(cmd) + } + if(mode == 1) cmd = "-icanon" + else if(mode == 2) cmd = "-icanon -echo" + else if(mode == 3) cmd = "-icanon time 0 min 0 -echo" + else cmd = TGL_TERMMODE # restore the original mode + return system("stty " cmd ">/dev/null 2>&1") # execute the stty command +} + +function readkeynb(key) { # read a key, non-blocking fashion + KEY_INPUT_STREAM | getline key # open the subprocess + key = int(key) # read the key state + close(KEY_INPUT_STREAM) + if(key == 27) {shutdown(); exit(0)} # exit on Esc + if(key == 119 || key == 87) return 1 # W + if(key == 115 || key == 83) return 2 # S + if(key == 97 || key == 65) return 4 # A + if(key == 100 || key == 68) return 8 # D + if(key == 114 || key == 82) return 16 # R + return -1 # if not found, return -1 +} + +# draw a pixel pair according to the color codes +function getcolorpxl(val1, val2) { + return sprintf("\033[3%u;4%um\342\226\200", val1, val2) +} + +# all main rendering is done offscreen and then a single printf is called +function drawscreen(s, i) { + s = SCR_CLR # clear the screen + for(i=screenWidth;i 0) # non-empty pixel + initsprite(pos, screen[pos], sid++) + } +} + +# find a sprite ID by the screen position +# return 0 if not found +function findsprite(pos, sid, i, l, tarr) { + for(sid in spritemem) { + l = split(spritemem[sid], tarr) + for(i=2;i<=l;i++) + if(int(tarr[i]) == pos) return sid + } + return 0 +} + +# raw sprite movement (no blitting) +function movesprite(sid, dx, dy, px, py, tarr, rs, i, l) { + if(dx == 0 && dy == 0) return spritemem[sid] + l = split(spritemem[sid], tarr) + rs = int(tarr[1]) # start the resulting sprite line + for(i=2;i<=l;i++) { + px = int(tarr[i]) % screenWidth + py = int(int(tarr[i]) / screenWidth) + rs = rs " " getPos(px + dx, py + dy) + } + return rs +} + +# collision detection function that takes sprite ID and target X/Y +# return value: +# 0 if no collisions +# 1 if collision CANNOT be resolved +# 2 if collision was resolved by the deletion of a sprite +# 3 if collision leads to game over +# 4 if collision leads to victory +function collide(sid, dx, dy, tarr, i, l, dsid, pos, stype, dtype, cst) { + l = split(movesprite(sid, dx, dy), tarr) # temporary move + cst = 0 # collision status + stype = int(tarr[1]) # source pixel type + for(i=2;i<=l;i++) { # collision detection loop + pos = tarr[i] # get current position + dtype = screen[pos] # get destination pixel type + if(dtype > 0 && (dsid = findsprite(pos)) != sid) { # collision detected + if((stype == 6 && dtype == 1) || (stype == 1 && dtype == 6)) + return 3 # player-enemy collision, game over + else if((stype == 6 && dtype == 3) || (stype == 3 && dtype == 6)) { + # player-barrier collision + if(cst != 1) cst = 2 + sweeps[stype == 3 ? sid : dsid] = 1 + break + } + else if((stype == 6 && dtype == 2) || (stype == 2 && dtype == 6)) { + # player-goal collision + if(cst != 1) cst = 2 + goalCount-- + sweeps[stype == 2 ? sid : dsid] = 1 + break + } + else { # any other type of collision is marked as unresolved + cst = 1 + break + } + } + } + if(goalCount <= 0) return 4 # victory condition + return cst +} + +# draw a single sprite onto the screen +function drawsprite(sid, tarr, i, l) { + if(sid in spritemem) { # sprite still here => let's draw + l = split(spritemem[sid], tarr) + for(i=2;i<=l;i++) # actual drawing loop + screen[tarr[i]] = tarr[1] # draw this pixel + } +} + +# sprite auto-movement engine + +# some quicksort implementation +function qsort(A, left, right, i, last) { + if(left >= right) return + swap(A, left, left+int((right-left+1)*rand())) + last = left + for(i = left+1; i <= right; i++) + if(int(A[i]) < int(A[left])) + swap(A, ++last, i) + swap(A, left, last) + qsort(A, left, last-1) + qsort(A, last+1, right) +} + +function swap(A, i, j, t) { + t = A[i]; A[i] = A[j]; A[j] = t +} + +# uniq implementation +function uniq(A, l, tmpx, i, c) { + for(i in A) { + tmpx[int(A[i])] = i + delete A[i] + } + c = 1 # counter + for(i in tmpx) { + A[c++] = int(i) + delete tmpx[i] + } + return c-1 # new length of A +} + +# detect the box under which the sprite pixels are drawn +# return the following concatenated values: +# width height startx starty +function detectbox(pxl, l, i, x, y, minx, miny, maxx, maxy) { + maxx = maxy = 0 + minx = screenWidth + miny = screenHeight + for(i=1;i<=l;i++) { + x = pxl[i] % screenWidth + y = int(pxl[i] / screenWidth) + if(x > maxx) maxx = x + if(y > maxy) maxy = y + if(x < minx) minx = x + if(y < miny) miny = y + } + return (maxx - minx + 1) " " (maxy - miny + 1) " " minx " " miny +} + +function abs(v) {return v < 0 ? -v : v} + +# detect movement direction from the sorted sprite shape +# returned direction value is: +# up-left 5 +# up 1 +# up-right 9 +# left 4 +# right 8 +# down-left 6 +# down 2 +# down-right 10 +function detectdir(pxl, l, i, sw, sh, md, xs, box, f, hf) { + if(l%2 == 0 || l < 3) return 0 # all arrows have odd number of pixels + split(detectbox(pxl, l), box) + sw = box[1] # sprite width + sh = box[2] # sprite height + md = sw < sh ? sw : sh # minimum dimension + if(md < 2) return 0 # all arrow sprites are at least 2x2 + if(l != 2*md - 1) return 0 # all arrow sprites have 2*md - 1 entries + split("", xs) # clear x coordinate vector + for(i=1;i<=l;i++) xs[i-1] = (pxl[i] % screenWidth) - box[3] + # now, we have a clear pattern of X coordinate numbers + # (because the pixels are ordered, we don't need to check Y coordinates) + if(sw == sh) { # diagonal movement is only defined for square boxes + hf = 1 # horizontal line detection flag + for(i=0;i 0) # arrow sprite detected + autos[sid] = rs # save the direction + } +} + +# flip an auto-moving sprite direction and redraw it +function flipdirection(sid, fliph, flipv, tarr, i, l, pxl, \ + dir, rs, box, x, y, sw, sh, sx, sy) { + # change the direction + dir = int(autos[sid]) + if(flipv && (dir%4)) # vertical flip logic + dir = int(dir/4) * 4 + (3 - (dir%4)) + if(fliph && int(dir/4)) # horizontal flip logic + dir = (int(dir/8) ? 4 : 8) + (dir%4) + autos[sid] = dir + # redraw the sprite + l = split(spritemem[sid], tarr) + rs = tarr[1] # start the resulting sprite line + split("", pxl) # clear the pixel array + for(i=2;i<=l;i++) # iterate over pixel positions + pxl[i-1] = int(tarr[i]) + l-- # get the pixel array length into l + split(detectbox(pxl, l), box) # get the box + sw = box[1] # sprite width + sh = box[2] # sprite height + sx = box[3] # start x coord + sy = box[4] # start y coord + for(i in pxl) { # flip individual pixels according to the box + x = pxl[i] % screenWidth + y = int(pxl[i] / screenWidth) + if(fliph) x = sx + sw - (x - sx) - 1 + if(flipv) y = sy + sh - (y - sy) - 1 + rs = rs " " getPos(x, y) + } + spritemem[sid] = rs # save the updated sprite +} + +# perform all logic here +function logicloop(i, dx, dy, adx, ady, cres, deltas) { + if(victoryFlag) { + showVictory() # show victory banner + if(keystatus > 0) return 999 # exit on any key + else return 0 + } + else if(gameoverFlag) { + showGameover() # show game over banner + return 0 + } + dx = dy = adx = ady = 0 + if(keystatus == 1) dy = -1 # move up + else if(keystatus == 2) dy = 1 # move down + else if(keystatus == 4) dx = -1 # move left + else if(keystatus == 8) dx = 1 # move right + # clear the screen buffer + for(i=0;i 0) { # fill raw image data + if(NF > 0) + for(j=1;j<=NF;j++) + IMGDATA[i++] = int($j) + } + close(cmd) + # the first three values are width, height and maxval + screenWidth = IMGDATA[0] + screenHeight = IMGDATA[1] + mval = IMGDATA[2] + # now, convert the image data into the actual field data + # according to the terminal color codes: + # black 0, red 1, green 2, yellow 3, cyan 6, white 7 + screenSize = screenWidth * screenHeight + goalCount = 0 # green pixel count + gameoverFlag = 0 # game over flag + victoryFlag = 0 # game victory flag + for(i=0;i 0) keystatus = key + else keystatus = 0 + if(keystatus == 16) {loopstatus = 888; break} + loopstatus = logicloop() # handle all events + if(loopstatus > 0) break # break on anomaly + drawscreen() + a=0 + for(i=0;imilli + # otherwise we need to use an alternate, POSIX-compatible method + cmd = "date +%s" + cmd | getline res + close(cmd) + return int(res) * 1000 # s=>milli +} + +# determine the amount of empty cycles needed to fill a single frame +function hostprofile(i, cps, sc, st, et) { + sc = 2000000 # this is an arbitrarily large (but not too large) cycle count + do { + sc += 200000 + st = timestampms() + a = 0 + for(i=0;i, < +# - also supports bitwise operators: & (and), ! (or), # (xor) +# - jumps to itself (like 10 #=10) are prohibited and counted as nops +# - no file I/O (only the option to preload VTL code from a file) +# - self-modifying programs won't work correctly +# +# Created by Luxferre in 2023, released into public domain + +# Global arrays and vars: ORD, PROGLINES, VARS, SCRATCH, PROCCHARS + +# fatal error reporter +function trapout(errmsg) {printf("Fatal error: %s\n", errmsg); exit(1)} + +# read a generic/system variable +function getvar(varname, c) { + res = 0 + if(varname == "$") { # get a single character from stdin + (cmd="c='';read -r -n 1 c;echo \"$c\"") | getline c + close(cmd) + res = ORD[substr(c,1,1)] + } + else if(varname == "?") { # get a number from stdin + getline c # read the line + res = and(int(c), 65535) # cast to 16-bit integer + } + else if(varname == "'") res = int(rand()*65536) # get a pseudorandom number + else if(varname in VARS) res = VARS[varname] # regular or system variable + return res +} + +# set a generic/system variable +function setvar(varname, value, qi) { + if(varname == "$") printf("%c", and(int(value), 255)) # output a character + else if(varname == "?") { # output a string or a number + if(VARS["\""]) { # we print a string + qi = index(value, "\"") # find the ending quote + printf("%s", substr(value, 1, qi - 1)) + # also print LF unless stated otherwise with ; modifier + if(substr(value, qi+1, 1) != ";") printf("\n") + VARS["\""] = 0 # reset the printing flag + } + else printf("%u", and(int(value), 65535)) # we print a number, no LF + } + else if(varname in VARS) { # regular or system variable + value = and(int(value), 65535) # cast to 16-bit integer + # if the var is #, cache the return address before writing + if(varname == "#" && value > 0) VARS["!"] = VARS["#"] + 1 + VARS[varname] = value # update the value + } + else trapout("invalid variable name!") +} + +# strict LTR evaluator +function evalexpr(expr, tkn, i, l, opex, acc, cop, oprnd, subs, res) { + opex = 1 + res = cop = acc = oprnd = 0 + l = length(expr) # GAWK doesn't support multi-init in fors, busybox does + for(i=1;i<=l;i++) { # token position is stored in i + tkn = substr(expr, i, 1) # get a single character token + if(tkn == ")") break # ditch the rest of expression + if(tkn == " " || tkn == "\t") continue # skip all whitespace + if(opex = 1 - opex) { # we expect an operator + if(index("+-*/=<>&!#", tkn)) cop = tkn # save the valid operator + else trapout(sprintf("unexpected binary operator at %u\n", VARS["#"])) + } + else { # we expect a value, a variable, a subscript or a subexpr + if(tkn == "\"") { # quote immediately prints and returns + VARS[tkn] = 1 + return substr(expr, i+1) # return everything after the quote + } + # extract and evaluate a parens or subscript expression + else if(tkn == ":" || tkn == "(") { + subs = evalexpr(substr(expr, i+1)) # process everything after tkn + # set the operand to array elem or eval result + oprnd = (tkn == ":") ? SCRATCH[subs] : subs + i += PROCCHARS # update the outer index + } + else if(index("0123456789", tkn)) { # it's a digit + oprnd = int(substr(expr, i)) # read the integer + i += length(oprnd) - 1 # skip the rest + } + else oprnd = getvar(tkn) # we assume it's a valid variable name + # now, perform the calculation - no default branch! + if(oprnd < 0) oprnd += 65536 # keep the operand positive + oprnd = and(oprnd, 65535) # keep the operand within 16 bits + if(cop == 0) acc = oprnd + else if(cop == "+") acc += oprnd + else if(cop == "-") acc -= oprnd + else if(cop == "*") acc *= oprnd + else if(cop == "/" && oprnd) { + VARS["%"] = acc % oprnd # keep the remainder in the % sysvar + acc = int(acc/oprnd) + } + else if(cop == "=") acc = (acc == oprnd) ? 1 : 0 + else if(cop == ">") acc = (acc >= oprnd) ? 1 : 0 + else if(cop == "<") acc = (acc < oprnd) ? 1 : 0 + else if(cop == "&") acc = and(acc, oprnd) + else if(cop == "!") acc = or(acc, oprnd) + else if(cop == "#") acc = xor(acc, oprnd) + if(acc < 0) acc += 65536 # keep the accumulator positive + res = acc = and(acc, 65535) # keep the accumulator within 16 bits + } + } + PROCCHARS = i # because we started from 1 + return res +} + +# run the statement +function exec_stmt(stmt, ln, lcache, lhs, rhs, ei, vn) { + lcache = ln # cache the line number + ei = index(stmt, "=") # get the first occurrence of = + lhs = substr(stmt, 1, ei - 1) # extract LHS as everything before = + rhs = substr(stmt, ei + 1) # extract RHS as everything after = + if(!length(rhs) || !length(lhs)) return # nop on invalid statements + ei = evalexpr(rhs) # evaluate RHS + vn = substr(lhs, 1, 1) # extract the varname + # evaluate the array subscript and update the array + if(vn == ":") SCRATCH[evalexpr(substr(lhs, 2))] = ei + else setvar(vn, ei) # update the (pseudo) variable + # seek the next statement + ln = VARS["#"] + # we're starting from interactive mode OR updating the number + if((!ln && lcache) || (ln > 0 && ln == lcache)) ln = lcache + 1 + if(ln > 0) { + for(ei=ln;ei<65536;ei++) if(ei in PROGLINES) {ln = ei; break} # search + if(ei >= 65536) ln = 0 # line not found + VARS["#"] = ln # save the closest line number + if(ln) exec_stmt(PROGLINES[ln], ln) # pass new statement for execution + } +} + +BEGIN { # interpreter init + "date +%N"|getline rseed;srand(rseed) # init the PRNG + PROCCHARS = 0 # global cache to track processed characters + for(i=0;i<256;i++) ORD[sprintf("%c", i)] = i # init char-to-ASCII mapping + for(i=0;i<26;i++) VARS[sprintf("%c", i+ORD["A"])] = 0 # init the var memory + # initialize sysvars + VARS["!"] = VARS["\""] = VARS["#"] = VARS["%"] = VARS["&"] = 0 + VARS["*"] = 65535 + for(i=0;i<65536;i++) SCRATCH[i] = 0 # initialize the scratch area + print "LVTL-W by Luxferre\nPress Ctrl+C to exit\n\nOK" +} + +{ # main line-by-line processing + ln = and(int($1), 65535) # attempt to scan line number (default to 0) + if(NF > 1) $1 = "" # prepare to scan the statement + gsub(/^[ \t]+|[ \t]+$/, "") # trim the statement + if(ln > 0) { # we have a line number + if($0 == "" || $0 == ln) delete PROGLINES[ln] # delete the line + else PROGLINES[ln] = $0 # cache the statement + } + else { # it's an immediate statement + if($0 == "0") { # list the program + for(i=1;i<65536;i++) # start from 1 because 0 is not a valid line number + if(i in PROGLINES) print i, PROGLINES[i] # list the found line + } + else exec_stmt($0, 0) # pass to immediate execution + print "\nOK" + } +} diff --git a/engines/subleq.awk b/engines/subleq.awk new file mode 100644 index 0000000..d5e3672 --- /dev/null +++ b/engines/subleq.awk @@ -0,0 +1,40 @@ +#!/sbin/env awk -f +# POSIX AWK port of Subleq VM (16-bit variant) +# Accepts .dec files as input +# Usage: [busybox] awk -f subleq.awk program.dec +# Created by Luxferre in 2023, released into public domain + +function L(v) { # cast any value to unsigned 16-bit integer + v = int(v) + while(v < 0) v += 65536 + return int(v%65536) +} + +function getchar(c, cmd) { # POSIX-compatible getchar emulation with sh read + (cmd="c='';IFS= read -r -n 1 -d $'\\0' c;printf '%u' \"'$c\"") | getline c + close(cmd) + return int(c) +} + +BEGIN { + for(pc=0;pc<65536;pc++) MEM[pc] = 0 # init the memory array + pc = a = b = c = 0 # reset the program counter and other vars +} + +# match on any 16-bit signed integer in the .dec file +{ for(i=1;i<=NF;i++) if($i ~ /^[-0-9][0-9]*$/) MEM[pc++] = L($i) } + +END { # start the actual execution + for(pc=0;pc<32768;) { + a = MEM[pc++]; b = MEM[pc++]; c = MEM[pc++] # fill the cell addresses + # -1 in cell A => input to cell B + if(a == 65535) MEM[b] = L(getchar()) + # -1 in cell B => output cell A + else if(b == 65535) printf("%c", MEM[a]%256) + # main OISC logic here + else { + MEM[b] = L(MEM[b] - MEM[a]) # subtract the first 2 cells and cast + if(MEM[b] == 0 || (MEM[b] > 32767)) pc = c # jump if result <=0 + } + } +} diff --git a/engines/tch.awk b/engines/tch.awk new file mode 100644 index 0000000..37e213d --- /dev/null +++ b/engines/tch.awk @@ -0,0 +1,57 @@ +#!/sbin/env awk -f +# TCh: the simplest TinyChoice game engine port to POSIX AWK +# Usage: awk -f tch.awk your-story.txt +# Controls: respond with a number to go to a particular choice, q to quit +# Created by Luxferre in 2023, released into public domain + +BEGIN { + curscreen = "" # store the current screen key + firstscreen = "" + split("", screens) # init the screens map + if(ARGC < 2) {print "No input story file!"; exit(1)} + while((getline < ARGV[1]) > 0) { + if($0 ~ /^=.*=$/) { # header line processing + if(curscreen) { # finalize the previous entry + gsub(/^\n+/, "", screens[curscreen]) + gsub(/\n+$/, "", screens[curscreen]) + } else firstscreen = -999 + curscreen = tolower(substr($0, 2, length($0)-2)) + if(firstscreen == -999) firstscreen = curscreen # save the key value + } else if(curscreen) screens[curscreen] = screens[curscreen] $0 "\n" + } + close(ARGV[1]) + delete ARGV[1] + # actual runtime logic + curscreen = ("start" in screens) ? "start" : firstscreen + while(length(curscreen) > 0) { # get the lines of the current screen + l = split(screens[curscreen], lines, "\n") + split("", choices) # init the choices array + ch = 1 # init the choice number + for(i=1;i<=l;i++) { # we must iterate in order + line = lines[i] + if(index(line, "->") > 0) { # we have a choice spec + split(line, rch, /[ \t]*\->[ \t]*/) + tlabel = rch[1] # label is before -> + tscreen = rch[2] # target screen is after -> + gsub(/^[ \t]+/, "", tlabel) # trim leading spaces in the label + gsub(/[ \t]+$/, "", tlabel) # trim trailing spaces in the label + gsub(/^[ \t]+/, "", tscreen) # trim leading spaces in the ref + gsub(/[ \t]+$/, "", tscreen) # trim trailing spaces in the ref + if(!(tscreen in screens)) { + printf("Invalid reference %s!\n", tscreen) + exit(1) + } + choices[ch] = tscreen + printf("%u) %s\n", ch++, tlabel) + } else print line # just print the line "as is" + } + do { + printf "\n> "; if((getline) <= 0) break # prompt for the user choice + if($1 == "q" || $1 == "Q" || $1 == "quit" || $1 == "QUIT") { + print "Bye!"; exit(0) + } + ch = int($1) + } while(!(ch && (ch in choices) && (choices[ch] in screens))) + curscreen = choices[ch] # jump to the selected screen + } +} diff --git a/games/awlite b/games/awlite new file mode 160000 index 0000000..026c365 --- /dev/null +++ b/games/awlite @@ -0,0 +1 @@ +Subproject commit 026c365e50d9e602a5ec22501eb016a3ae515345 diff --git a/games/nnfc b/games/nnfc new file mode 160000 index 0000000..4b5c40b --- /dev/null +++ b/games/nnfc @@ -0,0 +1 @@ +Subproject commit 4b5c40b637a7cba26ffbfd3827457fd40c52d422 diff --git a/utils/textereo.awk b/utils/textereo.awk new file mode 100644 index 0000000..3aac96a --- /dev/null +++ b/utils/textereo.awk @@ -0,0 +1,73 @@ +# Textereo: ASCII stereogram generator in AWK +# Usage: [busybox] awk -f textereo.awk map.txt +# Map format: +# First line: [desired width] [maxpattern length] +# Second line: alphabet (allowed characters to generate patterns from) +# Next H lines: depth map, W digits from 0 to 7 without spaces +# You can insert single zeros or empty lines into the map to adjust height +# +# Created by Luxferre in 2023, released into public domain + +function dchar(str, pos) { # delete a character from str at position pos + if(!pos) return str + else return substr(str, 1, pos - 1) substr(str, pos + 1) +} + +function ichar(str, pos, c) { # insert a character c into str at position pos + return substr(str, 1, pos - 1) c substr(str, pos) +} + +BEGIN { # we use 95-character subset from ASCII by default + "date +%N"|getline rseed;srand(rseed) # init the PRNG + getline # read the first line from the file to parse parameters + WIDTH = int($1) # desired width of the image to generate + PATLEN = int($2) # generated pattern length (must be between 8 and W/2) + getline # read the full character set + CHARSET = $0 + CHRANGE = length(CHARSET) + if(WIDTH == 0) WIDTH = 78 + if(PATLEN < 8 || PATLEN > (WIDTH/2)) PATLEN = 10 # 10 to 15 is optimal +} + +{ # process the map itself + j = 0 # set the secondary counter to store previous value + FREEBUF = CHARSET # start with the full charset + PATBUF = "" # start with empty pattern buffer + mapline = $0 # cache the map line + RWIDTH = length(mapline); # real pattern width + tlen = (WIDTH - RWIDTH) / 2 # trailer AND header length + for(i=0;i WIDTH) mapline = substr(mapline, 1, WIDTH) + for(i=0;i 0) # add unused characters if decreasing depth value + for(j=0;j>: bw_rshift +# - <<: bw_lshift +# +# Created by Luxferre in 2023, released into public domain + +# set/restore the terminal input mode using stty +# usage: setterm(0|1|2|3) +# 0 - restore the original terminal input mode +# 1 - blocking single-character input with echo +# 2 - blocking single-character input without echo +# 3 - non-blocking single-character input without echo +# in pipes, this function doesn't do anything +# (but returns 0 since it's not an error) +# otherwise an actual stty exit code is returned +function setterm(mode, cmd) { + if(system("stty >/dev/null 2>&1")) return 0 # exit code 0 means we're in a tty + if(!TGL_TERMMODE) { # cache the original terminal input mode + (cmd = "stty -g") | getline TGL_TERMMODE + close(cmd) + } + if(mode == 1) cmd = "-icanon" + else if(mode == 2) cmd = "-icanon -echo" + else if(mode == 3) cmd = "-icanon time 0 min 0 -echo" + else cmd = TGL_TERMMODE # restore the original mode + return system("stty " cmd ">/dev/null 2>&1") # execute the stty command +} + +# getchar emulation using od +# caches the read command for further usage +# also able to capture null bytes, unlike read/printf approach +# use in conjunction with setterm to achieve different input modes +# setting LANG=C envvar is recommended, for GAWK it is required +# usage: getchar() => integer +function getchar(c) { + if(!TGL_GCH_CMD) TGL_GCH_CMD = "od -tu1 -w1 -N1 -An -v" # first time usage + TGL_GCH_CMD | getline c + close(TGL_GCH_CMD) + return int(c) +} + +# get the ASCII code of a character +# setting LANG=C envvar is recommended, for GAWK it is required +# usage: ord(c) => integer +function ord(c, b) { + # init char-to-ASCII mapping if it's not there yet + if(!TGL_ORD["#"]) for(b=0;b<256;b++) TGL_ORD[sprintf("%c", b)] = b + return int(TGL_ORD[c]) +} + +# encode a single integer UTF-8 codepoint into a byte sequence in a string +# setting LANG=C envvar is recommended, for GAWK it is required +# usage: wctomb(code) => string +# we can safely use the string type for all codepoints above 0 as all +# multibyte sequences have a high bit set, so no null byte is there +# for invalid codepoints, an empty string will be returned +function wctomb(code, s) { + code = int(code) + if(code < 0 || code > 1114109) s = "" # invalid codepoint + else if(code < 128) s = sprintf("%c", code) # single byte + else if(code < 2048) # 2-byte sequence + s = sprintf("%c%c", \ + 192 + (int(code/64) % 32), \ + 128 + (code % 64)) + else if(code < 65536) # 3-byte sequence + s = sprintf("%c%c%c", \ + 224 + (int(code/4096) % 16), \ + 128 + (int(code/64) % 64), \ + 128 + (code % 64)) + else # 4-byte sequence + s = sprintf("%c%c%c%c", \ + 240 + (int(code/262144) % 8), \ + 128 + (int(code/4096) % 64), \ + 128 + (int(code/64) % 64), \ + 128 + (code % 64)) + return s +} + +# decode a byte string into a UTF-8 codepoint +# setting LANG=C envvar is recommended, for GAWK it is required +# usage: mbtowc(s) => integer +# decoding stops on the first encountered invalid byte +function mbtowc(s, len, code, b, pos) { + len = length(s) + code = 0 + for(pos=1;pos<=len;pos++) { + code *= 64 # shift the code 6 bits left + b = ord(substr(s, pos, 1)) + if(pos == 1) { # expect a single or header byte + if(b < 128) {code = b; break} # it resolves into a single byte + else if(b >= 192 && b < 224) # it's a header byte of 2-byte sequence + code += b % 32 + else if(b >= 224 && b < 240) # it's a header byte of 3-byte sequence + code += b % 16 + else if(b >= 240) # it's a header byte of 4-byte sequence + code += b % 8 + else break # a trailer byte in the header position is invalid + } + else if(b >= 128 && b < 192) # it must be a trailer byte + code += b % 64 + else break # a header byte in the trailer position is invalid + } + return code +} + +# load any binary file into an AWK array (0-indexed), depends on od +# returns the resulting array length +# usage: loadbin(fname, arr, len, wordsize) => integer +# len parameter is optional, specifies how many bytes to read +# (if 0 or unset, read everything) +# wordsize parameter is optional, 1 byte by default +# multibyte words are considered little-endian +function loadbin(fname, arr, len, wordsize, cmd, i) { + wordsize = int(wordsize) + if(wordsize < 1) wordsize = 1 + len = int(len) + i = (len > 0) ? (" -N" len " ") : "" + cmd = "od -tu" wordsize " -An -w" wordsize i " -v \"" fname "\"" + # every line should be a single decimal integer (with some whitespace) + i = 0 + while((cmd | getline) > 0) # read the next line from the stream + if(NF) arr[i++] = int($1) # read the first and only field + close(cmd) # close the od process + return i +} + +# save an AWK array (0-indexed) into a binary file +# setting LANG=C envvar is recommended, for GAWK it is required +# returns the amount of written elements +# usage: savebin(fname, arr, len, wordsize) => integer +# wordsize parameter is optional, 1 byte by default +# multibyte words are considered little-endian +function savebin(fname, arr, len, wordsize, i, j) { + wordsize = int(wordsize) + if(wordsize < 1) wordsize = 1 + printf("") > fname # truncate the file and open the stream + for(i=0;i> fname + else # we have a multibyte word size + for(j=0;j> fname + } + close(fname) # close the output file + return i +} + +# the missing tangent/cotangent functions + +function tan(x) {return sin(x)/cos(x)} +function cotan(x) {return cos(x)/sin(x)} + +# the missing sign/floor/ceil functions + +function sign(x) {return x < 0 ? -1 : !!x} +function floor(x, f) { + f = int(x) + if(x == f) return x + else return x >= 0 ? f : (f - 1) +} +function ceil(x, f) { + f = int(x) + if(x == f) return x + else return x >= 0 ? (f + 1) : f +} + +# Bitwise operations section + +# test if the AWK engine has non-POSIX bitwise operation functions +# (and, or, xor, compl, lshift, rshift) implemented natively: +# if compl is missing, it will be concatenated with 1 and equal to 1 +# so the inverse of this condition will be the result +function bw_native_support() {return (compl (1) != 1)} + +# now, the implementation of the operations themselves +# note that all complements are 32-bit and all operands must be non-negative + +function bw_compl(a) {return 4294967295 - int(a)} +function bw_lshift(a, b) {for(;b>0;b--) a = int(a/2);return a} +function bw_rshift(a, b) {for(;b>0;b--) a *= 2;return int(a)} +function bw_and(a, b, v, r) { + v = 1; r = 0 + while(a > 0 || b > 0) { + if((a%2) == 1 && (b%2) == 1) r += v + a = int(a/2) + b = int(b/2) + v *= 2 + } + return int(r) +} +function bw_or(a, b, v, r) { + v = 1; r = 0 + while(a > 0 || b > 0) { + if((a%2) == 1 || (b%2) == 1) r += v + a = int(a/2) + b = int(b/2) + v *= 2 + } + return int(r) +} +function bw_xor(a, b, v, r) { + v = 1; r = 0 + while(a > 0 || b > 0) { + if((a%2) != (b%2)) r += v + a = int(a/2) + b = int(b/2) + v *= 2 + } + return int(r) +} +function bw_nand(a, b) {return bw_compl(bw_and(a,b))} +function bw_nor(a, b) {return bw_compl(bw_or(a,b))} +