nnfc

No-nonsense FreeCell game
git clone git://git.luxferre.top/nnfc.git
Log | Files | Refs

commit d00c7efc6e198508685d7084ed0c47f0bd7f59cb
Author: Luxferre <lux@ferre>
Date:   Mon, 15 Jan 2024 17:49:11 +0200

initial prototype

Diffstat:
Annfc.awk | 217+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 217 insertions(+), 0 deletions(-)

diff --git a/nnfc.awk b/nnfc.awk @@ -0,0 +1,217 @@ +# no-nonsense FreeCell +# run as: [n]awk -f nnfc.awk [-v ASCII=1] [-v MONOCHROME=1] [-v SEED=x] +# created by Luxferre in 2024, released into public domain + +# Helper functions + +function alen(a, i, k) { # determine array length + k = 0 + for(i in a) k++ + return k +} + +# split a string with separator into an array but with 0-based indexes +function asplit(s, a, sep, len, i) { + split(s, a, sep) # get 1-based-index array in a + len = alen(a) + for(i=0;i<len;i++) a[i] = a[i+1] # move everything to the left + delete a[len] # delete the last element after moving +} + +# serialize any associative array into a string +# sep must be a single char not occurring in any key or value +function assocser(aa, sep, outs, k) { + outs = "" # initialize an ordered array string + for(k in aa) # iterate over keys + outs = outs sep k sep aa[k] + return substr(outs, 2) # delete the first sep +} + +# deserialize any associative array from a string +# sep must be a single char used when calling assocser() function +function assocdes(s, aa, sep, oa, i, len) { + split("", aa) # erase aa array + split(s, oa, sep) # get 1-based ordered array in oa + len = alen(oa) # ordered array length + for(i=1;i<=len;i+=2) # populate aa + aa[oa[i]] = oa[i+1] +} + + +# get card suit index (0=clubs, 1=diamonds, 2=hearts, 3=spades) +function getsuit(n) {return int(n/13)} + +# get card color by numeric value: 0 is red, 1 is black +function getcolor(n, si) {si = getsuit(n); return (si > 0 && si < 3) ? 0 : 1} + +# Traditional M$ FreeCell LCG for deal emulation +function fclcg() { + fc_lstate = (214013 * fc_lstate + 2531011) % 2147483648 + return int(fc_lstate / 65536) +} + +# render the playfield from cardstr, table, fcell and fnd arrays +function render(i, j, res) { + res = "" + # top left: free cells + for(i=0;i<4;i++) res = res sprintf("%s", cardstr[fcell[i]]) + # top right: foundations + for(i=0;i<4;i++) res = res sprintf("%s", cardstr[fnd[i]]) + res = res "\n" + # delimiter + for(i=0;i<23;i++) res = res "-" + res = res "\n" + # main table rendering + # max possible column length is 20 elements (8 + 12) + for(j=0;j<20;j++) { + for(i=0;i<8;i++) { + if((i,j) in table) + res = res cardstr[table[i,j]] + else res = res " " + } + res = res "\n" + } + sub(/[ \t\n\r]+$/, "", res) # trim all trailing whitespace + printf("\nGame #%u\n", fc_deal) + print res # finally print out the resulting field +} + +# main logic/move function (also handles the undo) +# statuses: 0 - irrecoverable error, 1 - continue, 2 - victory +function domove(cmd, valid, nums, from, to, si) { + if(cmd == "u") { # undo logic + if(undoidx > 0) { + undoidx-- + assocdes(undos["table",undoidx], table, ",") + assocdes(undos["fnd",undoidx], fnd, ",") + assocdes(undos["fcell",undoidx], fcell, ",") + assocdes(undos["tablens",undoidx], tablens, ",") + } else print "Nowhere to undo!" + return 1 # continue + } + valid = 0 # invalid by default until all checks are done + # if cmd is not q or one of the above, then it must be two numbers + # 1 to 8 is the column (tableau) number + # 9 to 12 are free cell indices, 0 means foundation + split(cmd, nums, " ") # numbers must be space-separated + from = int(nums[1]) # location from which we're moving + to = int(nums[2]) # location to which we're moving + rfrom = from - 1 # real from location + rto = to - 1 # real to location + if(from > 0 && from < 13 && to > -1 && to < 13) { # first check + # determine what card we're trying to move + cval = -1 + if(from < 9) { moving from a non-empty tableau + if(tablens[rfrom] > 0 && length(table[rfrom,tablens[rfrom]-1]) > 0) + cval = int(table[rfrom,tablens[rfrom]-1]) + } else cval = fcell[from - 9] + if(cval > -1) { # we are moving a non-empty value + if(to == 0) { # moving to a foundation + si = getsuit(cval) # get suit index + if((fnd[si]%13) == ((cval%13) - 1)) { # we can move there + fnd[si] = cval + if(from < 9) { # moving from a tableau + delete table[rfrom,tablens[rfrom]-1] # delete the card + tablens[rfrom]-- # decrease tableau length + } else fcell[from - 9] = -1 # moving from a free cell + valid = 1 # mark the move as valid + } + } else if(to < 9) { # moving to a tableau + si = table[rto,tablens[rto]-1] # target value + if((getcolor(cval) != getcolor(si) && (cval%13) == (si%13) - 1) || si == "") { + table[rto,tablens[rto]] = cval # copy the card there + tablens[rto]++ # increase tableau length + if(from < 9) { # moving from a tableau + delete table[rfrom,tablens[rfrom]-1] # delete the card + tablens[rfrom]-- # decrease tableau length + } else fcell[from - 9] = -1 # moving from a free cell + valid = 1 # mark the move as valid + } + } else if(fcell[to - 9] == -1) { # moving to a free cell + fcell[to - 9] = cval + if(from < 9) { # moving from a tableau + delete table[rfrom,tablens[rfrom]-1] # delete the card + tablens[rfrom]-- # decrease tableau length + } else fcell[rfrom - 9] = -1 # moving from a free cell + valid = 1 # mark the move as valid + } + } + } + if(valid == 1) { # the move is valid and performed + undoidx++ # increment the undo index + # save the new playfield data under it + undos["table",undoidx] = assocser(table, ",") + undos["fnd",undoidx] = assocser(fnd, ",") + undos["fcell",undoidx] = assocser(fcell, ",") + undos["tablens",undoidx] = assocser(tablens, ",") + } + else print "Invalid move!" + # now, check for the win condition: all kings in fnd + if(fnd[0] == 12 && fnd[1] == 25 && fnd[2] == 38 && fnd[3] == 51) return 2 + else return 1 # continue +} + +BEGIN { # main code part + if(SEED) fc_lstate = SEED; else { # initialize PRNG + srand(); + fc_lstate = int(rand() * 2146483648) + } + fc_deal = fc_lstate # save the deal number + asplit("A 2 3 4 5 6 7 8 9 T J Q K", cvals, " ") # card values + suits[0] = ASCII ? "c" : "\xe2\x99\xa3" # clubs + suits[1] = ASCII ? "d" : "\xe2\x99\xa6" # diamonds + suits[2] = ASCII ? "h" : "\xe2\x99\xa5" # hearts + suits[3] = ASCII ? "s" : "\xe2\x99\xa0" # spades + cstart[0] = MONOCHROME ? "" : "\x1b[31m" # red + cstart[1] = MONOCHROME ? "" : "\x1b[37m" # black (white) + cend = MONOCHROME ? "" : "\x1b[39m" # reset color + split("", usedinds) # init used indices cache + split("", cardstr) # init card strings array (exactly 3 char long each) + for(i=0;i<52;i++) { # populate it + si = getsuit(i) # suit index + cardstr[i] = cstart[getcolor(i)] sprintf("%s", cvals[i%13]) suits[si] cend " " + } + cardstr[-1] = "__ " # empty position denoted by -1 index + split("", seqarr) # init sequential array + for(i=0;i<52;i+=4) { # populate it + seqarr[i] = int(i/4) + seqarr[i+1] = seqarr[i]+13 + seqarr[i+2] = seqarr[i]+26 + seqarr[i+3] = seqarr[i]+39 + } + rowidx = 0 # deal row index + colidx = 0 # deal column index + split("", table) # init tableau array + while((slen = alen(seqarr)) > 0) { # populate it + idx = fclcg() % slen # get the index to select + table[colidx++, rowidx] = seqarr[idx] # get the selected card value + seqarr[idx] = seqarr[slen - 1] # copy the last card to the index + delete seqarr[slen - 1] # shrink the array + if(colidx > 7) {rowidx++; colidx=0} # wrap around the deal columns + } + # init freecells and foundations arrays + fcell[0] = fnd[0] = -1 + fcell[1] = fnd[1] = -1 + fcell[2] = fnd[2] = -1 + fcell[3] = fnd[3] = -1 + # init tableu length tracker array + asplit("7 7 7 7 6 6 6 6", tablens, " ") + # init undo array and index + undos["table",0] = assocser(table, ",") + undos["fnd",0] = assocser(fnd, ",") + undos["fcell",0] = assocser(fcell, ",") + undos["tablens",0] = assocser(tablens, ",") + undoidx = 0 # the current undo index is 0 + # first-time display + print "nnfc by Luxferre" + render() + printf("\n> ") + while((getline cmd) > 0) { # main interactive loop + cmd = tolower(cmd) # accept both uppercase and lowercase + if(cmd == "q") {print "Quitting..."; break} + res = domove(cmd) # pass the command to the logic function + render() + if(res == 2) {print "Victory!"; break} + printf("\n> ") + } +}