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 }