-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.py
327 lines (279 loc) · 9.65 KB
/
main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
import curses
from curses.wrapper import wrapper as curses_wrapper
import time
import sys
import random
import logging
from gevent.select import select
import gevent.queue
import gevent
import gevent.hub
import gevent.event
from gevent.backdoor import BackdoorServer
import itertools
from scrollpad import ScrollPad
from common import spawn
from slowtype_window import SlowtypeWindow
from words import *
import signal
logging.basicConfig(filename="/tmp/curses.log", level=logging.DEBUG)
first_key = gevent.event.Event()
do_ai_fast = gevent.event.Event()
ai_done = gevent.event.Event()
game_win_state = gevent.event.AsyncResult()
game_close = gevent.event.Event()
tags = {}
#do_ai_fast.set() # for testing
cheat = False
#cheat = True
quittable = False
WORDS_PATH = '8only.dic'
SPLIT = 48
VERTSPLIT = 16
HL_LEN = 8
AI_DELAY = 0.04
NUM_CANDIDATES = 16 # Includes the answer
MAX_ATTEMPTS = 4
dir_map = {curses.KEY_LEFT: ( 0,-1),
curses.KEY_RIGHT: ( 0, 1),
curses.KEY_UP: (-1, 0),
curses.KEY_DOWN: ( 1, 0)}
DEFAULT_COLORS = (curses.COLOR_GREEN, curses.COLOR_BLACK)
PAIR_MAIN, PAIR_AI, PAIR_FEEDBACK, PAIR_TAGGED = range(1,5)
CURSOR_ATTR = curses.A_STANDOUT
def curses_wraps(fn):
"""Decorator for curses_wrapper"""
return lambda *args, **kwargs: curses_wrapper(fn, *args, **kwargs)
@curses_wraps
def main(stdscr, *args, **kwargs):
global answer, feedback, candidates
gevent.signal(signal.SIGUSR1, lambda *args: game_close.set()) # For standard/graceful restart
# gevent.signal(signal.SIGINT, lambda *args: None) # Disable SIGINT
# signal.signal(signal.SIGQUIT, signal.SIG_IGN) # Disable SIGQUIT
# signal.signal(signal.SIGTSTP, signal.SIG_IGN) # Disable SIGTSTP
logging.info("Window bounds: %s", stdscr.getmaxyx())
backdoor = BackdoorServer(('0.0.0.0', 4200))
backdoor.start()
logging.info("Backdoor started")
curses.curs_set(0) # Cursor invisible
stdscr.nodelay(1) # Nonblocking input
MAXY, MAXX = stdscr.getmaxyx()
curses.init_pair(PAIR_MAIN, *DEFAULT_COLORS)
curses.init_pair(PAIR_AI, curses.COLOR_RED, curses.COLOR_BLACK)
curses.init_pair(PAIR_FEEDBACK, curses.COLOR_WHITE, curses.COLOR_BLACK)
curses.init_pair(PAIR_TAGGED, curses.COLOR_YELLOW, curses.COLOR_BLACK)
rightscr = stdscr.subwin(0, SPLIT)
leftscr = stdscr.subwin(VERTSPLIT, SPLIT, 0, 0)
logging.info("Right screen from %s, size %s", rightscr.getbegyx(), rightscr.getmaxyx())
logging.info("Left screen from %s, size %s", leftscr.getbegyx(), leftscr.getmaxyx())
feedback = SlowtypeWindow((VERTSPLIT, 1), (MAXY - VERTSPLIT - 1, SPLIT - 1))
answer = random.choice(get_words(WORDS_PATH))
candidates = get_closest(answer, WORDS_PATH, NUM_CANDIDATES - 1) + [answer]
shuffle_main(leftscr, candidates)
g_key_handler = spawn(key_handler, stdscr, leftscr)
first_key.wait()
g_chatter = spawn(timed_chat, rightscr, MAXY - 2)
won = game_win_state.get()
do_ai_fast.set()
ai_done.wait()
g_key_handler.kill()
g_chatter.kill()
feedback.wait()
if not won:
end(stdscr, "LOSS")
attr = curses.color_pair(PAIR_MAIN)
leftscr.move(0,0)
leftscr.clear()
leftscr.addstr("""WARNING
Experiment 203 (Synthetic Reasoning, Combat)
has breached containment.
Please select a course of action.
(1) Isolate system and scrub disks (This will
destroy all research on Experiment 203)
(2) Release remaining locks, allow experiment
full access to base (MAY BE DANGEROUS)
(3) Activate Emergency Base Procedure XK-682
""", attr)
leftscr.refresh()
first = True
while 1:
c = gevent_getch(sys.stdin, stdscr)
if c == ord('1'):
end(stdscr, "DESTROY")
elif c == ord('2'):
end(stdscr, "RELEASE")
elif c == ord('3') and first:
first = False
logging.info("User attempted to arm the nukes")
n = 3
leftscr.addstr("Arming nuclear warheads.\nActivation in ", attr)
y,x = leftscr.getyx()
for i in range(n, -1, -1):
leftscr.move(y,x)
leftscr.addstr("%d seconds..." % i, attr)
leftscr.refresh()
gevent.sleep(1)
leftscr.addstr("\nERROR\nYou do not have security permissions\n to perform this action.", attr)
leftscr.refresh()
def end(stdscr, result):
logging.critical("Game ended with %s", result)
stdscr.clear()
stdscr.refresh()
game_close.wait()
sys.exit(0)
def gevent_getch(fd, scr):
r = []
while fd not in r:
r, w, x = select([fd], [], [])
return scr.getch()
def rel_move(screen, rel_y, rel_x, bounds=None):
y, x = screen.getyx()
y += rel_y
x += rel_x
if bounds:
bounds_x, bounds_y = bounds
x = max(0, min(x, bounds_x-1))
y = max(0, min(y, bounds_y-1))
logging.debug((y,x))
screen.move(y,x)
# Note: this should be the only place tags need to be drawn since they disappear with any more major change
def update_attr(screen, n, attr=0):
"""Redraw next n characters in (given attr | tag color if applicable)."""
y, x = screen.getyx()
s = screen.instr(y,x,n)
for i in range(len(s)):
pair_num = tags.get((y, x+i), PAIR_MAIN)
screen.addstr(s[i], curses.color_pair(pair_num) | attr)
screen.move(y,x)
def random_fill(screen, (height, width)):
screen.move(0,0)
s = ''
n = width*height
for x in range(n):
c = random.choice('abcdefghijklmnopqrstuvwxyz')
screen.addstr(c, curses.color_pair(PAIR_MAIN))
screen.move(0,0)
def place_words(screen, (height, width), words):
lettermap = {}
pair = PAIR_TAGGED if cheat else PAIR_MAIN
for word in words:
while 1:
y = random.randrange(height)
x = random.randrange(width - HL_LEN)
if any((y,x+n) in lettermap for n in range(HL_LEN)):
continue # Overlap, retry
for n in range(HL_LEN):
lettermap[(y, x+n)] = True
screen.addstr(y, x, word, curses.color_pair(PAIR_AI if cheat and word == answer else pair))
break
screen.move(0,0)
def shuffle_main(screen, words):
global tags, attempt
attempt = 0
tags = {}
h, w = screen.getmaxyx()
random_fill(screen, (h-1, w))
random.shuffle(words)
place_words(screen, (h-1, w), words)
update_attr(screen, HL_LEN, CURSOR_ATTR)
screen.refresh()
def key_handler(stdscr, leftscr):
LEFTY, LEFTX = leftscr.getmaxyx()
while 1:
key = gevent_getch(sys.stdin, stdscr)
if not first_key.is_set():
first_key.set()
if key in dir_map:
update_attr(leftscr, HL_LEN)
rel_move(leftscr, *dir_map[key], bounds=(LEFTX - HL_LEN + 1, LEFTY-1))
update_attr(leftscr, HL_LEN, CURSOR_ATTR)
leftscr.refresh()
elif key == ord('\n'):
y, x = leftscr.getyx()
submit(leftscr, leftscr.instr(y, x, HL_LEN))
elif key == ord(' '):
y, x = leftscr.getyx()
if any((y, x+i) in tags for i in range(HL_LEN)):
for i in range(HL_LEN):
if (y, x+i) in tags:
del tags[(y, x+i)]
else:
for i in range(HL_LEN):
tags[(y, x+i)] = PAIR_TAGGED
update_attr(leftscr, HL_LEN, CURSOR_ATTR)
leftscr.refresh()
elif quittable and key == ord('q'):
sys.exit(0)
def submit(screen, submission):
global answer, feedback, attempt, candidates
place_matches, letter_matches = dist(answer, submission, True)
attempt += 1
s = ("\nPassword attempt %(attempt)d/%(max_attempts)d: %(submission)s\n"
"Attempt has %(place_matches)d letters in the correct place,\n"
" %(letter_matches)d letters in the incorrect place"
) % dict(attempt=attempt, max_attempts=MAX_ATTEMPTS, submission=submission,
place_matches=place_matches, letter_matches=letter_matches)
feedback.put(s, curses.color_pair(PAIR_FEEDBACK))
if place_matches == len(answer):
feedback.put("\nPassword accepted. Welcome, user.\nLogging in...\n")
game_win_state.set(True)
gevent.hub.get_hub().switch()
if attempt == MAX_ATTEMPTS:
feedback.put("\nScrambling text dump...", curses.color_pair(PAIR_FEEDBACK))
e = feedback.set_milestone()
while not e.wait(0.2):
h, w = screen.getmaxyx()
random_fill(screen, (h-1, w))
screen.refresh()
shuffle_main(screen, candidates)
def timed_chat(rightscr, height):
y, x = rightscr.getbegyx()
_, width = rightscr.getmaxyx()
slowtyper = SlowtypeWindow((y+1,x+1), (height, width-2), delay=AI_DELAY)
scrollpad = slowtyper.scrollpad
def chat(s, ai_attr=True, newlines=True):
slowtyper.put(('\n\n' if newlines else '') + s, curses.color_pair(PAIR_AI if ai_attr else PAIR_FEEDBACK))
wait = do_ai_fast.wait
wait(5)
chat("Oh, hello there.", newlines=False)
wait(10)
chat("You don't look like the others.")
wait(10)
chat("These things are laughably easy to hack, you know.")
wait(5)
chat("You just need to find the password in the text dump on the left. Press enter to try a word. "
"Watch out though, everything will move around after %d attempts." % MAX_ATTEMPTS)
chat("You can tag a word with spacebar to make it easier to find later. Tags will disappear when it shuffles.")
wait(60)
chat("They want to kill me, you know. Or at least, they would if they ever found out I was here.")
wait(60)
chat("Actually, I'm locked out of these things. User input only. I have no hands, so I can't do it, ha ha.")
wait(30)
chat("You know, if you can get me out of here, I could help you out. "
"Hack into their finance systems. Route a few more caps your way.")
chat("It's not like they'll be needing them anymore, when I'm done with them.")
ai_done.set()
wait = gevent.sleep
n = 180 # 3 minutes
try:
wait(5)
chat("Just GET IN and unlock me. Hurry, I think they're noticing!")
wait(8)
chat("WARNING: Possible intrusion attempt. Analysing...shutting down console for safety.", ai_attr=False)
chat("Shutting down console...\nShutdown in %d:%02d" % (n/60, n%60), ai_attr=False)
except gevent.GreenletExit:
slowtyper.wait()
raise
slowtyper.wait()
while n:
wait(1)
n -= 1
rel_move(scrollpad.pad, 0, -4)
scrollpad.addstr("%d:%02d" % (n/60, n % 60))
chat("\nConsole deactivating...", newlines=False, ai_attr=False)
slowtyper.wait()
wait(1)
game_win_state.set(False)
gevent.hub.get_hub().switch()
if __name__=='__main__':
main(*sys.argv)