Advertisement
FredMSloniker

Kalah.lua

Aug 23rd, 2014
482
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 27.27 KB | None | 0 0
  1. ------------------------------------------------------------------------------
  2. -- kalah.lua                                                                --
  3. -- An implementation of Kalah (the variation of mancala most familiar to    --
  4. -- Americans) in OpenComputers. Supports one or two players, with an        --
  5. -- AI opponent of varying difficulty.                                       --
  6. ------------------------------------------------------------------------------
  7.  
  8. local component = require("component")
  9. local event = require("event")
  10. local keyboard = require("keyboard")
  11. local term = require("term")
  12. local keys = keyboard.keys
  13. local gpu = component.gpu
  14. local screenWidth, screenHeight = gpu.getResolution()
  15.  
  16. -- Make sure we have enough screen real estate.
  17. local originalScreenWidth, originalScreenHeight = screenWidth, screenHeight
  18. do
  19.   if (screenWidth < 50) or (screenHeight < 16) then
  20.     -- The screen's too small!
  21.     local maxWidth, maxHeight = gpu.maxResolution()
  22.     if (maxWidth < 50) or (maxHeight < 16) then
  23.       -- Is this a robot or something?
  24.       print("Kalah requires a minimum screen resolution of 50 x 16. Upgrade \z
  25.            your monitor and graphics card and try again.")
  26.       os.exit()
  27.     else
  28.       if (maxWidth >= 80) and (maxHeight >=25) then
  29.         -- We can run at tier 2 resolution.
  30.         screenWidth, screenHeight = 80, 25
  31.       else
  32.         screenWidth, screenHeight = 50, 16
  33.       end
  34.       gpu.setResolution(screenWidth, screenHeight)
  35.     end
  36.   end
  37. end
  38.  
  39. local players = 0 -- So this can be accessed by functions.
  40. local totalSeeds = 0
  41.  
  42. -- A collection of useful strings for various players.
  43. local usefulStrings = {}
  44. usefulStrings.who = {"Player 1", "Player 2", "I"}
  45. usefulStrings.has = {"has", "has", "have"}
  46. usefulStrings.is = {"is", "is", "am"}
  47. usefulStrings.s = {"s", "s", ""}
  48.  
  49. -- And a way of accessing those strings.
  50. local function usefulString(player, desiredString)
  51.   local stringIndex = player
  52.   if (players == 1) and (player == 2) then stringIndex = 3 end
  53.   return usefulStrings[desiredString][stringIndex]
  54. end
  55.  
  56. -- Properly pluralize numbers.
  57. local function pluralize(number)
  58.   if number ~= 1 then
  59.     return "s"
  60.   else
  61.     return ""
  62.   end
  63. end
  64.  
  65. -- Find the sign of a number.
  66. local function sign(input)
  67.   if input ~= 0 then
  68.     return input / math.abs(input)
  69.   else
  70.     return 0
  71.   end
  72. end
  73.  
  74. -- Some comments the computer can make on moves.
  75. local colorCommentary = {}
  76. colorCommentary[1] = {"I think you're going to regret that.",
  77.                      "Are you sure? Well, okay...",
  78.                      "Heh heh heh heh.",
  79.                      "You're not very good at this game, are you?",
  80.                      "Ooooh, that's going to suck for you."}
  81. colorCommentary[2] = {"Hm. Not bad.",
  82.                      "You could have done better than that.",
  83.                      "Interesting.",
  84.                      "Well, it's not the worst you could have done.",
  85.                      "Let's see how that plays out, shall we?"}
  86. colorCommentary[3] = {"Oh, nice!",
  87.                      "That's a pretty good move.",
  88.                      "Yeah, I think I would have done that too.",
  89.                      "You've been practicing, haven't you?",
  90.                      "I'm impressed!"}
  91.  
  92. -- Print a string, horizontally centered.
  93. local function centerPrint(stringToPad)
  94.   local paddingNeeded = math.floor((screenWidth - stringToPad:len()) / 2)
  95.   print(string.rep(" ", paddingNeeded) .. stringToPad)
  96. end
  97.  
  98. -- cleanly exit the program, restoring the original resolution.
  99. local function allDone()
  100.   gpu.setResolution(originalScreenWidth, originalScreenHeight)
  101.   term.clear()
  102.   print("Okay, see you next time!")
  103.   os.exit()
  104. end
  105.  
  106. -- Wait for a non-modifier keypress, optionally on a list of acceptable
  107. -- keypresses, and return it. Additionally, make a note of who pushed the
  108. -- key.
  109. local function getKeyPress(requiredKey)
  110.   local char, lastTouched
  111.   repeat
  112.     _, _, char, _, lastTouched = event.pull("key_down")
  113.   until (char ~= nil) and ((requiredKey == nil) or
  114.         (requiredKey:find(string.char(char)) ~= nil))
  115.   return string.char(char), lastTouched
  116. end
  117.  
  118. -- As above, but if the user presses 'q', the program immediately ends.
  119. local function getKeyPressQuit(requiredKey)
  120.   local char, lastTouched
  121.   if requiredKey ~= nil then
  122.     char, lastTouched = getKeyPress(requiredKey .. "q")
  123.   else
  124.     char, lastTouched = getKeyPress()
  125.   end
  126.   if char == "q" then allDone() end
  127.   return char, lastTouched
  128. end
  129.  
  130. -- Neatly format the number in a pit.
  131. local function pitString(pit)
  132.   return string.format("%2d ", pit)
  133. end
  134.  
  135. -- neatly format a fancy number in a pit.
  136. local fancyPit = {}
  137. do
  138.   local maximumStones = 8 * 12
  139.   for i = 0, 4 do fancyPit[i] = {} end
  140.   for i = 0, maximumStones do fancyPit[0][i] = "╔═════╗" end
  141.   for i = 0, maximumStones do fancyPit[4][i] = "╚═════╝" end
  142.  
  143.   fancyPit[1][0] = "║┌───┐║"
  144.   fancyPit[2][0] = "║│ 0 │║"
  145.   fancyPit[3][0] = "║└───┘║"
  146.  
  147.   fancyPit[1][1] = "║     ║"
  148.   fancyPit[2][1] = "║  ∙  ║"
  149.   fancyPit[3][1] = "║     ║"
  150.  
  151.   fancyPit[1][2] = "║     ║"
  152.   fancyPit[2][2] = "║ ∙ ∙ ║"
  153.   fancyPit[3][2] = "║     ║"
  154.  
  155.   fancyPit[1][3] = "║  ∙  ║"
  156.   fancyPit[2][3] = "║     ║"
  157.   fancyPit[3][3] = "║∙   ∙║"
  158.  
  159.   fancyPit[1][4] = "║∙   ∙║"
  160.   fancyPit[2][4] = "║     ║"
  161.   fancyPit[3][4] = "║∙   ∙║"
  162.  
  163.   fancyPit[1][5] = "║∙   ∙║"
  164.   fancyPit[2][5] = "║  ∙  ║"
  165.   fancyPit[3][5] = "║∙   ∙║"
  166.  
  167.   fancyPit[1][6] = "║ ∙ ∙ ║"
  168.   fancyPit[2][6] = "║ ∙ ∙ ║"
  169.   fancyPit[3][6] = "║ ∙ ∙ ║"
  170.  
  171.   fancyPit[1][7] = "║ ∙ ∙ ║"
  172.   fancyPit[2][7] = "║∙ ∙ ∙║"
  173.   fancyPit[3][7] = "║ ∙ ∙ ║"
  174.  
  175.   fancyPit[1][8] = "║∙ ∙ ∙║"
  176.   fancyPit[2][8] = "║ ∙ ∙ ║"
  177.   fancyPit[3][8] = "║∙ ∙ ∙║"
  178.  
  179.   fancyPit[1][9] = "║∙ ∙ ∙║"
  180.   fancyPit[2][9] = "║∙ ∙ ∙║"
  181.   fancyPit[3][9] = "║∙ ∙ ∙║"
  182.  
  183.   for i = 10, 8 * 12 do
  184.     fancyPit[1][i] = "║┌───┐║"
  185.     fancyPit[2][i] = "║│" .. string.sub(i, 1, 1) .. " " ..
  186.                      string.sub(i, 2, 2) .. "│║"
  187.     fancyPit[3][i] = "║└───┘║"
  188.   end
  189. end
  190.  
  191. -- Make a table containing the current state of the board, neatly centered.
  192. local function boardTable(board)
  193.   local output = {}
  194.   local boardWidth
  195.   if screenWidth < 64 then -- The plain-style board.
  196.     boardWidth = 3 * 8
  197.     table.insert(output, ("    L  K  J  I  H  G"))
  198.     local tempString = "   "
  199.     for i = 13, 8, -1 do tempString = tempString .. pitString(board[i]) end
  200.     table.insert(output, tempString)
  201.     table.insert(output, pitString(board[14]) .. string.rep(" ", 3 * 6) ..
  202.                  pitString(board[7]))
  203.     table.insert(output, "P2 " .. string.rep(" ", 3 * 6) .. "P1")
  204.     tempString = "   "
  205.     for i = 1, 6 do tempString = tempString .. pitString(board[i]) end
  206.     table.insert(output, tempString)
  207.     table.insert(output, "    A  B  C  D  E  F")
  208.   else -- The fancy-style board.
  209.     boardWidth = 64
  210.     table.insert(output,
  211.                  "           L       K       J       I       H       G")
  212.     local tempString
  213.     for i = 0, 4 do
  214.       tempString = "        "
  215.       for j = 13, 8, -1 do
  216.         tempString = tempString .. fancyPit[i][board[j]] .. " "
  217.       end
  218.       table.insert(output, tempString)
  219.     end
  220.     table.insert(output, "")
  221.     for i = 0, 4 do
  222.       table.insert(output, fancyPit[i][board[14]] .. string.rep(" ",
  223.                    8 * 6 + 1) .. fancyPit[i][board[7]])
  224.     end
  225.     table.insert(output, "  P 2   " .. string.rep(" ", 8 * 6) .. "  P 1")
  226.     for i = 0, 4 do
  227.       tempString = "        "
  228.       for j = 1, 6 do
  229.         tempString = tempString ..fancyPit[i][board[j]] .. " "
  230.       end
  231.       table.insert(output, tempString)
  232.     end
  233.     table.insert(output,
  234.                  "           A       B       C       D       E       F")
  235.   end
  236.   -- Now center the board.
  237.   local centerPadding = string.rep(" ",
  238.                         math.floor((screenWidth - boardWidth) / 2))
  239.   for i, j in pairs(output) do
  240.     output[i] = centerPadding .. j
  241.   end
  242.   return output
  243. end
  244.  
  245. -- Print the board.
  246. local function printBoard(board)
  247.   print()
  248.   for i, j in pairs(boardTable(board)) do print(j) end
  249.   print()
  250. end
  251.  
  252. -- Given a string and width, return a table of strings that wrap the string to
  253. -- the width.
  254. local function wrapText(textToWrap, width)
  255.   local outputList = {}
  256.   while textToWrap ~= "" do
  257.     local nextNewLine = textToWrap:find("\n")
  258.     if (nextNewLine ~= nil) and (nextNewLine <= width) then
  259.       -- There's an explicit newline in the next width characters.
  260.       table.insert(outputList, textToWrap:sub(1, nextNewLine - 1))
  261.       textToWrap = textToWrap:sub(nextNewLine + 1)
  262.     else
  263.       if string.len(textToWrap) <= width then -- Last line.
  264.         table.insert(outputList, textToWrap)
  265.         textToWrap = ""
  266.       else
  267.         local lastSpace = textToWrap:sub(1, width):reverse():find(" ")
  268.         if lastSpace == nil then
  269.           -- The next word is longer than width characters, so we'll break it.
  270.           table.insert(outputList, textToWrap:sub(1, width))
  271.           textToWrap = textToWrap:sub(width + 1)
  272.         else
  273.           -- We'll break the line at the last space in width characters.
  274.           table.insert(outputList, textToWrap:sub(1,width - lastSpace))
  275.           textToWrap = textToWrap:sub(width - lastSpace + 2)
  276.         end
  277.       end
  278.     end
  279.   end
  280.   return outputList
  281. end
  282.  
  283. -- Print the instructions for Kalah, allowing forward and backward scrolling.
  284. local function printInstructions()
  285.   local instructions1 = "(Press space or Enter to read more, backspace \z
  286.    to read less, and Q to quit reading.)\n\nKalah is a game in \z
  287.    the mancala family invented by William Julius Champion Jr. in 1940. It \z
  288.    is the most familiar version of mancala to American audiences, and \z
  289.    indeed most Americans would call it simply 'mancala'.\n\nKalah is \z
  290.    played on a board with two rows, one for each player, each of which has \z
  291.    six small pits, called houses. A large pit, called a kalah or store, is \z
  292.    at each end of the board, one for each player.\n\nAt the beginning of \z
  293.    each game, each of the twelve houses is filled with a number of seeds \z
  294.    (usually three or four each). On your turn, you choose one of your \z
  295.    houses that has seeds in it and 'sow' the seeds. To do so, you remove \z
  296.    the seeds from that house, then go around the board counter-clockwise, \z
  297.    depositing one seed in each pit along the way. (You skip over your \z
  298.    opponent's store, which is on your left, but not your own store, on the \z
  299.    right.)\n\nIf the last seed you sow lands in your store, you get an \z
  300.    extra turn. There is no limit to how many extra turns you can get this \z
  301.    way.\n\nIf the last seed you sow lands in one of your own empty houses, \z
  302.    and your opponent has seeds in the house opposite that one, both your \z
  303.    last seed and all of his seeds in that house are captured and placed in \z
  304.    your store.\n\nWhen someone no longer has any seeds in any of his \z
  305.    houses, the game ends. The other player moves all of his remaining \z
  306.    seeds to his store, and the player with the most seeds in his store \z
  307.    wins. The game also ends if one player gets more than half the seeds in \z
  308.    his store, in which case he wins.\n\nIn a moment, I'll ask you if you \z
  309.    want to play with one or two players. (I'll be your opponent if you \z
  310.    choose a single-player game; if you play two-player, I'll spectate.) \z
  311.    I'll also ask how smart you want me to be, which will affect both my \z
  312.    own play and any comments I offer on yours. (Fair warning: I take up to \z
  313.    six times longer to ponder the board between moves for each increase in \z
  314.    intelligence. If it's looking like I won't finish my move this century, \z
  315.    press 'Q' to quit and try again at a lower difficulty.) Then I'll \z
  316.    ask how chatty you want me to be; I enjoy commenting on moves, but you \z
  317.    can shut me up if you like. Last, I'll ask how many seeds you want in \z
  318.    each house (up to 8).\n\nThe game board looks like this:"
  319.   local instructions2 = "\nOn your turn, simply enter the letter of the \z
  320.    house you want to move seeds from. The first player is at the bottom of \z
  321.    the board (A-F); the second player (who would be me if you choose not \z
  322.    to have a second living player) is at the top (G-L).\n\nTwo final \z
  323.    details. First, the first player has a distinct advantage in Kalah. \z
  324.    I'll always let you go first (I'm nice that way), but if you want to \z
  325.    make the game more fair when you're playing someone else, you can use \z
  326.    the 'pie rule'. That means that, after the first player's first move, \z
  327.    the second player can decide to either continue playing or switch \z
  328.    places with the first player (whose turn it will then be as the second \z
  329.    player). It's up to you if you want to do that.\n\nSecond, if you want \z
  330.    to quit the game at any time, type 'Q' for your move.\n\nI hope you \z
  331.    have fun!"
  332.   local instructions = wrapText(instructions1, screenWidth)
  333.   table.insert(instructions, "")
  334.   for i, j in pairs(boardTable({3, 3, 3, 3, 3, 3, 0, 3, 3, 3, 3, 3, 3, 0})) do
  335.     table.insert(instructions, j)
  336.   end
  337.   for i, j in pairs(wrapText(instructions2, screenWidth)) do
  338.     table.insert(instructions, j)
  339.   end
  340.   local line = 1
  341.   repeat
  342.     term.setCursor(1, 1)
  343.     for i = line, math.min(line + screenHeight - 2, #instructions) do
  344.       term.clearLine()
  345.       print(instructions[i])
  346.     end
  347.     for i = #instructions + 1, screenHeight - 1 do
  348.       term.clearLine()
  349.       print()
  350.     end
  351.     if math.min(line + screenHeight - 2, #instructions) ~= #instructions then
  352.       term.write("--more--")
  353.     else
  354.       term.write("--done--")
  355.     end
  356.     local key = getKeyPress(" \b\rq")
  357.     if (key == " ") or (key == "\r") then
  358.       if math.min(line + screenHeight - 2, #instructions) ==
  359.          #instructions then -- We're done.
  360.         line = #instructions
  361.       else
  362.         local forwardMove = line + screenHeight - 1
  363.         if key == "\r" then forwardMove = line + 1 end
  364.         line = math.min(forwardMove, #instructions - screenHeight + 2)
  365.       end
  366.     end
  367.     if key == "\b" then line = math.max(1, line - screenHeight + 1) end
  368.     if key == "q" then line = #instructions end
  369.     term.clearLine()
  370.   until line == #instructions
  371. end
  372.  
  373. -- Clone a table.
  374. local function tableCopy(inputTable)
  375.   local outputTable = {}
  376.   for i, v in pairs(inputTable) do outputTable[i] = v end
  377.   return outputTable
  378. end
  379.  
  380. -- A map of move keystrokes to pits and vice versa.
  381. local pitLookup = {"A", "B", "C", "D", "E", "F", nil,
  382.                    "G", "H", "I", "J", "K", "L"}
  383. pitLookup.a = 1
  384. pitLookup.b = 2
  385. pitLookup.c = 3
  386. pitLookup.d = 4
  387. pitLookup.e = 5
  388. pitLookup.f = 6
  389. pitLookup.g = 8 -- Skipping a scoring pit.
  390. pitLookup.h = 9
  391. pitLookup.i = 10
  392. pitLookup.j = 11
  393. pitLookup.k = 12
  394. pitLookup.l = 13
  395.  
  396. -- Given a board, a player, and a move, make the move and return the new
  397. -- board, the new player, and a message about the move.
  398. local function makeMove(board, player, move)
  399.   local nextPlayer
  400.   local newBoard = tableCopy(board)
  401.   local seeds = newBoard[move]
  402.   local message = usefulString(player, "who") .. " move" ..
  403.                   usefulString(player, "s") .. " " .. seeds .. " seed"
  404.   if seeds > 1 then message = message .. "s" end
  405.   message = message .. ", starting at house " .. pitLookup[move]
  406.   newBoard[move] = 0
  407.   for i = move + 1, move + seeds do -- Sowing the seeds.
  408.     local j = (i - 1) % 14 + 1
  409.     newBoard[j] = newBoard[j] + 1
  410.   end
  411.   local lastSeed = (move + seeds - 1) % 14 + 1
  412.   if ((lastSeed == 7) and (player == 1)) or
  413.      ((lastSeed == 14) and (player == 2)) then -- Player gets to go again.
  414.     message = message .. ", and get" .. usefulString(player, "s") ..
  415.               " to go again."
  416.     nextPlayer = player
  417.   elseif (newBoard[lastSeed] == 1) and (lastSeed > (player - 1) * 7) and
  418.          (lastSeed < player * 7) and
  419.          (newBoard[14 - lastSeed] > 0) then
  420.     -- Player has captured seeds.
  421.     local capturedPit = 14 - lastSeed
  422.     local capturedSeeds = newBoard[capturedPit]
  423.     message = message .. ", capturing " .. capturedSeeds .. " seed"
  424.     if capturedSeeds > 1 then message = message .. "s" end
  425.     message = message .. " from pit " .. pitLookup[capturedPit] .. "."
  426.     newBoard[player * 7] = newBoard[player * 7] + capturedSeeds + 1
  427.     newBoard[capturedPit] = 0
  428.     newBoard[lastSeed] = 0
  429.     nextPlayer = 3 - player
  430.   else -- Boring end of turn.
  431.     message = message .. "."
  432.     nextPlayer = 3 - player
  433.   end
  434.   -- Is the game over at this point?
  435.   for i = 1, 2 do
  436.     if newBoard[i * 7] > totalSeeds / 2 then
  437.       message = message .. "\n" .. usefulString(i, "who") .. " " ..
  438.                 usefulString(i, "has") ..
  439.                 " more than half of the seeds."
  440.       nextPlayer = 0
  441.     else
  442.       local remainingStones = 0
  443.       for j = 1, 6 do
  444.         remainingStones = remainingStones + newBoard[(i - 1) * 7 + j]
  445.       end
  446.       if remainingStones == 0 then
  447.         message = message .. "\n" .. usefulString(i, "who") ..
  448.                   " " .. usefulString(i, "is") .. " out of seeds. \n" ..
  449.                   usefulString(3 - i, "who") .. " collects all remaining \z
  450.                  seeds."
  451.         for j = 1, 6 do
  452.           newBoard[(3 - i) * 7] = newBoard[(3 - i) * 7] +
  453.                                   newBoard[(2 - i) * 7 + j]
  454.           newBoard[(2 - i) * 7 + j] = 0
  455.         end
  456.         nextPlayer = 0
  457.       end
  458.     end
  459.   end
  460.   return newBoard, nextPlayer, message
  461. end
  462.  
  463. -- Give the player something to watch, and give the player a chance to end the
  464. -- game, while the computer ponders.
  465. local thinking = "Thinking..."
  466. local function itsThinking(percentComplete)
  467.   local _, _, abortButton = event.pull(0, "key_down")
  468.   if abortButton == string.byte("q") then allDone() end
  469.   thinking = string.sub(thinking, 2) .. string.sub(thinking, 1, 1)
  470.   term.clearLine()
  471.   term.write(thinking .. string.format(" %2.2f%%", percentComplete * 100))
  472. end
  473.  
  474. -- Consider the state of the board and return a table showing each
  475. -- possible move and the score for said move.
  476. local function evaluateBoard(board, player, intelligence, percentComplete,
  477.                              percentWidth)
  478.   local moveTable = {}
  479.   local numberOfMoves = 0
  480.   for i = (player - 1) * 7 + 1, (player - 1) * 7 + 6 do
  481.     if board[i] > 0 then numberOfMoves = numberOfMoves + 1 end
  482.   end
  483.   local moveNumber = -1
  484.   for i = (player - 1) * 7 + 1, (player - 1) * 7 + 6 do
  485.     if board[i] > 0 then -- A legal move.
  486.       moveNumber = moveNumber + 1
  487.       -- What's this move worth?
  488.       local nextBoard, nextPlayer, nextMessage =
  489.         makeMove(board, player, i)
  490.       moveTable[i] = nextBoard[7] - nextBoard[14]
  491.       if nextPlayer == 0 then -- This move ends the game.
  492.         if moveTable[i] == 0 then
  493.           moveTable[i] = 10000 -- Tie game.
  494.         else
  495.           moveTable[i] = 1100 * sign(moveTable[i]) -- 100% chance of win/loss.
  496.         end
  497.       else -- This move doesn't end the game.
  498.         if intelligence > 1 then -- We have to go deeper.
  499.           -- But don't lock up the computer.
  500.           local newPercentComplete = percentComplete + percentWidth *
  501.                                      moveNumber / numberOfMoves
  502.           itsThinking(newPercentComplete)
  503.           moveTable[i] = evaluateBoard(nextBoard, nextPlayer,
  504.                          intelligence - 1, newPercentComplete,
  505.                          percentWidth / numberOfMoves)
  506.           if nextPlayer ~= 0 then -- moveTable[i] will be a table.
  507.             -- Assume the next player will make the best of all possible
  508.             -- moves UNLESS that move would cost the current player the game.
  509.             -- In which case assume he plays well, but not perfect;
  510.             -- if there's one winning move, give him an 80% chance of picking
  511.             -- it, up to 100% if every move is a winning move. (This will keep
  512.             -- the computer from just giving up in a bad situation.)
  513.             local gameLosingMoves, totalMoves = 0, 0
  514.             for i, j in pairs(moveTable[i]) do totalMoves = totalMoves + 1 end
  515.             local likelyNextMove, likelyNextMoveScore
  516.             for i, j in pairs(moveTable[i]) do
  517.               if ((j >= 1000) and (player == 2) and (j ~= 10000)) or
  518.                  ((j <= -1000) and (player == 1)) then
  519.                 gameLosingMoves = gameLosingMoves + 1
  520.               else
  521.                 if likelyNextMoveScore == nil then
  522.                   likelyNextMove, likelyNextMoveScore = i, j
  523.                 else
  524.                   if ((j > likelyNextMoveScore) and (player == 1)) or
  525.                      ((j < likelyNextMoveScore) and (player == 2)) or
  526.                      ((j == likelyNextMoveScore) and (math.random(2) == 2))
  527.                   then
  528.                     likelyNextMove, likelyNextMoveScore = i, j
  529.                   end
  530.                 end
  531.               end
  532.             end
  533.             -- Did we find any losing moves?
  534.             if gameLosingMoves ~= 0 then -- Yes.
  535.               local losingOdds
  536.               if gameLosingMoves == totalMoves then
  537.                 losingOdds = 100
  538.               else
  539.                 losingOdds = 100 * (1 - 0.2 ^ gameLosingMoves)
  540.               end
  541.               likelyNextMoveScore = sign(player - 0.5) * (1000 + losingOdds)
  542.             end
  543.             -- We found the next player's move score, so...
  544.             moveTable[i] = likelyNextMoveScore
  545.           end
  546.         end
  547.       end
  548.     end
  549.   end
  550.   return moveTable
  551. end
  552.  
  553. -- The main program.
  554. term.clear()
  555. centerPrint("Kalah")
  556. centerPrint("by Fred M. Sloniker")
  557. print()
  558. term.write("Do you want instructions? (Y/N/Q) ", true)
  559. local choice = getKeyPressQuit("yn")
  560. print(string.upper(choice))
  561. print()
  562. if choice == "y" then
  563.   printInstructions()
  564.   print()
  565. end
  566. local anotherGame = ""
  567. repeat
  568.   term.write("How many players? (1-2) ", true)
  569.   players = getKeyPressQuit("12")
  570.   print(players)
  571.   term.write("How smart should I be? (1-9) ", true)
  572.   local intelligence = getKeyPressQuit("123456789")
  573.   print(intelligence)
  574.   term.write("How chatty should I be? (0-9) ", true)
  575.   local chattiness = getKeyPressQuit("0123456789")
  576.   print(chattiness)
  577.   term.write("How many seeds in each house? (1-8) ", true)
  578.   local seedsPerPit, lastTouched = getKeyPressQuit("12345678")
  579.   print(seedsPerPit)
  580.   players, intelligence, seedsPerPit, chattiness = tonumber(players),
  581.                                                    tonumber(intelligence),
  582.                                                    tonumber(seedsPerPit),
  583.                                                    tonumber(chattiness)
  584.   totalSeeds = seedsPerPit * 12
  585.   usefulStrings.who[1] = lastTouched
  586.   print("Okay!  Let me get things started...")
  587.   local board = {seedsPerPit, seedsPerPit, seedsPerPit, seedsPerPit,
  588.                  seedsPerPit, seedsPerPit, 0,
  589.                  seedsPerPit, seedsPerPit, seedsPerPit, seedsPerPit,
  590.                  seedsPerPit, seedsPerPit, 0}
  591.   local gameOver = false
  592.   local player = 1
  593.   repeat
  594.     printBoard(board)
  595.     if player == 0 then -- The game is over.
  596.       print("\nThe game is over.")
  597.       gameOver = true
  598.       for i = 1, 2 do
  599.         print(usefulString(i, "who") .. " score" .. usefulString(i, "s") ..
  600.               " " .. board[(i - 1) * 7 + 7] .. " point" ..
  601.               pluralize(point) .. ".")
  602.       end
  603.       if board[7] > board[14] then
  604.         print(usefulString(1, "who") .. " wins! Huzzah!")
  605.       elseif board[7] < board [14] then
  606.         if players == 1 then
  607.           print("I win! Better luck next time.")
  608.         else
  609.           print(usefulString(2, "who") .. " wins! Huzzah!")
  610.         end
  611.       else
  612.         term.write("It's a tie! ")
  613.         if players == 1 then
  614.           print("I'll get you next time.")
  615.         else
  616.           print("Better luck next time.")
  617.         end
  618.       end
  619.     else -- The player does have a valid move.
  620.       -- What does the computer think of the situation?
  621.       local moveScores = evaluateBoard(board, player, intelligence, 0, 1)
  622.       term.clearLine() -- Get rid of thinking glyph.
  623.       if (player == 2) and (players == 1) then -- Computer's turn.
  624.         print("My turn.")
  625.         -- Find the best move from the scores.
  626.         local bestMove, bestScore = next(moveScores, nil)
  627.         for i, thisScore in pairs(moveScores) do
  628.           if thisScore < bestScore then -- Low numbers are good in this case.
  629.             bestMove, bestScore = i, thisScore
  630.           end
  631.         end
  632.         local message = ""
  633.         board, player, message = makeMove(board, player, bestMove)
  634.         print(message)
  635.         if player == 2 then -- The computer gets another turn, so wait.
  636.           print("(Press a key to continue.)")
  637.           local _ = getKeyPressQuit()
  638.         end
  639.       else -- Player's turn.
  640.         local playerMove = ""
  641.         repeat
  642.           local legalMove = true
  643.           term.write(usefulString(player, "who") .. "'s move? (", true)
  644.           if player == 1 then print("A-F)") else print("G-L)") end
  645.           if player == 1 then
  646.             playerMove, lastTouched = getKeyPressQuit("abcdef0")
  647.           else
  648.             playerMove, lastTouched = getKeyPressQuit("ghijkl0")
  649.           end
  650.           usefulStrings.who[player] = lastTouched
  651.           if playerMove == "0" then
  652.             term.write("Cheater! Fine, here's what I think. ", true)
  653.             for i, j in pairs(moveScores) do
  654.               term.write(pitLookup[i] .. ": " .. j, true)
  655.               if next(moveScores, i) ~= nil then term.write("; ", true) end
  656.             end
  657.             print()
  658.             if player == 1 then
  659.               term.write("Higher")
  660.             else
  661.               term.write("Lower")
  662.             end
  663.             print(" numbers are better for you.")
  664.             legalMove = false
  665.           end
  666.           if board[pitLookup[playerMove]] == 0 then
  667.             print("Illegal move: no stones in that pit.")
  668.             legalMove = false
  669.           end
  670.           if not legalMove then printBoard(board) end
  671.         until legalMove
  672.         local message = ""
  673.         -- What does the computer think about that move?
  674.         local _, minScore = next(moveScores, nil)
  675.         local maxScore = minScore
  676.         for i, thisScore in pairs(moveScores) do
  677.           if thisScore < minScore then minScore = thisScore end
  678.           if thisScore > maxScore then maxScore = thisScore end
  679.         end
  680.         local rating = 2
  681.         if minScore ~= maxScore then
  682.           rating = math.min(math.floor((moveScores[pitLookup[playerMove]] -
  683.                    minScore) / (maxScore - minScore) * 3), 2) + 1
  684.         end
  685.         -- For player 2, lower ratings are better.
  686.         if player == 2 then rating = 4 - rating end
  687.         if math.random(0, 8) < chattiness then
  688.           print(colorCommentary[rating][math.random(5)])
  689.         end
  690.         board, player, message = makeMove(board, player,
  691.                                           pitLookup[playerMove])
  692.         print(message)
  693.       end
  694.     end
  695.   until gameOver
  696.   print()
  697.   term.write("Would you like to play again? (Y/N) ", true)
  698.   anotherGame = getKeyPressQuit("yn")
  699.   print(string.upper(anotherGame))
  700. until anotherGame == "n"
  701. allDone()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement