nnfc

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

nnfc.awk (13786B)


      1 # no-nonsense FreeCell in POSIX AWK
      2 # run as: [n]awk -f nnfc.awk [-v ASCII=1] [-v MONOCHROME=1] [-v SEED=x]
      3 # commands: q - quit, u - undo, h - help, r - restart the run
      4 # [number][number] - move from column/freecell to column/freecell/foundation:
      5 # - 0 is foundation (can be omitted)
      6 # - 1 to 8 are column numbers
      7 # - a, b, c, d are freecell numbers
      8 # bulk move/supermove operation between columns is performed automatically when possible
      9 # SEED variable can be used to init games M$-style
     10 # created by Luxferre in 2024, released into public domain
     11 
     12 # Helper functions
     13 
     14 function alen(a, i, k) { # determine array length
     15   k = 0
     16   for(i in a) k++
     17   return k
     18 }
     19 
     20 # split a string with separator into an array but with 0-based indexes
     21 function asplit(s, a, sep, len, i) {
     22   split(s, a, sep) # get 1-based-index array in a
     23   len = alen(a)
     24   for(i=0;i<len;i++) a[i] = a[i+1] # move everything to the left
     25   delete a[len] # delete the last element after moving
     26 }
     27 
     28 # serialize any associative array into a string
     29 # sep must be a single char not occurring in any key or value
     30 function assocser(aa, sep, outs, k) {
     31   outs = "" # initialize an ordered array string
     32   for(k in aa) # iterate over keys
     33     outs = outs sep k sep aa[k]
     34   return substr(outs, 2) # delete the first sep
     35 }
     36 
     37 # deserialize any associative array from a string
     38 # sep must be a single char used when calling assocser() function
     39 function assocdes(s, aa, sep, oa, i, len) {
     40   split("", aa) # erase aa array
     41   split(s, oa, sep) # get 1-based ordered array in oa
     42   len = alen(oa) # ordered array length
     43   for(i=1;i<=len;i+=2) # populate aa
     44     aa[oa[i]] = oa[i+1]
     45 }
     46 
     47 
     48 # get card suit index (0=clubs, 1=diamonds, 2=hearts, 3=spades)
     49 function getsuit(n) {return int(n/13)}
     50 
     51 # get card color by numeric value: 0 is red, 1 is black
     52 function getcolor(n, si) {si = getsuit(n); return (si > 0 && si < 3) ? 0 : 1}
     53 
     54 # main card stacking condition
     55 function stackcond(hi, lo) {
     56   return (getcolor(hi) != getcolor(lo)) && ((lo%13) == (hi%13) - 1)
     57 }
     58 
     59 # Traditional M$ FreeCell LCG for deal emulation
     60 function fclcg() {
     61   fc_lstate = (214013 * fc_lstate + 2531011) % 2147483648
     62   return int(fc_lstate / 65536)
     63 }
     64 
     65 # render the playfield from cardstr, table, fcell and fnd arrays
     66 function render(i, j, res) {
     67   res = ""
     68   # top left: free cells
     69   for(i=0;i<4;i++) res = res sprintf("%s", cardstr[fcell[i]])
     70   # top right: foundations
     71   for(i=0;i<4;i++) res = res sprintf("%s", cardstr[fnd[i]])
     72   res = res "\n"
     73   # delimiter
     74   for(i=0;i<23;i++) res = res "-"
     75   res = res "\n"
     76   # main table rendering 
     77   # max possible column length is 20 elements (8 + 12)
     78   for(j=0;j<20;j++) {
     79     for(i=0;i<8;i++) {
     80       if((i,j) in table)
     81         res = res cardstr[table[i,j]]
     82       else res = res "   "
     83     }
     84     res = res "\n"
     85   } 
     86   sub(/[ \t\n\r]+$/, "", res) # trim all trailing whitespace
     87   printf("\nGame #%u\n", fc_deal)
     88   print res # finally print out the resulting field
     89 }
     90 
     91 function undo(i) { # undo functionality
     92   if(undoidx > 0) {
     93     for(i=0;i<undomoves;i++) {
     94       undoidx--
     95       if(undoidx >= 0) {
     96         assocdes(undos["table",undoidx], table, ",")
     97         assocdes(undos["fnd",undoidx], fnd, ",")
     98         assocdes(undos["fcell",undoidx], fcell, ",")
     99         assocdes(undos["tablens",undoidx], tablens, ",")
    100       }
    101     }
    102     if(undoidx < 0) undoidx = 0
    103   } else print "Nowhere to undo!"
    104 }
    105 
    106 # main logic/move function (also handles the undo)
    107 # statuses: 0 - invalid move, 1 - continue, 2 - victory
    108 function domove(cmd, isauto,  valid, nums, from, to, amt, i, si, ec, efc) {
    109   valid = 0 # invalid by default until all checks are done
    110   # if cmd is not q, h, r or u, then it must be two "digits"
    111   split(cmd, nums, "") # parse the command as a whole
    112   from = nums[1] # location from which we're moving
    113   to = nums[2] # location to which we're moving
    114   if(to == "h") to = 0 # for compatibility with standard notation
    115   amt = 1 # the amount of cards being moved (default 1)
    116   # identify free cells
    117   if(from == "a") from = 9
    118   if(from == "b") from = 10
    119   if(from == "c") from = 11
    120   if(from == "d") from = 12
    121   if(to == "a") to = 9
    122   if(to == "b") to = 10
    123   if(to == "c") to = 11
    124   if(to == "d") to = 12
    125   from = int(from)
    126   to = int(to)
    127   rfrom = from - 1 # real from location
    128   rto = to - 1 # real to location
    129   # if moving between columns, we can move over 1 card, detect how many 
    130   if(from > 0 && from < 9 && to > 0 && to < 9) { # both must be columns
    131     # calculate how many cards we can theoretically move in a stack
    132     # starting index is the last in the tableau
    133     for(i=tablens[rfrom]-1;i>1;i--) { # check the primary condition
    134       si = table[rfrom,i-1] # the previous value 
    135       cval = table[rfrom,i] # the current value
    136       if(length(cval) == 0 || length(si) == 0) break # we're out of bounds
    137       # increase the amount as long as the condition is satisfied
    138       if(stackcond(si, cval)) amt++
    139     }
    140     # calculate the max amount of cards we're allowed to move
    141     efc = 0 # count empty freecells here
    142     ec = 0  # count empty columns and final result here
    143     for(i=0;i<4;i++) if(fcell[i] == -1) efc++
    144     for(i=0;i<8;i++) if(tablens[i] == 0) ec++
    145     # now, calculate the max card amount to move
    146     ec = (2^ec) * (efc + 1) # base formula
    147     if(tablens[rto] == 0) ec /= 2 # halve if moving to an empty column
    148     if(amt > ec) amt = ec # hard limit the amount of moved cards to the max
    149     # now check how much of the stack actually matches the target
    150     if(tablens[rto] > 0) { # we're moving to a non-empty column
    151       efc = tablens[rfrom]-amt # index of the first stack element in the tableau
    152       si = table[rto,tablens[rto]-1] # target value
    153       for(i=efc;i<tablens[rfrom];i++) { # iterate over the stack
    154         # subtract the amount as long as the condition is not met
    155         if(stackcond(si, table[rfrom,i])) break
    156         else amt--
    157       }
    158       if(amt < 1) amt = 1 # try moving one card in any case
    159     } 
    160   }
    161   if(from > 0 && from < 13 && to > -1 && to < 13) { # first check
    162     # determine what card we're trying to move
    163     cval = -1
    164     if(from < 9) { moving from a non-empty tableau
    165       if(tablens[rfrom] > 0 && length(table[rfrom,tablens[rfrom]-1]) > 0)
    166         cval = int(table[rfrom,tablens[rfrom]-1])
    167     } else cval = fcell[from - 9]
    168     if(cval > -1) { # we are moving a non-empty value
    169       if(to == 0) { # moving to a foundation
    170         si = getsuit(cval) # get suit index
    171         efc = cval % 13 # reuse for card rank value
    172         if((fnd[si]%13) == (efc - 1)) { # we can move there
    173           # check the automove constraints, aces and twos are always played
    174           if(isauto && efc > 1) {
    175             # check if all cards with efc-1 of the opposite color are there
    176             if(getcolor(cval) == 0) { # we're red, check black
    177               if(!(fnd[0] >= (efc - 1) && fnd[3] >= (efc - 1))) return 0
    178             } else { # we're black, check red
    179               if(!(fnd[1] >= (efc - 1) && fnd[2] >= (efc - 1))) return 0
    180             }
    181           }
    182           fnd[si] = cval 
    183           if(from < 9) { # moving from a tableau
    184             delete table[rfrom,tablens[rfrom]-1] # delete the card
    185             tablens[rfrom]-- # decrease tableau length
    186           } else fcell[from - 9] = -1 # moving from a free cell 
    187           valid = 1 # mark the move as valid
    188         }
    189       } else if(to < 9) { # moving to a tableau
    190         efc = 0 # reuse for starting index in source tableau
    191         if(from < 9) efc = tablens[rfrom] - amt
    192         if(efc > -1) { # check if we aren't moving more cards than allowed
    193           if(from < 9) { # re-read the source value if looping over a column
    194             cval = table[rfrom, efc] # checking the first stack value
    195             if(cval == "") cval = -1 # we try to read more than is available
    196             else cval = int(cval)
    197           }
    198           si = table[rto,tablens[rto]-1] # target value
    199           if(cval > -1 && (stackcond(si, cval) || tablens[rto] == 0)) {
    200             for(i=0;i<amt;i++) { # move the cards directly
    201               if(from < 9) { # moving from a tableau
    202                 table[rto,tablens[rto]] = table[rfrom,efc+i] # copy the card there
    203                 delete table[rfrom,efc+i] # delete the card
    204                 tablens[rfrom]-- # decrease tableau length
    205               } else { # moving from a free cell 
    206                 table[rto,tablens[rto]] = fcell[from - 9] # copy the card there
    207                 fcell[from - 9] = -1
    208               }
    209               tablens[rto]++ # increase tableau length
    210               valid = 1 # mark the move as valid
    211             }
    212           }
    213         }
    214       } else if(fcell[to - 9] == -1) { # moving to a free cell
    215         fcell[to - 9] = cval
    216         if(from < 9) { # moving from a tableau
    217           delete table[rfrom,tablens[rfrom]-1] # delete the card
    218           tablens[rfrom]-- # decrease tableau length
    219         } else fcell[from - 9] = -1 # moving from a free cell
    220         valid = 1 # mark the move as valid
    221       }
    222     }
    223   }
    224   if(valid == 1) { # the move is valid and performed
    225     undomoves++ # increase recent move counter
    226     undoidx++ # increment the undo index
    227     # save the new playfield data under it
    228     undos["table",undoidx] = assocser(table, ",")
    229     undos["fnd",undoidx] = assocser(fnd, ",")
    230     undos["fcell",undoidx] = assocser(fcell, ",")
    231     undos["tablens",undoidx] = assocser(tablens, ",")
    232   }
    233   # now, check for the win condition: all kings in fnd
    234   if(fnd[0] == 12 && fnd[1] == 25 && fnd[2] == 38 && fnd[3] == 51) return 2
    235   else return valid # we can use the external move status
    236 }
    237 
    238 # automatically move all eligible cards to the foundation
    239 function autohome(chres, lres, i) {
    240   do {
    241     chres = 0
    242     for(i=1;i<9;i++) {
    243       lres = domove(i "0", 1)
    244       if(lres == 2) return 2
    245       else chres += lres
    246     }
    247     lres = domove("a0", 1)
    248     if(lres == 2) return 2
    249     else chres += lres
    250     lres = domove("b0", 1)
    251     if(lres == 2) return 2
    252     else chres += lres
    253     lres = domove("c0", 1)
    254     if(lres == 2) return 2
    255     else chres += lres
    256     lres = domove("d0", 1)
    257     if(lres == 2) return 2
    258     else chres += lres
    259   } while(chres > 0)
    260   return 1 # always continue
    261 }
    262 
    263 function help() {
    264   print "nnfc: no-nonsense FreeCell in POSIX AWK"
    265   print "Created by Luxferre in 2024, released into public domain"
    266   print "Script parameters:"
    267   print "-v ASCII=1\tuse ASCII instead of Unicode suits"
    268   print "-v MONOCHROME=1\tdon't use colors"
    269   print "-v SEED=xxxxx\tset game seed number (M$-compatible)"
    270   print "Commands: q - quit, u - undo, r - restart, h -  this help"
    271   print "Moves: [digit][digit], where \"digits\" are:"
    272   print "- 0 is foundation (can be omitted)"
    273   print "- 1 to 8 are column (tableau) numbers"
    274   print "- a, b, c, d are freecell IDs"
    275   print "Examples:"
    276   print "7a\tmove from column 7 to freecell a"
    277   print "63\tmove from column 6 to column 3"
    278   print "d1\tmove from freecell d to column 1"
    279   print "4\tmove from column 4 to the foundation"
    280   print "c\tmove from freecell c to the foundation"
    281   return 1 # continue
    282 }
    283 
    284 BEGIN { # main code part
    285   if(SEED) fc_lstate = SEED; else { # initialize PRNG
    286     srand();
    287     fc_lstate = int(rand() * 2146483648)
    288   }
    289   fc_deal = fc_lstate # save the deal number
    290   asplit("A 2 3 4 5 6 7 8 9 T J Q K", cvals, " ") # card values
    291   suits[0] = ASCII ? "c" : "\342\231\243" # clubs
    292   suits[1] = ASCII ? "d" : "\342\231\246" # diamonds
    293   suits[2] = ASCII ? "h" : "\342\231\245" # hearts
    294   suits[3] = ASCII ? "s" : "\342\231\240" # spades
    295   cstart[0] = MONOCHROME ? "" : "\033[31m" # red
    296   cstart[1] = MONOCHROME ? "" : "\033[37m" # black (white)
    297   cend = MONOCHROME ? "" : "\033[39m" # reset color
    298   split("", usedinds) # init used indices cache
    299   split("", cardstr) # init card strings array (exactly 3 char long each)
    300   for(i=0;i<52;i++) { # populate it
    301     si = getsuit(i) # suit index
    302     cardstr[i] = cstart[getcolor(i)] sprintf("%s", cvals[i%13]) suits[si] cend " "
    303   }
    304   cardstr[-1] = "__ " # empty position denoted by -1 index
    305   split("", seqarr) # init sequential array
    306   for(i=0;i<52;i+=4) { # populate it
    307     seqarr[i] = int(i/4)
    308     seqarr[i+1] = seqarr[i]+13
    309     seqarr[i+2] = seqarr[i]+26
    310     seqarr[i+3] = seqarr[i]+39
    311   }
    312   rowidx = 0 # deal row index
    313   colidx = 0 # deal column index
    314   split("", table) # init tableau array
    315   while((slen = alen(seqarr)) > 0) { # populate it
    316     idx = fclcg() % slen   # get the index to select
    317     table[colidx++, rowidx] = seqarr[idx] # get the selected card value
    318     seqarr[idx] = seqarr[slen - 1] # copy the last card to the index
    319     delete seqarr[slen - 1] # shrink the array
    320     if(colidx > 7) {rowidx++; colidx=0} # wrap around the deal columns
    321   }
    322   # init freecells and foundations arrays
    323   fcell[0] = fnd[0] = -1
    324   fcell[1] = fnd[1] = -1
    325   fcell[2] = fnd[2] = -1
    326   fcell[3] = fnd[3] = -1
    327   # init tableu length tracker array
    328   asplit("7 7 7 7 6 6 6 6", tablens, " ")
    329   # init undo array and index
    330   undos["table",0] = assocser(table, ",")
    331   undos["fnd",0] = assocser(fnd, ",")
    332   undos["fcell",0] = assocser(fcell, ",")
    333   undos["tablens",0] = assocser(tablens, ",")
    334   undoidx = 0 # the current undo index is 0
    335   # first-time display
    336   print "nnfc by Luxferre\nenter h for command help"
    337   render()
    338   printf("\n> ")
    339   while((getline cmd) > 0) { # main interactive loop
    340     cmd = tolower(cmd) # accept both uppercase and lowercase
    341     if(cmd == "q") {print "Quitting..."; break}
    342     else if(cmd == "h")  res = help()
    343     else if(cmd == "u")  undo()
    344     else if(cmd == "r") { # restart logic
    345       while(undoidx > 0) undo()
    346       undomoves = 0
    347     } else { # pass the command to the logic function
    348       undomoves = 0 # set global undo move counter
    349       res = domove(cmd, 0) # manual move 
    350       if(res == 0) print "Invalid move!"
    351       else if(res < 2) res = autohome()
    352     }
    353     render()
    354     if(res == 2) {print "Victory!"; break}
    355     printf("\n> ")
    356   }
    357 }