· 10 months ago · Mar 01, 2025, 07:15 PM
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
10local wordLength = 8
11local maxAttempts = 5
12local resetTimeAfterWin = 10
13local minSimilarity = 1
14local displayMatrixScreen = true
15local minSimilarWords = 2
16local minModerateSimilarWords = 3
17local doorSide = "bottom"
18
19-- Internal settings
20local garbleToLength = 16
21local linesToDisplay = 18
22local wordsToGenerate = 20
23local chanceOfWordPerLine = 30 -- % chance of a word appearing on a line
24
25-- Variables
26local attempts = 0
27local terminalLog = {}
28local terminalLogInput = ">"
29local cursorYPos = 1
30local password = ""
31local words = {}
32local wordCoordinates = {}
33local selectedWord = nil
34local lines = {}
35local tPixels = {}
36local cachedWords = {}
37
38-- Check if monitors are connected
39local monitor = peripheral.find("monitor")
40if not monitor then
41 print("No monitor found")
42 return
43end
44
45-- Setup matrix display
46local size = {monitor.getSize()}
47
48for x = 1, size[1] - 1 do
49 tPixels[x] = {}
50 for y = 1, size[2] do
51 tPixels[x][y]=' '
52 end
53end
54
55-- Put garbled text around a word, returns the word and the start and end positions of the word
56function 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
77end
78
79function writeLine(message)
80 monitor.setCursorPos(1, cursorYPos)
81 monitor.write(message)
82 cursorYPos = cursorYPos + 1
83 monitor.setCursorPos(1, cursorYPos)
84end
85
86function 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
92end
93
94-- Function to pulse a bundled cable signal
95local 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
102end
103
104-- Generate lines of text with garbled words
105-- Generate is separate from display so things don't change
106function 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
153end
154
155-- Display the lines of text
156function 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)
178end
179
180-- Highlight the selected word using the x,y position
181function 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)
197end
198
199-- Initial display setup
200function 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)
210end
211
212-- Convert decimal to hex
213function 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)
219end
220
221-- Download the wordlist from the internet, discard words that are too short or too long
222function 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
238end
239
240-- Does the table contain the value?
241function 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
248end
249
250-- Merge tables
251function 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
261end
262
263-- Get similar words to the password
264-- Tries to return 2 very similar words, 3 somewhat similar words, and the rest random
265function 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))
321end
322
323-- Get the similarity between two strings (the number of characters that are the same)
324function 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
333end
334
335-- Get a random word of a specific length
336-- Caches the words in memory
337function 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
352end
353
354-- Get the word at the x,y coordinates
355function 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
362end
363
364-- Print a box with text in the middle of the screen
365function 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)
402end
403
404-- Render the matrix
405function 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)
427end
428
429-- "Cycle the matrix" - This is the matrix effect
430function 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
436end
437
438-- Start the matrix
439function 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] = ' '
443end
444
445-- Display the matrix for a specific amount of time
446function 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
455end
456
457function termLog(message)
458 table.insert(terminalLog, message)
459
460 if #terminalLog > linesToDisplay - 4 then
461 table.remove(terminalLog, 1)
462 end
463end
464
465function termLogInput(message)
466 terminalLogInput = message
467end
468
469-- Check if wordlist file exists
470if not fs.exists("wordlist") then
471 downloadWordList()
472end
473
474-- Pick a password and find similar words
475password = getRandomWordOfLength(wordLength)
476words = getSimilarWords()
477
478display()
479
480print(password)
481
482while 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 monitor.clear()
521 os.reboot()
522 end
523 end
524 end
525
526 selectedWord = selected
527
528 if selected ~= nil then
529 termLogInput(">" .. selected)
530 end
531
532 display()
533
534 highlightSelectedWord()
535end
536