Advertisement
IAmCiel_

ComputerCraft Fallout 4 Hacking mini-game with Matrix BG

Mar 1st, 2025 (edited)
251
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. -- Fallout 4 like hacking minigame, displays a list of words, one of which is the correct password.
  2. -- The player has 5 attempts to guess the correct password.
  3. -- The player can select a word by clicking on it twice, which will display the likeness of the selected word to the password.
  4. -- If the player selects the correct password, the game will display "Access Granted" and reboot the computer.
  5. -- If the player runs out of attempts, the game will display "LOCKED" and reboot the computer.
  6. -- Works best with a 4x2 monitor.
  7. -- Built for ComputerCraft in lua
  8.  
  9. -- Settings
  10. local wordLength = 8
  11. local maxAttempts = 5
  12. local resetTimeAfterWin = 120
  13. local minSimilarity = 1
  14. local displayMatrixScreen = true
  15. local minSimilarWords = 2
  16. local minModerateSimilarWords = 3
  17. local doorSide = "bottom"
  18.  
  19. -- Internal settings
  20. local garbleToLength = 16
  21. local linesToDisplay = 18
  22. local wordsToGenerate = 20
  23. local chanceOfWordPerLine = 30 -- % chance of a word appearing on a line
  24.  
  25. -- Variables
  26. local attempts = 0
  27. local terminalLog = {}
  28. local terminalLogInput = ">"
  29. local cursorYPos = 1
  30. local password = ""
  31. local words = {}
  32. local wordCoordinates = {}
  33. local selectedWord = nil
  34. local lines = {}
  35. local tPixels = {}
  36. local cachedWords = {}
  37.  
  38. -- Check if monitors are connected
  39. local monitor = peripheral.find("monitor")
  40. if not monitor then
  41.     print("No monitor found")
  42.     return
  43. end
  44.  
  45. -- Setup matrix display
  46. local size = {monitor.getSize()}
  47.  
  48. for x = 1, size[1] - 1 do
  49.     tPixels[x] = {}
  50.     for y = 1, size[2] do
  51.         tPixels[x][y]=' '
  52.     end
  53. end
  54.  
  55. -- Put garbled text around a word, returns the word and the start and end positions of the word
  56. function garble(word)
  57.     local chars = {
  58.         "\"", "!", "@", "#", "%", "^", "&", "*", "(", ")", "_", "+", "-",
  59.         "=", "{", "}", "[", "]", "|", ":", ";", "'", "<", ">", ",", ".", "?",
  60.         "/", "`", "~"
  61.     }
  62.  
  63.     -- Place our word in the middle of a garbled string
  64.     local garbled = ""
  65.     for i = 1, garbleToLength do
  66.         garbled = garbled .. chars[math.random(1, #chars)]
  67.     end
  68.  
  69.     if word == "" then
  70.         return garbled
  71.     end
  72.  
  73.     local start = math.random(1, garbleToLength - wordLength)
  74.     garbled = string.sub(garbled, 1, start) .. word .. string.sub(garbled, start + wordLength + 1, garbleToLength)
  75.  
  76.     return garbled, start
  77. end
  78.  
  79. function writeLine(message)
  80.     monitor.setCursorPos(1, cursorYPos)
  81.     monitor.write(message)
  82.     cursorYPos = cursorYPos + 1
  83.     monitor.setCursorPos(1, cursorYPos)
  84. end
  85.  
  86. function shuffle(tbl)
  87.     for i = #tbl, 2, -1 do
  88.         local j = math.random(i)
  89.         tbl[i], tbl[j] = tbl[j], tbl[i]
  90.     end
  91.     return tbl
  92. end
  93.  
  94. -- Function to pulse a bundled cable signal [Custom]
  95. local function pulseBundledSignal(color)
  96.     for i = 1, 8 do
  97.         redstone.setBundledOutput(doorSide, color)
  98.         sleep(0.2)
  99.         redstone.setBundledOutput(doorSide, 0)
  100.         sleep(0.2)
  101.     end
  102. end
  103.  
  104. -- Generate lines of text with garbled words
  105. -- Generate is separate from display so things don't change
  106. function generateLines()
  107.     local lines = {}
  108.  
  109.     table.insert(lines, 1, "Welcome to TPDCO Industries (TM) Termlink")
  110.     table.insert(lines, 2, "Password Required")
  111.     table.insert(lines, 3, "Attempts Remaining: " .. string.rep("#", (maxAttempts - attempts)))
  112.     table.insert(lines, 4, "")
  113.  
  114.     local line = ""
  115.     local displayedWords = 0
  116.     local linesDisplayed = 0
  117.     wordCoordinates = {}
  118.     local lineForPassword = math.random(1, linesToDisplay * 2)
  119.  
  120.     -- linesToDisplay * 2 as we display 2 lines at a time
  121.     while linesDisplayed < (linesToDisplay * 2) do
  122.         local randomNumber = decToHex(math.random(20000, 22000))
  123.         local word = words[displayedWords + 1]
  124.         local prefixLength = string.len(randomNumber) + 1 + string.len(line)
  125.  
  126.         -- Best way to ensure we definitely display the password, force it to be on a specific line
  127.         if lineForPassword == linesDisplayed + 1 then
  128.             word = password
  129.         end
  130.  
  131.         -- If we dont have a word or we dont hit the chance or we're out of words, then don't display a word.
  132.         -- Unless of course we're on the line that has the password
  133.         if
  134.             word == nil or ((math.random(1, 100) > chanceOfWordPerLine or displayedWords == #words) and linesDisplayed + 1 ~= lineForPassword) then
  135.             local garbledWord, start = garble("")
  136.             line = line .. randomNumber .. " " .. garbledWord .. " "
  137.         else
  138.             local garbledWord, start = garble(word)
  139.             line = line .. randomNumber .. " " .. garbledWord .. " "
  140.  
  141.             displayedWords = displayedWords + 1
  142.             wordCoordinates[word] = {prefixLength + start, #lines + 1}
  143.         end
  144.  
  145.         linesDisplayed = linesDisplayed + 1
  146.         if (linesDisplayed % 2) == 0 then
  147.             table.insert(lines, line)
  148.             line = ""
  149.         end
  150.     end
  151.  
  152.     return lines
  153. end
  154.  
  155. -- Display the lines of text
  156. function display()
  157.     resetDisplay()
  158.  
  159.     if #lines == 0 then
  160.         lines = generateLines()
  161.     end
  162.  
  163.     lines[3] = "Attempts Remaining: " .. string.rep("#", (maxAttempts - attempts))
  164.  
  165.     for i, line in ipairs(lines) do
  166.         writeLine(line)
  167.     end
  168.  
  169.     -- Display terminal log to the right - TODO: make this dynamic
  170.     for i, terminalLine in ipairs(terminalLog) do
  171.         local y = #lines - #terminalLog + i
  172.         monitor.setCursorPos(50, y - 1)
  173.         monitor.write(terminalLine)
  174.     end
  175.  
  176.     monitor.setCursorPos(50, #lines)
  177.     monitor.write(terminalLogInput)
  178. end
  179.  
  180. -- Highlight the selected word using the x,y position
  181. function highlightSelectedWord()
  182.     word = selectedWord
  183.  
  184.     if word == nil then
  185.         return
  186.     end
  187.  
  188.     x = wordCoordinates[word][1]
  189.     y = wordCoordinates[word][2]
  190.  
  191.     monitor.setCursorPos(x + 1, y)
  192.     monitor.setTextColor(colors.white)
  193.     monitor.setBackgroundColor(colors.green)
  194.     monitor.write(string.upper(word))
  195.     monitor.setTextColor(colors.green)
  196.     monitor.setBackgroundColor(colors.black)
  197. end
  198.  
  199. -- Initial display setup
  200. function resetDisplay()
  201.     -- Clear monitor
  202.     monitor.clear()
  203.     monitor.setCursorPos(1, 1)
  204.     cursorYPos = 1
  205.  
  206.     -- Set monitor settings
  207.     monitor.setTextScale(0.5)
  208.     monitor.setTextColor(colors.green)
  209.     monitor.setBackgroundColor(colors.black)
  210. end
  211.  
  212. -- Convert decimal to hex
  213. function decToHex(int)
  214.     local hex = string.format("%x", int)
  215.     if string.len(hex) == 1 then
  216.         hex = "0" .. hex
  217.     end
  218.     return "0x" .. string.upper(hex)
  219. end
  220.  
  221. -- Download the wordlist from the internet, discard words that are too short or too long
  222. function downloadWordList()
  223.     local url = "https://raw.githubusercontent.com/dolph/dictionary/master/popular.txt"
  224.     local response = http.get(url)
  225.     if response then
  226.         local file = fs.open("wordlist", "w")
  227.         local data = response.readAll()
  228.  
  229.         -- Remove all words shorter than 5 characters or longer than 9
  230.         for word in string.gmatch(data, "%a+") do
  231.             if string.len(word) >= 5 and string.len(word) <= 9 then
  232.                 file.write(word .. "\n")
  233.             end
  234.         end
  235.  
  236.         file.close()
  237.     end
  238. end
  239.  
  240. -- Does the table contain the value?
  241. function tableContainsItem(arr, val)
  242.     for i, v in ipairs(arr) do
  243.         if v == val then
  244.             return true
  245.         end
  246.     end
  247.     return false
  248. end
  249.  
  250. -- Merge tables
  251. function tableMerge(...)
  252.     local newTable = {}
  253.  
  254.     for i, t in ipairs({...}) do
  255.         for j, v in ipairs(t) do
  256.             table.insert(newTable, v)
  257.         end
  258.     end
  259.  
  260.     return newTable
  261. end
  262.  
  263. -- Get similar words to the password
  264. -- Tries to return 2 very similar words, 3 somewhat similar words, and the rest random
  265. function getSimilarWords()
  266.     local similarWords = {}
  267.     local somewhatSimilarWords = {}
  268.     local randomWords = {}
  269.     local loopLimit = 50000
  270.     local loopCount = 0
  271.  
  272.     while #similarWords < minSimilarWords and loopCount < loopLimit do
  273.         local word = getRandomWordOfLength(wordLength)
  274.         local sim  = getStringSimilarity(password, word)
  275.  
  276.         if
  277.             word ~= password
  278.             and sim >= #password - 2
  279.             and tableContainsItem(similarWords, word) == false
  280.         then
  281.             table.insert(similarWords, word)
  282.         end
  283.         loopCount = loopCount + 1
  284.     end
  285.  
  286.     loopCount = 0
  287.  
  288.     while #somewhatSimilarWords < minModerateSimilarWords and loopCount < loopLimit do
  289.         local word = getRandomWordOfLength(wordLength)
  290.         local sim = getStringSimilarity(password, word)
  291.  
  292.         if
  293.             word ~= password
  294.             and sim >= #password - 5
  295.             and tableContainsItem(similarWords, word) == false
  296.             and tableContainsItem(somewhatSimilarWords, word) == false
  297.         then
  298.             table.insert(somewhatSimilarWords, word)
  299.         end
  300.         loopCount = loopCount + 1
  301.     end
  302.  
  303.     loopCount = 0
  304.  
  305.     while (#randomWords + #somewhatSimilarWords + #similarWords) < wordsToGenerate do
  306.         local word = getRandomWordOfLength(wordLength)
  307.         local sim = getStringSimilarity(password, word)
  308.  
  309.         if
  310.             word ~= password
  311.             and sim > minSimilarity
  312.             and tableContainsItem(similarWords, word) == false
  313.             and tableContainsItem(somewhatSimilarWords, word) == false
  314.             and tableContainsItem(randomWords, word) == false
  315.         then
  316.             table.insert(randomWords, word)
  317.         end
  318.     end
  319.  
  320.     return shuffle(tableMerge(similarWords, somewhatSimilarWords, randomWords))
  321. end
  322.  
  323. -- Get the similarity between two strings (the number of characters that are the same)
  324. function getStringSimilarity(string1, string2)
  325.     local difference = 0
  326.     for i = 1, string.len(string1) do
  327.         if string.sub(string1, i, i) ~= string.sub(string2, i, i) then
  328.             difference = difference + 1
  329.         end
  330.     end
  331.  
  332.     return #string1 - difference
  333. end
  334.  
  335. -- Get a random word of a specific length
  336. -- Caches the words in memory
  337. function getRandomWordOfLength(length)
  338.     if #cachedWords == 0 then
  339.         local file = fs.open("wordlist", "r")
  340.         for line in file.readLine do
  341.             table.insert(cachedWords, string.upper(line))
  342.         end
  343.         file.close()
  344.     end
  345.  
  346.     local word = cachedWords[math.random(1, #cachedWords)]
  347.     while string.len(word) ~= length do
  348.         word = cachedWords[math.random(1, #cachedWords)]
  349.     end
  350.  
  351.     return word
  352. end
  353.  
  354. -- Get the word at the x,y coordinates
  355. function getWordAtCoordinates(x, y)
  356.     for word, coordinates in pairs(wordCoordinates) do
  357.         if x >= coordinates[1] and x <= coordinates[1] + string.len(word) and y == coordinates[2] then
  358.             return word
  359.         end
  360.     end
  361.     return nil
  362. end
  363.  
  364. -- Print a box with text in the middle of the screen
  365. function printOverlay(texts)
  366.     local numLines = #texts
  367.     local width = 0
  368.     for i = 1, numLines do
  369.         width = math.max(width, #texts[i])
  370.     end
  371.  
  372.     local screenCenterX = math.floor(size[1] / 2)
  373.     local screenCenterY = math.floor(size[2] / 2)
  374.     local boxHeight = numLines + 2
  375.     local boxWidth = width + 2
  376.     local boxX = screenCenterX - math.floor(boxWidth / 2)
  377.     local boxY = screenCenterY - math.floor(boxHeight / 2)
  378.  
  379.     -- Print box
  380.     for y = 0, boxHeight - 1 do
  381.         for x = 0, boxWidth - 1 do
  382.             monitor.setCursorPos(boxX + x, boxY + y)
  383.             monitor.setBackgroundColor(colors.cyan)
  384.             monitor.write(' ')
  385.         end
  386.     end
  387.  
  388.     -- Print text
  389.     for i = 1, numLines do
  390.         local text = texts[i]
  391.         local textX = screenCenterX - math.floor(#text / 2)
  392.         local textY = screenCenterY - math.floor(numLines / 2) + i - 1
  393.  
  394.         monitor.setCursorPos(textX, textY)
  395.         monitor.setBackgroundColor(colors.cyan)
  396.         monitor.setTextColor(colors.white)
  397.         monitor.write(text)
  398.     end
  399.  
  400.     monitor.setBackgroundColor(colors.black)
  401.     monitor.setTextColor(colors.lime)
  402. end
  403.  
  404. -- Render the matrix
  405. function matrixRender(overlayTexts)
  406.     monitor.clear()
  407.     monitor.setCursorPos(1, 1)
  408.  
  409.     if (displayMatrixScreen) then
  410.         for y = 1, #tPixels[1] do
  411.             monitor.setCursorPos(1, y)
  412.             if y ~= 1 then
  413.                 monitor.write('')
  414.             end
  415.             for x = 1, #tPixels do
  416.                 monitor.setCursorPos(x, y)
  417.                 monitor.setTextColor(colors.lime)
  418.                 monitor.write(tPixels[x][y])
  419.             end
  420.         end
  421.     end
  422.  
  423.     printOverlay(overlayTexts)
  424.  
  425.     monitor.setBackgroundColor(colors.black)
  426.     monitor.setTextColor(colors.lime)
  427. end
  428.  
  429. -- "Cycle the matrix" - This is the matrix effect
  430. function matrixCycle()
  431.     for x = 1, #tPixels do
  432.         for y = #tPixels[x], 2, -1 do
  433.             tPixels[x][y] = (tPixels[x][y - 1] == ' ' and ' ') or ((tPixels[x][y] ~= ' ' and tPixels[x][y]) or string.char(math.random(32, 126)))
  434.         end
  435.     end
  436. end
  437.  
  438. -- Start the matrix
  439. function matrixCreate()
  440.     tPixels[math.random(1, #tPixels)][1] = string.char(math.random(32, 126))
  441.     tPixels[math.random(1, #tPixels)][1] = ' '
  442.     tPixels[math.random(1, #tPixels)][1] = ' '
  443. end
  444.  
  445. -- Display the matrix for a specific amount of time
  446. function displayMatrix(seconds, overlayTexts)
  447.     local loops = seconds * 10
  448.  
  449.     for i = 1, loops do
  450.         matrixCycle()
  451.         matrixCreate()
  452.         matrixRender(overlayTexts)
  453.         sleep(.1)
  454.     end
  455. end
  456.  
  457. function termLog(message)
  458.     table.insert(terminalLog, message)
  459.  
  460.     if #terminalLog > linesToDisplay - 4 then
  461.         table.remove(terminalLog, 1)
  462.     end
  463. end
  464.  
  465. function termLogInput(message)
  466.     terminalLogInput = message
  467. end
  468.  
  469. -- Check if wordlist file exists
  470. if not fs.exists("wordlist") then
  471.     downloadWordList()
  472. end
  473.  
  474. -- Pick a password and find similar words
  475. password = getRandomWordOfLength(wordLength)
  476. words = getSimilarWords()
  477.  
  478. display()
  479.  
  480. print(password)
  481.  
  482. while true do
  483.     event, side, x, y = os.pullEvent("monitor_touch")
  484.     local selected = getWordAtCoordinates(x, y)
  485.  
  486.     if selected == nil then
  487.         selectedWord = nil
  488.         display()
  489.         termLogInput(">")
  490.     end
  491.  
  492.  if selected == selectedWord and selected ~= nil then
  493.         -- Word selected
  494.         termLog(">" .. selected)
  495.  
  496.         if selected == password then
  497.             termLog(">Access Granted")
  498.  
  499.             -- Set the redstone signal out the back of the computer
  500.             -- redstone.setOutput("back", true)
  501.             pulseBundledSignal(colors.green)
  502.  
  503.             for s = 1, resetTimeAfterWin do
  504.                 displayMatrix(1, {"HACKED", "Reset in " .. (resetTimeAfterWin - s) .. "s"})
  505.             end
  506.  
  507.             monitor.clear()
  508.             pulseBundledSignal(colors.red)
  509.             os.reboot()
  510. else
  511.             termLog(">Entry Denied")
  512.             termLog(">Likeness=" .. getStringSimilarity(password, selected))
  513.             selectedWord = nil
  514.             attempts = attempts + 1
  515.             if attempts >= maxAttempts then
  516.  
  517.                 for s = 1, 5 do
  518.                     displayMatrix(1, {"LOCKED", "Reset in " .. (resetTimeAfterWin - s) .. "s"})
  519.                 end
  520.        
  521.                 monitor.clear()
  522.                 pulseBundledSignal(colors.red)
  523.                 os.reboot()
  524.             end
  525.         end
  526.     end
  527.  
  528.     selectedWord = selected
  529.  
  530.     if selected ~= nil then
  531.         termLogInput(">" .. selected)
  532.     end
  533.  
  534.     display()
  535.  
  536.     highlightSelectedWord()
  537. end
  538.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement