# no-nonsense FreeCell in POSIX AWK # run as: [n]awk -f nnfc.awk [-v ASCII=1] [-v MONOCHROME=1] [-v SEED=x] [-v NAH=1] # commands: q - quit, u - undo, h - help, r - restart the run # [number][number] - move from column/freecell to column/freecell/foundation: # - 0 is foundation (can be omitted) # - 1 to 8 are column numbers # - a, b, c, d are freecell numbers # bulk move/supermove operation between columns is performed automatically when possible # SEED variable can be used to init games M$-style # 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 0 && si < 3) ? 0 : 1} # main card stacking condition function stackcond(hi, lo) { return (getcolor(hi) != getcolor(lo)) && ((lo%13) == (hi%13) - 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 } function undo(i) { # undo functionality if(undoidx > 0) { for(i=0;i= 0) { assocdes(undos["table",undoidx], table, ",") assocdes(undos["fnd",undoidx], fnd, ",") assocdes(undos["fcell",undoidx], fcell, ",") assocdes(undos["tablens",undoidx], tablens, ",") } } if(undoidx < 0) undoidx = 0 } else print "Nowhere to undo!" } # main logic/move function (also handles the undo) # statuses: 0 - invalid move, 1 - continue, 2 - victory function domove(cmd, valid, nums, from, to, amt, i, si, ec, efc) { valid = 0 # invalid by default until all checks are done # if cmd is not q, h, r or u, then it must be two "digits" split(cmd, nums, "") # parse the command as a whole from = nums[1] # location from which we're moving to = nums[2] # location to which we're moving amt = 1 # the amount of cards being moved (default 1) # identify free cells if(from == "a") from = 9 if(from == "b") from = 10 if(from == "c") from = 11 if(from == "d") from = 12 if(to == "a") to = 9 if(to == "b") to = 10 if(to == "c") to = 11 if(to == "d") to = 12 from = int(from) to = int(to) rfrom = from - 1 # real from location rto = to - 1 # real to location # if moving between columns, we can move over 1 card, detect how many if(from > 0 && from < 9 && to > 0 && to < 9) { # both must be columns # calculate how many cards we can theoretically move in a stack # starting index is the last in the tableau for(i=tablens[rfrom]-1;i>1;i--) { # check the primary condition si = table[rfrom,i-1] # the previous value cval = table[rfrom,i] # the current value if(length(cval) == 0 || length(si) == 0) break # we're out of bounds # increase the amount as long as the condition is satisfied if(stackcond(si, cval)) amt++ } # calculate the max amount of cards we're allowed to move efc = 0 # count empty freecells here ec = 0 # count empty columns and final result here for(i=0;i<4;i++) if(fcell[i] == -1) efc++ for(i=0;i<8;i++) if(tablens[i] == 0) ec++ # now, calculate the max card amount to move ec = (2^ec) * (efc + 1) # base formula if(tablens[rto] == 0) ec /= 2 # halve if moving to an empty column if(amt > ec) amt = ec # hard limit the amount of moved cards to the max # now check how much of the stack actually matches the target if(tablens[rto] > 0) { # we're moving to a non-empty column efc = tablens[rfrom]-amt # index of the first stack element in the tableau si = table[rto,tablens[rto]-1] # target value for(i=efc;i 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 efc = 0 # reuse for starting index in source tableau if(from < 9) efc = tablens[rfrom] - amt if(efc > -1) { # check if we aren't moving more cards than allowed for(i=0;i 0) return 1 # always continue } function help() { print "nnfc: no-nonsense FreeCell in POSIX AWK" print "Created by Luxferre in 2024, released into public domain" print "Script parameters:" print "-v ASCII=1\tuse ASCII instead of Unicode suits" print "-v MONOCHROME=1\tdon't use colors" print "-v SEED=xxxxx\tset game seed number (M$-compatible)" print "-v NAH=1\tdisable autohoming after every move (use ah for this)" print "Commands: q - quit, u - undo, r - restart, h - this help" print "Moves: [digit][digit], where \"digits\" are:" print "- 0 is foundation (can be omitted)" print "- 1 to 8 are column (tableau) numbers" print "- a, b, c, d are freecell IDs" print "Examples:" print "7a\tmove from column 7 to freecell a" print "63\tmove from column 6 to column 3" print "d1\tmove from freecell d to column 1" print "4\tmove from column 4 to the foundation" print "c\tmove from freecell c to the foundation" 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\nenter h for command help" render() printf("\n> ") while((getline cmd) > 0) { # main interactive loop cmd = tolower(cmd) # accept both uppercase and lowercase if(cmd == "q") {print "Quitting..."; break} else if(cmd == "h") res = help() else if(cmd == "ah") res = autohome() else if(cmd == "u") undo() else if(cmd == "r") { # restart logic while(undoidx > 0) undo() undomoves = 0 } else { # pass the command to the logic function undomoves = 0 # set global undo move counter res = domove(cmd) if(res == 0) print "Invalid move!" else if(NAH != 1 && res < 2) res = autohome() print "Undomoves:", undomoves } render() if(res == 2) {print "Victory!"; break} printf("\n> ") } }