SHOW:
|
|
- or go back to the newest paste.
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 = 10 |
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 | - | table.insert(lines, 1, "Welcome to ROBCO Industries (TM) Termlink") |
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 | - | if selected == selectedWord and selected ~= nil then |
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 | - | redstone.setOutput("back", true) |
489 | + | |
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 |