awk-gold-collection

My best software created for POSIX AWK
git clone git://git.luxferre.top/awk-gold-collection.git
Log | Files | Refs | Submodules | README

commit 7cfe539edac0ba47d1dfa51aa19806b2a7795e76
Author: Luxferre <lux@ferre>
Date:   Tue, 23 Jan 2024 10:29:28 +0200

First upload

Diffstat:
A.gitmodules | 9+++++++++
AREADME | 32++++++++++++++++++++++++++++++++
Aengines/awpix.awk | 633+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aengines/dale-8a | 1+
Aengines/lvtl.awk | 187+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aengines/subleq.awk | 40++++++++++++++++++++++++++++++++++++++++
Aengines/tch.awk | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agames/awlite | 1+
Agames/nnfc | 1+
Autils/textereo.awk | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Autils/tgl.awk | 231+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
11 files changed, 1265 insertions(+), 0 deletions(-)

diff --git a/.gitmodules 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 @@ -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 @@ -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<screenSize;i++) { + # render two pixel lines into one text line + s = s getcolorpxl(screen[i-screenWidth], screen[i]) + if((i % screenWidth) == (screenWidth - 1)) { + s = s "\n" + i += screenWidth + } + } + s = s SCR_SRESET # reset styling + printf("%s", s) # output everything +} + +# show the game over banner +function showGameover(w, h, x, y, i, j, datastr, banner) { + w = 35 # banner width + h = 5 # banner height + x = int((screenWidth - w) / 2) # start x position + y = int((screenHeight - h) / 2) # start y position + datastr = \ + "0 1 1 0 0 1 0 0 1 0 1 0 1 1 1 0 0 0 0 0 0 1 0 0 1 0 1 0 1 1 1 0 1 1 0 " \ + "1 0 0 0 1 0 1 0 1 1 1 0 1 0 0 0 0 0 0 0 1 0 1 0 1 0 1 0 1 0 0 0 1 0 1 " \ + "1 0 1 0 1 1 1 0 1 1 1 0 1 1 0 0 0 0 0 0 1 0 1 0 1 0 1 0 1 1 0 0 1 1 0 " \ + "1 0 1 0 1 0 1 0 1 0 1 0 1 0 0 0 0 0 0 0 1 0 1 0 1 0 1 0 1 0 0 0 1 0 1 " \ + "0 1 1 0 1 0 1 0 1 0 1 0 1 1 1 0 0 0 0 0 0 1 0 0 0 1 0 0 1 1 1 0 1 0 1" + split(datastr, banner) + for(i=0;i<screenSize;i++) screen[i] = 0 # clear the screen + # fill the banner + for(j=0;j<h;j++) + for(i=0;i<w;i++) + screen[(y+j) * screenWidth + x + i] = banner[1 + j*w + i] +} + +# show the victory banner +function showVictory(w, h, x, y, i, j, datastr, banner) { + w = 27 # banner width + h = 5 # banner height + x = int((screenWidth - w) / 2) # start x position + y = int((screenHeight - h) / 2) # start y position + datastr = \ + "2 0 2 0 2 2 2 0 0 2 2 0 2 2 2 0 0 2 0 0 2 2 0 0 2 0 2 " \ + "2 0 2 0 0 2 0 0 2 0 0 0 0 2 0 0 2 0 2 0 2 0 2 0 2 0 2 " \ + "2 0 2 0 0 2 0 0 2 0 0 0 0 2 0 0 2 0 2 0 2 2 0 0 0 2 0 " \ + "2 0 2 0 0 2 0 0 2 0 0 0 0 2 0 0 2 0 2 0 2 0 2 0 0 2 0 " \ + "0 2 0 0 2 2 2 0 0 2 2 0 0 2 0 0 0 2 0 0 2 0 2 0 0 2 0 " + split(datastr, banner) + for(i=0;i<screenSize;i++) screen[i] = 0 # clear the screen + # fill the banner + for(j=0;j<h;j++) + for(i=0;i<w;i++) + screen[(y+j) * screenWidth + x + i] = banner[1 + j*w + i] +} + +# game logic implemented here + +function getPos(x, y) { # calculate the actual screen position + x = (screenWidth + x) % screenWidth + y = (screenHeight + y) % screenHeight + return y * screenWidth + x +} + +# locate and initialize all sprite objects +# a sprite here is a sequence of connected same-color pixels +# any spritemem entry is a sequence of numbers: +# color pos1 pos2 pos3 ... + +function initsprite(pos, color, sid, x, y, i, scross, si) { + if(screen[pos] != color) return # do nothing if the color doesn't match + # restore the coordinates (it's more convenient) + x = pos % screenWidth + y = int(pos / screenWidth) + if(!(sid in spritemem)) { # first-time sprite adding logic + spritemem[sid] = color # start the sprite line + if(color == 1) enemies[sid] = sid + else if(color == 2) {goals[sid] = sid; goalCount++} + else if(color == 3) barriers[sid] = sid + else if(color == 6) players[sid] = sid + else if(color == 7) walls[sid] = sid + } + split("", scross) # init sprite cross + scross[0] = pos + si = 1 # sprite cross index + # try to identify same sprite pixels on the same line and column + for(i=1;i<screenWidth;i++) { + pos = getPos(x+i, y) + if(screen[pos] == color) + scross[si++] = pos # append this position + else break + } + for(i=1;i<screenWidth;i++) { + pos = getPos(x-i, y) + if(screen[pos] == color) + scross[si++] = pos # append this position + else break + } + for(i=1;i<screenHeight;i++) { + pos = getPos(x, y+i) + if(screen[pos] == color) + scross[si++] = pos # append this position + else break + } + for(i=1;i<screenHeight;i++) { + pos = getPos(x, y-i) + if(screen[pos] == color) + scross[si++] = pos # append this position + else break + } + for(i in scross) { # iterate over the cross + screen[scross[i]] = 0 # clear this pixel + spritemem[sid] = spritemem[sid] " " scross[i] + x = scross[i] % screenWidth + y = int(scross[i] / screenWidth) + # now, recursively call this function for all edges + pos = getPos(x - 1, y - 1) # upper left + if(screen[pos] == color) initsprite(pos, color, sid) + pos = getPos(x + 1, y - 1) # upper right + if(screen[pos] == color) initsprite(pos, color, sid) + pos = getPos(x - 1, y + 1) # lower left + if(screen[pos] == color) initsprite(pos, color, sid) + pos = getPos(x + 1, y + 1) # lower right + if(screen[pos] == color) initsprite(pos, color, sid) + } +} + +function buildsprites(sid, pos) { + spritemem[0] = 0 # the first entry is always 0 + sid = 1 # start from sprite id 1 + for(pos=0;pos<screenSize;pos++) { + if(screen[pos] > 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<sw;i++) hf = hf && (xs[i] == i) + if(hf) { # up-left or up-right + f = 1 # detection flag + for(i=sw;i<(2*sw)-1;i++) f = f && (xs[i] == 0) + if(f) return 5 # up-left + f = 1 # detection flag + for(i=sw;i<(2*sw)-1;i++) f = f && (xs[i] == (sw-1)) + if(f) return 9 # up-right + } + hf = 1 # horizontal line detection flag + for(i=sw-1;i<(2*sw)-1;i++) hf = hf && (xs[i] == (i-sw+1)) + if(hf) { # down-left or down-right + f = 1 # detection flag + for(i=0;i<sw-1;i++) f = f && (xs[i] == 0) + if(f) return 6 # down-left + f = 1 # detection flag + for(i=0;i<sw-1;i++) f = f && (xs[i] == sw-1) + if(f) return 10 # down-right + } + } else if(sw == 2*sh - 1) { # try to detect a vertically moving arrow + f = 1 # detection flag + for(i=0;i<l;i++) + f = f && (xs[i] == sh - 1 + int((i+1)/2)*(i%2 ? -1 : 1)) + if(f) return 1 # arrow up detected + f = 1 # detection flag + for(i=0;i<l;i++) + f = f && (xs[l - 1 - i] == sh - 1 + int((i+1)/2)*(i%2 ? 1 : -1)) + if(f) return 2 # arrow down detected + } else if(sh == 2*sw - 1) { # try to detect a horizontally moving arrow + f = 1 # detection flag + for(i=0;i<l;i++) f = f && (xs[i] == abs(sw - i - 1)) + if(f) return 4 # arrow left detected + f = 1 # detection flag + for(i=0;i<l;i++) f = f && (xs[i] == sw - abs(sw - i - 1) - 1) + if(f) return 8 # arrow right detected + } + return 0 # no movement detected +} + +# detect auto-moving sprites from sprite memory +function buildautos(sid, tarr, i, l, pxl, rs) { + split("", autos) # clear the array + for(sid in spritemem) { + l = split(spritemem[sid], tarr) + 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 + l = uniq(pxl, l) + qsort(pxl, 1, l) # get sorted pixel positions into pxl + rs = int(tarr[1]) # build the sorted sprite + for(i=1;i<=l;i++) rs = rs " " pxl[i] + spritemem[sid] = rs # save the sorted sprite + if((rs = detectdir(pxl, l)) > 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<screenSize;i++) screen[i] = 0 + split("", moves) # clear the move map + # pre-draw the objects for collision detection + for(i in walls) drawsprite(i) + for(i in enemies) drawsprite(i) + for(i in goals) drawsprite(i) + for(i in barriers) drawsprite(i) + # pre-draw and pre-move all manually movable sprites + for(i in players) { + drawsprite(i) + if(!(i in autos)) moves[i] = dx " " dy + } + # pre-move all automatically movable sprites + for(i in autos) { # key: sid, value: 1248 up down left right + adx = ady = 0 + if(autos[i]%2) ady = -1 + if(int(autos[i]/2)%2) ady = 1 + if(int(autos[i]/4)%2) adx = -1 + if(int(autos[i]/8)%2) adx = 1 + moves[i] = adx " " ady + } + # perform all movements with collision detection + for(i in moves) { # key: sid, value: dx dy pair + split(moves[i], deltas) + dx = int(deltas[1]); dy = int(deltas[2]) + if(dx || dy) { # only do anything if movement is performed + cres = collide(i, dx, dy) # run the collision simulator + if(cres == 1 || cres == 2) { # unresolvable collision + keystatus = 0 + # don't do anything unless this is an auto-moving sprite + if(i in autos) { # reuse adx and ady to save additional results + adx = ady = 0 + if(dx == 0) ady = 1 # only vertical flip + else if(dy == 0) adx = 1 # only horizontal flip + else { # we need to detect what side we collided with + if(collide(i, dx, 0) == cres) adx = 1 # left/right side + if(collide(i, 0, dy) == cres) ady = 1 # lower/upper side + if(adx == 0 && ady == 0) adx = ady = 1 + } + flipdirection(i, adx, ady) # flip the sprite and its direction + while(collide(i, 0, 0) == cres) # we still are in a collision state + spritemem[i] = movesprite(i, -dx, -dy) + } + } else { # no collision or it's resolved + if(cres == 3) {gameoverFlag = 1;keystatus = 0} + else if(cres == 4) {victoryFlag = 1;keystatus = 0} + if(i in spritemem) # sprite still here, move it for real + spritemem[i] = movesprite(i, dx, dy) + } + } + } + # sweep all the sprites pending deletion + for(i in sweeps) { + if(i in spritemem) delete spritemem[i] + if(i in players) delete players[i] + if(i in goals) delete goals[i] + if(i in walls) delete walls[i] + if(i in enemies) delete enemies[i] + if(i in barriers) delete barriers[i] + if(i in moves) delete moves[i] + if(i in autos) delete autos[i] + if(i in sweeps) delete sweeps[i] + } + # clear the screen buffer + for(i=0;i<screenSize;i++) screen[i] = 0 + # update the screen buffer in the correct order + for(i in walls) drawsprite(i) + for(i in enemies) drawsprite(i) + for(i in goals) drawsprite(i) + for(i in barriers) drawsprite(i) + for(i in players) drawsprite(i) + return 0 # normal loop iteration +} + +# entry point code here + +function runmachine(fname) { + # clear the arrays + split("", screen) + split("", spritemem) + split("", sweeps) + split("", walls) # 7 + split("", enemies) # 1 + split("", goals) # 2 + split("", barriers) # 3 + split("", players) # 6 + + # load the rom in a clever way: + cmd = "png2pnm -n \"" fname "\"" + cmd | getline pformat + if(pformat != "P3") trapout("Invalid image format!") + i = 0 + while((cmd | getline) > 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<screenSize;i++) { + j = (i+1) * 3 # base index to read from + r = IMGDATA[j]; g = IMGDATA[j+1]; b = IMGDATA[j+2] + if(r == 0 && g == 0 && b == 0) screen[i] = 0 # black + else if(r == mval && g == 0 && b == 0) screen[i] = 1 # red + else if(r == 0 && g == mval && b == 0) screen[i] = 2 # green + else if(r == mval && g == mval && b == 0) screen[i] = 3 # yellow + else if(r == 0 && g == mval && b == mval) screen[i] = 6 # cyan + else if(r == mval && g == mval && b == mval) screen[i] = 7 # white + else trapout(sprintf("invalid color %d, %d, %d!", r, g, b)) + delete IMGDATA[j]; delete IMGDATA[j+1]; delete IMGDATA[j+2] + } + delete IMGDATA[0] + delete IMGDATA[1] + delete IMGDATA[2] + # now, we have all screen data in screen array + buildsprites() # build the spritemem array with all sprites + buildautos() # build the autos array with auto-moving sprites + # main execution logic starts here + altbufon() # enter the alternative screen buffer + setterm(3) # enter the non-blocking input mode before the event loop + while(1) { # our event loop is here + if((key = readkeynb()) > 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;i<framecycle;i++) a+=i # sleep on 1/15 sec, more efficiently + } + if(loopstatus == 888) # game over/restart trigger + runmachine(fname) # restart from the beginning on the loop break + else return # victory +} + +# get current Unix timestamp with millisecond precision with various methods +function timestampms(cmd, res) { + cmd = "echo $EPOCHREALTIME" + cmd | getline res + close(cmd) + sub(/[,\.]/,"", res) + res = int(res) + if(res) return res / 1000 # micro=>milli + # 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<sc;i++) a += i + et = timestampms() + } while(et == st) + # now, we have our cps metric + cps = 1000 * sc / (int(et) - int(st)) + # but we need 1/15 second + return int(cps / 15) +} + +BEGIN { + print "Profiling the frame timing..." + framecycle = hostprofile() # get the amount of host cycles to skip + print "Detected cycles per frame:", framecycle + if(ARGC < 2) trapout("no cart .png file specified!") + # init some string constants and parameters + SCR_CLR = sprintf("\033[2J") # screen clear command + SCR_SRESET = sprintf("\033[0m\033[0;0H") + KEY_INPUT_STREAM = "od -tu1 -w1 -An -N1 -v" + for(c=1;c<ARGC;c++) runmachine(ARGV[c]) # run all arguments sequentially + shutdown() +} diff --git a/engines/dale-8a b/engines/dale-8a @@ -0,0 +1 @@ +Subproject commit c504f446ee8e6bc57a5ab893d442089404576eae diff --git a/engines/lvtl.awk b/engines/lvtl.awk @@ -0,0 +1,187 @@ +# LVTL-W: port of LVTL-R to Busybox/GNU AWK in under 130 SLOC +# should also work in any other AWK version with bitwise operations +# +# Usage: [busybox] awk -f lvtl.awk [prog.vtl -] +# (don't forget the - after the program or LVTL will exit upon preloading) +# +# Any program valid for LVTL-W will also run on LVTL-R and LVTL-O and, +# if it doesn't use bitwise ops, on the original VTL-2 and VTL02 too +# +# Differences from the C version of LVTL-R (besides being much slower): +# - lines are stored in an AWK array where line numbers are keys +# - maximum line length is not enforced +# - the variables and array areas are fully separated from line memory +# - because of all this, & is always 0 and * is always 65535 +# - extra whitespace before and after line numbers is accepted +# - only LFs are printed instead of CRLFs +# - strings are allowed in immediate statements +# +# Differences from the original VTL-2 for Altair: +# - both LF and CRLF are accepted (but not saved into RAM) +# - only LFs are printed instead of CRLFs +# - maximum line length is not enforced +# - all whitespace after the line number is fully ignored +# - any whitespace within expressions is also insignificant +# - parentheses are NOT auto-closed at the end of the statement +# - input with ? is NOT evaluated and only numbers are accepted +# - only 26 characters (A-Z) are valid generic variable names +# - supports all standard VTL-2 binary operators: +, -, *, /, =, >, < +# - 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 @@ -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 @@ -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 @@ -0,0 +1 @@ +Subproject commit 026c365e50d9e602a5ec22501eb016a3ae515345 diff --git a/games/nnfc b/games/nnfc @@ -0,0 +1 @@ +Subproject commit 4b5c40b637a7cba26ffbfd3827457fd40c52d422 diff --git a/utils/textereo.awk 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<tlen;i++) mapline = "0" mapline "0" # grow the line to the width + if(length(mapline) > WIDTH) mapline = substr(mapline, 1, WIDTH) + for(i=0;i<PATLEN;i++) { # fill in the pattern buffer + do # we can repeat the characters in the pattern but not immediately + pbc = substr(CHARSET, 1+int(rand()*CHRANGE), 1) + while(pbc == j || !length(pbc)) # don't allow empty or repeating chars + PATBUF = PATBUF pbc # concatenate the character to the pattern + j = pbc # save as the previous value to j + # delete this character from the unused buffer + FREEBUF = dchar(FREEBUF, index(FREEBUF, pbc)) + } + j = 0 # reset the secondary counter + prevlen = PATLEN # cache the previous pattern length + maplen = length(mapline) + patpos = 0 # track pattern position + for(i=0;++i<=maplen;) { # iterate over every character + curlen = PATLEN - int(substr(mapline, i, 1)) # extract depth value and length + delta = curlen - prevlen + if(delta > 0) # add unused characters if decreasing depth value + for(j=0;j<delta;j++) { + ri = 1+int(rand()*length(FREEBUF)) # get random index + rc = substr(FREEBUF, ri, 1) # retrieve the character + FREEBUF = dchar(FREEBUF, ri) # delete it from the buffer + PATBUF = ichar(PATBUF, 1+patpos, rc) # insert it here + } + else if(delta < 0) # remove characters if increasing depth value + for(j=0;j<-delta;j++) + PATBUF = dchar(PATBUF, 1+patpos) + prevlen = curlen # update the previous length + # now, output the current pattern character + printf("%c", substr(PATBUF, 1 + (patpos % curlen), 1)) + patpos = (patpos + 1) % curlen # update pattern position + } + printf("\n") +} diff --git a/utils/tgl.awk b/utils/tgl.awk @@ -0,0 +1,231 @@ +# The Great Library of useful AWK functions +# Fully POSIX-compatible but sometimes depends on other POSIX commands +# Use with your programs like this: +# LANG=C awk -f tgl.awk -f your_prog.awk [args] +# +# Current functionality: +# * single character input: setterm, getchar +# * ASCII and UTF-8 codepoint conversion: ord, wctomb, mbtowc +# * loading binary files as decimal integers into arrays: loadbin +# * saving binary files from arrays with decimal integers: savebin +# * tangent and cotangent functions: tan, cotan +# * signum, floor and ceiling functions: sign, floor, ceil +# * test for native bitwise operation support: bw_native_support +# * reimplementation of most bitwise operations (unsigned 32-bit): +# - NOT: bw_compl +# - AND: bw_and +# - OR: bw_or +# - XOR: bw_xor +# - NAND: bw_nand +# - NOR: bw_nor +# - >>: 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<len;i++) { + if(wordsize == 1) printf("%c", arr[i]) >> fname + else # we have a multibyte word size + for(j=0;j<wordsize;j++) + printf("%c", int(arr[i]/2^(8*j))%256) >> 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))} +