View difference between Paste ID: QEL3nJWD and SRwxXGbE
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