home · contact · privacy
Refactor client curses code.
[plomrogue2] / rogue_chat_curses.py
1 #!/usr/bin/env python3
2 import curses
3 import sys
4 from plomrogue.game import GameBase
5 from plomrogue.parser import Parser
6 from plomrogue.mapping import YX, MapGeometrySquare, MapGeometryHex
7 from plomrogue.things import ThingBase
8 from plomrogue.misc import quote
9 from plomrogue.errors import ArgError
10 from plomrogue_client.socket import ClientSocket
11 from plomrogue_client.tui import msg_into_lines_of_width, CursesScreen
12
13
14
15 mode_helps = {
16     'play': {
17         'short': 'play',
18         'intro': '',
19         'long': 'This mode allows you to interact with the map in various ways.'
20     },
21     'study': {
22         'short': 'study',
23         'intro': '',
24         'long': 'This mode allows you to study the map and its tiles in detail.  Move the question mark over a tile, and the right half of the screen will show detailed information on it.  Toggle the map view to show or hide different information layers.'},
25     'edit': {
26         'short': 'world edit',
27         'intro': '',
28         'long': 'This mode allows you to change the game world in various ways.  Individual map tiles can be protected by "protection characters", which you can see by toggling into the protections map view.  You can edit a tile if you set the world edit password that matches its protection character.  The character "." marks the absence of protection:  Such tiles can always be edited.'
29     },
30     'name_thing': {
31         'short': 'name thing',
32         'intro': '',
33         'long': 'Give name to/change name of carried thing.'
34     },
35     'command_thing': {
36         'short': 'command',
37         'intro': '',
38         'long': 'Enter a command to the thing you carry.  Enter nothing to return to play mode.'
39     },
40     'take_thing': {
41         'short': 'take',
42         'intro': 'Pick up a thing in reach by entering its index number.  Enter nothing to abort.',
43         'long': 'You see a list of things which you could pick up.  Enter the target thing\'s index, or, to leave, nothing.'
44     },
45     'drop_thing': {
46         'short': 'drop',
47         'intro': 'Enter number of direction to which you want to drop thing.',
48         'long': 'Drop currently carried thing by entering the target direction index.  Enter nothing to return to play mode..'
49     },
50     'admin_thing_protect': {
51         'short': 'change thing protection',
52         'intro': '@ enter thing protection character:',
53         'long': 'Change protection character for carried thing.'
54     },
55     'enter_face': {
56         'short': 'edit face',
57         'intro': '@ enter face line:',
58         'long': 'Draw your face as ASCII art.  The string you enter must be 18 characters long, and will be divided on display into 3 lines of 6 characters each, from top to bottom..'
59     },
60     'enter_design': {
61         'short': 'edit design',
62         'intro': '@ enter design:',
63         'long': 'Enter design for carried thing as ASCII art.'
64     },
65     'write': {
66         'short': 'edit tile',
67         'intro': '',
68         'long': 'This mode allows you to change the map tile you currently stand on (if your world editing password authorizes you so).  Just enter any printable ASCII character to imprint it on the ground below you.'
69     },
70     'control_pw_type': {
71         'short': 'change protection character password',
72         'intro': '@ enter protection character for which you want to change the password:',
73         'long': 'This mode is the first of two steps to change the password for a protection character.  First enter the protection character for which you want to change the password.'
74     },
75     'control_pw_pw': {
76         'short': 'change protection character password',
77         'intro': '',
78         'long': 'This mode is the second of two steps to change the password for a protection character.  Enter the new password for the protection character you chose.'
79     },
80     'control_tile_type': {
81         'short': 'change tiles protection',
82         'intro': '@ enter protection character which you want to draw:',
83         'long': 'This mode is the first of two steps to change tile protection areas on the map.  First enter the tile protection character you want to write.'
84     },
85     'control_tile_draw': {
86         'short': 'change tiles protection',
87         'intro': '',
88         'long': 'This mode is the second of two steps to change tile protection areas on the map.  Toggle tile protection drawing on/off and move the ?? cursor around the map to draw the selected protection character.'
89     },
90     'annotate': {
91         'short': 'annotate tile',
92         'intro': '',
93         'long': 'This mode allows you to add/edit a comment on the tile you are currently standing on (provided your world editing password authorizes you so).  Hit Return to leave.'
94     },
95     'portal': {
96         'short': 'edit portal',
97         'intro': '',
98         'long': 'This mode allows you to imprint/edit/remove a teleportation target on the ground you are currently standing on (provided your world editing password authorizes you so).  Enter or edit a URL to imprint a teleportation target; enter emptiness to remove a pre-existing teleportation target.  Hit Return to leave.'
99     },
100     'chat': {
101         'short': 'chat',
102         'intro': '',
103         'long': 'This mode allows you to engage in chit-chat with other users.  Any line you enter into the input prompt that does not start with a "/" will be sent out to nearby players – but barriers and distance will reduce what they can read, so stand close to them to ensure they get your message.  Lines that start with a "/" are used for commands like:\n\n/nick NAME – re-name yourself to NAME'
104     },
105     'login': {
106         'short': 'login',
107         'intro': '',
108         'long': 'Enter your player name.'
109     },
110     'waiting_for_server': {
111         'short': 'waiting for server response',
112         'intro': '@ waiting for server …',
113         'long': 'Waiting for a server response.'
114     },
115     'post_login_wait': {
116         'short': 'waiting for server response',
117         'intro': '',
118         'long': 'Waiting for a server response.'
119     },
120     'password': {
121         'short': 'set world edit password',
122         'intro': '',
123         'long': 'This mode allows you to change the password that you send to authorize yourself for editing password-protected elements of the world.  Hit return to confirm and leave.'
124     },
125     'admin_enter': {
126         'short': 'become admin',
127         'intro': '@ enter admin password:',
128         'long': 'This mode allows you to become admin if you know an admin password.'
129     },
130     'admin': {
131         'short': 'admin',
132         'intro': '',
133         'long': 'This mode allows you access to actions limited to administrators.'
134     }
135 }
136
137 def cmd_TURN(game, n):
138     game.turn_complete = False
139 cmd_TURN.argtypes = 'int:nonneg'
140
141 def cmd_OTHER_WIPE(game):
142     game.portals_new = {}
143     game.annotations_new = {}
144     game.things_new = []
145 cmd_OTHER_WIPE.argtypes = ''
146
147 def cmd_LOGIN_OK(game):
148     game.tui.switch_mode('post_login_wait')
149     game.tui.send('GET_GAMESTATE')
150     game.tui.log_msg('@ welcome!')
151 cmd_LOGIN_OK.argtypes = ''
152
153 def cmd_ADMIN_OK(game):
154     game.tui.is_admin = True
155     game.tui.log_msg('@ you now have admin rights')
156     game.tui.switch_mode('admin')
157     game.tui.do_refresh = True
158 cmd_ADMIN_OK.argtypes = ''
159
160 def cmd_REPLY(game, msg):
161     game.tui.log_msg('#MUSICPLAYER: ' + msg)
162     game.tui.do_refresh = True
163 cmd_REPLY.argtypes = 'string'
164
165 def cmd_CHAT(game, msg):
166     game.tui.log_msg('# ' + msg)
167     game.tui.do_refresh = True
168 cmd_CHAT.argtypes = 'string'
169
170 def cmd_CHATFACE(game, thing_id):
171     game.tui.draw_face = thing_id
172     game.tui.do_refresh = True
173 cmd_CHATFACE.argtypes = 'int:pos'
174
175 def cmd_PLAYER_ID(game, player_id):
176     game.player_id = player_id
177 cmd_PLAYER_ID.argtypes = 'int:nonneg'
178
179 def cmd_PLAYERS_HAT_CHARS(game, hat_chars):
180     game.players_hat_chars_new = hat_chars
181 cmd_PLAYERS_HAT_CHARS.argtypes = 'string'
182
183 def cmd_THING(game, yx, thing_type, protection, thing_id, portable, commandable):
184     t = game.get_thing_temp(thing_id)
185     if not t:
186         t = ThingBase(game, thing_id)
187         game.things_new += [t]
188     t.position = yx
189     t.type_ = thing_type
190     t.protection = protection
191     t.portable = portable
192     t.commandable = commandable
193 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg bool bool'
194
195 def cmd_THING_NAME(game, thing_id, name):
196     t = game.get_thing_temp(thing_id)
197     t.name = name
198 cmd_THING_NAME.argtypes = 'int:pos string'
199
200 def cmd_THING_FACE(game, thing_id, face):
201     t = game.get_thing_temp(thing_id)
202     t.face = face
203 cmd_THING_FACE.argtypes = 'int:pos string'
204
205 def cmd_THING_HAT(game, thing_id, hat):
206     t = game.get_thing_temp(thing_id)
207     t.hat = hat
208 cmd_THING_HAT.argtypes = 'int:pos string'
209
210 def cmd_THING_DESIGN(game, thing_id, size, design):
211     t = game.get_thing_temp(thing_id)
212     t.design = [size, design]
213 cmd_THING_DESIGN.argtypes = 'int:pos yx_tuple string'
214
215 def cmd_THING_CHAR(game, thing_id, c):
216     t = game.get_thing_temp(thing_id)
217     t.thing_char = c
218 cmd_THING_CHAR.argtypes = 'int:pos char'
219
220 def cmd_MAP(game, geometry, size, content):
221     map_geometry_class = globals()['MapGeometry' + geometry]
222     game.map_geometry_new = map_geometry_class(size)
223     game.map_content_new = content
224     if type(game.map_geometry_new) == MapGeometrySquare:
225         game.tui.movement_keys = {
226             game.tui.keys['square_move_up']: 'UP',
227             game.tui.keys['square_move_left']: 'LEFT',
228             game.tui.keys['square_move_down']: 'DOWN',
229             game.tui.keys['square_move_right']: 'RIGHT',
230         }
231     elif type(game.map_geometry_new) == MapGeometryHex:
232         game.tui.movement_keys = {
233             game.tui.keys['hex_move_upleft']: 'UPLEFT',
234             game.tui.keys['hex_move_upright']: 'UPRIGHT',
235             game.tui.keys['hex_move_right']: 'RIGHT',
236             game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
237             game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
238             game.tui.keys['hex_move_left']: 'LEFT',
239         }
240 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
241
242 def cmd_FOV(game, content):
243     game.fov_new = content
244 cmd_FOV.argtypes = 'string'
245
246 def cmd_MAP_CONTROL(game, content):
247     game.map_control_content_new = content
248 cmd_MAP_CONTROL.argtypes = 'string'
249
250 def cmd_GAME_STATE_COMPLETE(game):
251     game.tui.do_refresh = True
252     game.tui.info_cached = None
253     game.things = game.things_new
254     game.portals = game.portals_new
255     game.annotations = game.annotations_new
256     game.fov = game.fov_new
257     game.map_geometry = game.map_geometry_new
258     game.map_content = game.map_content_new
259     game.map_control_content = game.map_control_content_new
260     game.player = game.get_thing(game.player_id)
261     game.players_hat_chars = game.players_hat_chars_new
262     game.bladder_pressure = game.bladder_pressure_new
263     game.energy = game.energy_new
264     game.turn_complete = True
265     if game.tui.mode.name == 'post_login_wait':
266         game.tui.switch_mode('play')
267 cmd_GAME_STATE_COMPLETE.argtypes = ''
268
269 def cmd_PORTAL(game, position, msg):
270     game.portals_new[position] = msg
271 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
272
273 def cmd_PLAY_ERROR(game, msg):
274     game.tui.log_msg('? ' + msg)
275     game.tui.flash = True
276     game.tui.do_refresh = True
277 cmd_PLAY_ERROR.argtypes = 'string'
278
279 def cmd_GAME_ERROR(game, msg):
280     game.tui.log_msg('? game error: ' + msg)
281     game.tui.do_refresh = True
282 cmd_GAME_ERROR.argtypes = 'string'
283
284 def cmd_ARGUMENT_ERROR(game, msg):
285     game.tui.log_msg('? syntax error: ' + msg)
286     game.tui.do_refresh = True
287 cmd_ARGUMENT_ERROR.argtypes = 'string'
288
289 def cmd_ANNOTATION(game, position, msg):
290     game.annotations_new[position] = msg
291     if game.tui.mode.shows_info:
292         game.tui.do_refresh = True
293 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
294
295 def cmd_TASKS(game, tasks_comma_separated):
296     game.tasks = tasks_comma_separated.split(',')
297     game.tui.mode_write.legal = 'WRITE' in game.tasks
298     game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks
299     game.tui.mode_take_thing.legal = 'PICK_UP' in game.tasks
300     game.tui.mode_drop_thing.legal = 'DROP' in game.tasks
301 cmd_TASKS.argtypes = 'string'
302
303 def cmd_THING_TYPE(game, thing_type, symbol_hint):
304     game.thing_types[thing_type] = symbol_hint
305 cmd_THING_TYPE.argtypes = 'string char'
306
307 def cmd_THING_INSTALLED(game, thing_id):
308     game.get_thing_temp(thing_id).installed = True
309 cmd_THING_INSTALLED.argtypes = 'int:pos'
310
311 def cmd_THING_CARRYING(game, thing_id, carried_id):
312     game.get_thing_temp(thing_id).carrying = game.get_thing_temp(carried_id)
313 cmd_THING_CARRYING.argtypes = 'int:pos int:pos'
314
315 def cmd_TERRAIN(game, terrain_char, terrain_desc):
316     game.terrains[terrain_char] = terrain_desc
317 cmd_TERRAIN.argtypes = 'char string'
318
319 def cmd_PONG(game):
320     pass
321 cmd_PONG.argtypes = ''
322
323 def cmd_DEFAULT_COLORS(game):
324     game.tui.set_default_colors()
325 cmd_DEFAULT_COLORS.argtypes = ''
326
327 def cmd_RANDOM_COLORS(game):
328     game.tui.set_random_colors()
329 cmd_RANDOM_COLORS.argtypes = ''
330
331 def cmd_STATS(game, bladder_pressure, energy):
332     game.bladder_pressure_new = bladder_pressure
333     game.energy_new = energy
334 cmd_STATS.argtypes = 'int:nonneg int'
335
336 class Game(GameBase):
337     turn_complete = False
338     tasks = {}
339     thing_types = {}
340     things_new = []
341
342     def __init__(self, *args, **kwargs):
343         super().__init__(*args, **kwargs)
344         self.register_command(cmd_LOGIN_OK)
345         self.register_command(cmd_ADMIN_OK)
346         self.register_command(cmd_PONG)
347         self.register_command(cmd_CHAT)
348         self.register_command(cmd_CHATFACE)
349         self.register_command(cmd_REPLY)
350         self.register_command(cmd_PLAYER_ID)
351         self.register_command(cmd_TURN)
352         self.register_command(cmd_OTHER_WIPE)
353         self.register_command(cmd_THING)
354         self.register_command(cmd_THING_TYPE)
355         self.register_command(cmd_THING_NAME)
356         self.register_command(cmd_THING_CHAR)
357         self.register_command(cmd_THING_FACE)
358         self.register_command(cmd_THING_HAT)
359         self.register_command(cmd_THING_DESIGN)
360         self.register_command(cmd_THING_CARRYING)
361         self.register_command(cmd_THING_INSTALLED)
362         self.register_command(cmd_TERRAIN)
363         self.register_command(cmd_MAP)
364         self.register_command(cmd_MAP_CONTROL)
365         self.register_command(cmd_PORTAL)
366         self.register_command(cmd_ANNOTATION)
367         self.register_command(cmd_GAME_STATE_COMPLETE)
368         self.register_command(cmd_PLAYERS_HAT_CHARS)
369         self.register_command(cmd_ARGUMENT_ERROR)
370         self.register_command(cmd_GAME_ERROR)
371         self.register_command(cmd_PLAY_ERROR)
372         self.register_command(cmd_TASKS)
373         self.register_command(cmd_FOV)
374         self.register_command(cmd_DEFAULT_COLORS)
375         self.register_command(cmd_RANDOM_COLORS)
376         self.register_command(cmd_STATS)
377         self.map_content = ''
378         self.players_hat_chars = ''
379         self.player_id = -1
380         self.annotations = {}
381         self.annotations_new = {}
382         self.portals = {}
383         self.portals_new = {}
384         self.terrains = {}
385         self.player = None
386
387     def get_string_options(self, string_option_type):
388         if string_option_type == 'map_geometry':
389             return ['Hex', 'Square']
390         elif string_option_type == 'thing_type':
391             return self.thing_types.keys()
392         return None
393
394     def get_command(self, command_name):
395         from functools import partial
396         f = partial(self.commands[command_name], self)
397         f.argtypes = self.commands[command_name].argtypes
398         return f
399
400     def get_thing_temp(self, id_):
401         for thing in self.things_new:
402             if id_ == thing.id_:
403                 return thing
404         return None
405
406 class Mode:
407
408     def __init__(self, name, has_input_prompt=False, shows_info=False,
409                  is_intro=False, is_single_char_entry=False):
410         self.name = name
411         self.short_desc = mode_helps[name]['short']
412         self.available_modes = []
413         self.available_actions = []
414         self.has_input_prompt = has_input_prompt
415         self.shows_info = shows_info
416         self.is_intro = is_intro
417         self.help_intro = mode_helps[name]['long']
418         self.intro_msg = mode_helps[name]['intro']
419         self.is_single_char_entry = is_single_char_entry
420         self.legal = True
421
422     def iter_available_modes(self, tui):
423         for mode_name in self.available_modes:
424             mode = getattr(tui, 'mode_' + mode_name)
425             if not mode.legal:
426                 continue
427             key = tui.keys['switch_to_' + mode.name]
428             yield mode, key
429
430     def list_available_modes(self, tui):
431         msg = ''
432         if len(self.available_modes) > 0:
433             msg = 'Other modes available from here:\n'
434             for mode, key in self.iter_available_modes(tui):
435                 msg += '[%s] – %s\n' % (key, mode.short_desc)
436         return msg
437
438     def mode_switch_on_key(self, tui, key_pressed):
439         for mode, key in self.iter_available_modes(tui):
440             if key_pressed == key:
441                 tui.switch_mode(mode.name)
442                 return True
443         return False
444
445 class TUI:
446     mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
447     mode_admin = Mode('admin')
448     mode_play = Mode('play')
449     mode_study = Mode('study', shows_info=True)
450     mode_write = Mode('write', is_single_char_entry=True)
451     mode_edit = Mode('edit')
452     mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
453     mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
454     mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
455     mode_control_tile_draw = Mode('control_tile_draw')
456     mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
457     mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
458     mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
459     mode_chat = Mode('chat', has_input_prompt=True)
460     mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
461     mode_login = Mode('login', has_input_prompt=True, is_intro=True)
462     mode_post_login_wait = Mode('post_login_wait', is_intro=True)
463     mode_password = Mode('password', has_input_prompt=True)
464     mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
465     mode_command_thing = Mode('command_thing', has_input_prompt=True)
466     mode_take_thing = Mode('take_thing', has_input_prompt=True)
467     mode_drop_thing = Mode('drop_thing', has_input_prompt=True)
468     mode_enter_face = Mode('enter_face', has_input_prompt=True)
469     mode_enter_design = Mode('enter_design', has_input_prompt=True)
470     is_admin = False
471     tile_draw = False
472
473     def __init__(self, host):
474         import os
475         import json
476         self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
477                                           "command_thing", "take_thing",
478                                           "drop_thing"]
479         self.mode_play.available_actions = ["move", "teleport", "door", "consume",
480                                             "install", "wear", "spin", "dance"]
481         self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
482         self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
483         self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
484                                            "control_tile_type", "chat",
485                                            "study", "play", "edit"]
486         self.mode_admin.available_actions = ["move", "toggle_map_mode"]
487         self.mode_control_tile_draw.available_modes = ["admin_enter"]
488         self.mode_control_tile_draw.available_actions = ["move_explorer",
489                                                          "toggle_tile_draw"]
490         self.mode_edit.available_modes = ["write", "annotate", "portal",
491                                           "name_thing", "enter_face", "enter_design",
492                                           "password",
493                                           "chat", "study", "play", "admin_enter"]
494         self.mode_edit.available_actions = ["move", "flatten", "install",
495                                             "toggle_map_mode"]
496         self.mode = None
497         self.socket = ClientSocket(host, self.socket_log)
498         self.game = Game()
499         self.game.tui = self
500         self.parser = Parser(self.game)
501         self.log = []
502         self.do_refresh = True
503         self.login_name = None
504         self.map_mode = 'terrain + things'
505         self.password = 'foo'
506         self.switch_mode('waiting_for_server')
507         self.keys = {
508             'switch_to_chat': 't',
509             'switch_to_play': 'p',
510             'switch_to_password': 'P',
511             'switch_to_annotate': 'M',
512             'switch_to_portal': 'T',
513             'switch_to_study': '?',
514             'switch_to_edit': 'E',
515             'switch_to_write': 'm',
516             'switch_to_name_thing': 'N',
517             'switch_to_command_thing': 'O',
518             'switch_to_admin_enter': 'A',
519             'switch_to_control_pw_type': 'C',
520             'switch_to_control_tile_type': 'Q',
521             'switch_to_admin_thing_protect': 'T',
522             'flatten': 'F',
523             'switch_to_enter_face': 'f',
524             'switch_to_enter_design': 'D',
525             'switch_to_take_thing': 'z',
526             'switch_to_drop_thing': 'u',
527             'teleport': 'p',
528             'consume': 'C',
529             'door': 'D',
530             'install': 'I',
531             'wear': 'W',
532             'spin': 'S',
533             'dance': 'T',
534             'help': 'h',
535             'toggle_map_mode': 'L',
536             'toggle_tile_draw': 'm',
537             'hex_move_upleft': 'w',
538             'hex_move_upright': 'e',
539             'hex_move_right': 'd',
540             'hex_move_downright': 'x',
541             'hex_move_downleft': 'y',
542             'hex_move_left': 'a',
543             'square_move_up': 'w',
544             'square_move_left': 'a',
545             'square_move_down': 's',
546             'square_move_right': 'd',
547         }
548         if os.path.isfile('config.json'):
549             with open('config.json', 'r') as f:
550                 keys_conf = json.loads(f.read())
551             for k in keys_conf:
552                 self.keys[k] = keys_conf[k]
553         self.show_help = False
554         self.input_lines = []
555         self.fov = ''
556         self.flash = False
557         self.map_lines = []
558         self.ascii_draw_stage = 0
559         self.full_ascii_draw = ''
560         self.offset = YX(0,0)
561         self.screen = CursesScreen()
562         self.screen.wrap_loop(self.loop)
563
564     def update_on_connect(self):
565         self.socket.send('TASKS')
566         self.socket.send('TERRAINS')
567         self.socket.send('THING_TYPES')
568         self.switch_mode('login')
569
570     def reconnect(self):
571         import time
572         self.log_msg('@ attempting reconnect')
573         self.socket.send('QUIT')
574         # necessitated by some strange SSL race conditions with ws4py
575         time.sleep(0.1)  # FIXME find out why exactly necessary
576         self.switch_mode('waiting_for_server')
577         self.socket.connect()
578         self.update_on_connect()
579
580     def send(self, msg):
581         self.socket.send(msg)
582         if self.socket.disconnected:
583             self.do_refresh = True
584
585     def socket_log(self, msg):
586         self.log_msg('@ ' + msg)
587
588     def log_msg(self, msg):
589         self.log += [msg]
590         if len(self.log) > 100:
591             self.log = self.log[-100:]
592
593     def restore_input_values(self):
594         if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
595             self.input_ = self.game.annotations[self.explorer]
596         elif self.mode.name == 'portal' and self.explorer in self.game.portals:
597             self.input_ = self.game.portals[self.explorer]
598         elif self.mode.name == 'password':
599             self.input_ = self.password
600         elif self.mode.name == 'name_thing':
601             if hasattr(self.game.player.carrying, 'name'):
602                 self.input_ = self.game.player.carrying.name
603         elif self.mode.name == 'admin_thing_protect':
604             if hasattr(self.game.player.carrying, 'protection'):
605                 self.input_ = self.game.player.carrying.protection
606         elif self.mode.name == 'enter_face':
607             start = self.ascii_draw_stage * 6
608             end = (self.ascii_draw_stage + 1) * 6
609             self.input_ = self.game.player.face[start:end]
610         elif self.mode.name == 'enter_design':
611             width = self.game.player.carrying.design[0].x
612             start = self.ascii_draw_stage * width
613             end = (self.ascii_draw_stage + 1) * width
614             self.input_ = self.game.player.carrying.design[1][start:end]
615
616     def send_tile_control_command(self):
617         self.send('SET_TILE_CONTROL %s %s' %
618                   (self.explorer, quote(self.tile_control_char)))
619
620     def toggle_map_mode(self):
621         if self.map_mode == 'terrain only':
622             self.map_mode = 'terrain + annotations'
623         elif self.map_mode == 'terrain + annotations':
624             self.map_mode = 'terrain + things'
625         elif self.map_mode == 'terrain + things':
626             self.map_mode = 'protections'
627         elif self.map_mode == 'protections':
628             self.map_mode = 'terrain only'
629
630     def switch_mode(self, mode_name):
631
632         def fail(msg, return_mode='play'):
633             self.log_msg('? ' + msg)
634             self.flash = True
635             self.switch_mode(return_mode)
636
637         if self.mode and self.mode.name == 'control_tile_draw':
638             self.log_msg('@ finished tile protection drawing.')
639         self.draw_face = False
640         self.tile_draw = False
641         self.ascii_draw_stage = 0
642         self.full_ascii_draw = ''
643         if mode_name == 'command_thing' and\
644            (not self.game.player.carrying or
645             not self.game.player.carrying.commandable):
646             return fail('not carrying anything commandable')
647         if mode_name == 'name_thing' and not self.game.player.carrying:
648             return fail('not carrying anything to re-name', 'edit')
649         if mode_name == 'admin_thing_protect' and not self.game.player.carrying:
650             return fail('not carrying anything to protect')
651         if mode_name == 'take_thing' and self.game.player.carrying:
652             return fail('already carrying something')
653         if mode_name == 'drop_thing' and not self.game.player.carrying:
654             return fail('not carrying anything droppable')
655         if mode_name == 'enter_design' and\
656            (not self.game.player.carrying or
657             not hasattr(self.game.player.carrying, 'design')):
658             return fail('not carrying designable to edit', 'edit')
659         if mode_name == 'admin_enter' and self.is_admin:
660             mode_name = 'admin'
661         self.mode = getattr(self, 'mode_' + mode_name)
662         if self.mode.name in {'control_tile_draw', 'control_tile_type',
663                               'control_pw_type'}:
664             self.map_mode = 'protections'
665         elif self.mode.name != 'edit':
666             self.map_mode = 'terrain + things'
667         if self.mode.shows_info or self.mode.name == 'control_tile_draw':
668             self.explorer = YX(self.game.player.position.y,
669                                self.game.player.position.x)
670         if self.mode.is_single_char_entry:
671             self.show_help = True
672         if len(self.mode.intro_msg) > 0:
673             self.log_msg(self.mode.intro_msg)
674         if self.mode.name == 'login':
675             if self.login_name:
676                 self.send('LOGIN ' + quote(self.login_name))
677             else:
678                 self.log_msg('@ enter username')
679         elif self.mode.name == 'take_thing':
680             self.log_msg('Portable things in reach for pick-up:')
681             directed_moves = {
682                 'HERE': YX(0, 0), 'LEFT': YX(0, -1), 'RIGHT': YX(0, 1)
683             }
684             if type(self.game.map_geometry) == MapGeometrySquare:
685                 directed_moves['UP'] = YX(-1, 0)
686                 directed_moves['DOWN'] = YX(1, 0)
687             elif type(self.game.map_geometry) == MapGeometryHex:
688                 if self.game.player.position.y % 2:
689                     directed_moves['UPLEFT'] = YX(-1, 0)
690                     directed_moves['UPRIGHT'] = YX(-1, 1)
691                     directed_moves['DOWNLEFT'] = YX(1, 0)
692                     directed_moves['DOWNRIGHT'] = YX(1, 1)
693                 else:
694                     directed_moves['UPLEFT'] = YX(-1, -1)
695                     directed_moves['UPRIGHT'] = YX(-1, 0)
696                     directed_moves['DOWNLEFT'] = YX(1, -1)
697                     directed_moves['DOWNRIGHT'] = YX(1, 0)
698             select_range = {}
699             for direction in directed_moves:
700                 move = directed_moves[direction]
701                 select_range[direction] = self.game.player.position + move
702             self.selectables = []
703             directions = []
704             for direction in select_range:
705                 for t in [t for t in self.game.things
706                           if t.portable and t.position == select_range[direction]]:
707                     self.selectables += [t.id_]
708                     directions += [direction]
709             if len(self.selectables) == 0:
710                 return fail('nothing to pick-up')
711             else:
712                 for i in range(len(self.selectables)):
713                     t = self.game.get_thing(self.selectables[i])
714                     self.log_msg('%s %s: %s' % (i, directions[i],
715                                                 self.get_thing_info(t)))
716         elif self.mode.name == 'drop_thing':
717             self.log_msg('Direction to drop thing to:')
718             self.selectables =\
719                 ['HERE'] + list(self.game.tui.movement_keys.values())
720             for i in range(len(self.selectables)):
721                 self.log_msg(str(i) + ': ' + self.selectables[i])
722         elif self.mode.name == 'enter_design':
723             if self.game.player.carrying.type_ == 'Hat':
724                 self.log_msg('@ The design you enter must be %s lines of max %s '
725                              'characters width each'
726                              % (self.game.player.carrying.design[0].y,
727                                 self.game.player.carrying.design[0].x))
728                 self.log_msg('@ Legal characters: ' + self.game.players_hat_chars)
729                 self.log_msg('@ (Eat cookies to extend the ASCII characters available for drawing.)')
730             else:
731                 self.log_msg('@ Width of first line determines maximum width for remaining design')
732                 self.log_msg('@ Finish design by entering an empty line (multiple space characters do not count as empty)')
733         elif self.mode.name == 'command_thing':
734             self.send('TASK:COMMAND ' + quote('HELP'))
735         elif self.mode.name == 'control_pw_pw':
736             self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
737         elif self.mode.name == 'control_tile_draw':
738             self.log_msg('@ can draw protection character "%s", turn drawing on/off with [%s], finish with [%s].' % (self.tile_control_char, self.keys['toggle_tile_draw'], self.keys['switch_to_admin_enter']))
739         self.input_ = ""
740         self.restore_input_values()
741
742     def set_default_colors(self):
743         if curses.can_change_color():
744             curses.init_color(7, 1000, 1000, 1000)
745             curses.init_color(0, 0, 0, 0)
746         self.do_refresh = True
747
748     def set_random_colors(self):
749
750         def rand(offset):
751             import random
752             return int(offset + random.random()*375)
753
754         if curses.can_change_color():
755             curses.init_color(7, rand(625), rand(625), rand(625))
756             curses.init_color(0, rand(0), rand(0), rand(0))
757         self.do_refresh = True
758
759     def get_info(self):
760         if self.info_cached:
761             return self.info_cached
762         pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
763         info_to_cache = ''
764         if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
765             info_to_cache += 'outside field of view'
766         else:
767             for t in self.game.things:
768                 if t.position == self.explorer:
769                     info_to_cache += '%s' % self.get_thing_info(t, True)
770             terrain_char = self.game.map_content[pos_i]
771             terrain_desc = '?'
772             if terrain_char in self.game.terrains:
773                 terrain_desc = self.game.terrains[terrain_char]
774             info_to_cache += 'TERRAIN: %s (%s' % (terrain_char,
775                                                        terrain_desc)
776             protection = self.game.map_control_content[pos_i]
777             if protection != '.':
778                 info_to_cache += '/protection:%s' % protection
779             info_to_cache += ')\n'
780             if self.explorer in self.game.portals:
781                 info_to_cache += 'PORTAL: ' +\
782                     self.game.portals[self.explorer] + '\n'
783             if self.explorer in self.game.annotations:
784                 info_to_cache += 'ANNOTATION: ' +\
785                     self.game.annotations[self.explorer]
786         self.info_cached = info_to_cache
787         return self.info_cached
788
789     def get_thing_info(self, t, detailed=False):
790         info = ''
791         if detailed:
792             info += '- '
793         info += self.game.thing_types[t.type_]
794         if hasattr(t, 'thing_char'):
795             info += t.thing_char
796         if hasattr(t, 'name'):
797             info += ': %s' % t.name
798         info += ' (%s' % t.type_
799         if hasattr(t, 'installed'):
800             info += '/installed'
801         if t.type_ == 'Bottle':
802             if t.thing_char == '_':
803                 info += '/empty'
804             elif t.thing_char == '~':
805                 info += '/full'
806         if detailed:
807             protection = t.protection
808             if protection != '.':
809                 info += '/protection:%s' % protection
810             info += ')\n'
811             if hasattr(t, 'hat') or hasattr(t, 'face'):
812                 info += '----------\n'
813             if hasattr(t, 'hat'):
814                 info += '| %s |\n' % t.hat[0:6]
815                 info += '| %s |\n' % t.hat[6:12]
816                 info += '| %s |\n' % t.hat[12:18]
817             if hasattr(t, 'face'):
818                 info += '| %s |\n' % t.face[0:6]
819                 info += '| %s |\n' % t.face[6:12]
820                 info += '| %s |\n' % t.face[12:18]
821                 info += '----------\n'
822             if hasattr(t, 'design'):
823                 line_length = t.design[0].x
824                 lines = []
825                 for i in range(t.design[0].y):
826                     start = i * line_length
827                     end = (i + 1) * line_length
828                     lines += [t.design[1][start:end]]
829                 info += '-' * (line_length + 4) + '\n'
830                 for line in lines:
831                     info += '| %s |\n' % line
832                 info += '-' * (line_length + 4) + '\n'
833         else:
834             info += ')'
835         return info
836
837     def loop(self):
838
839         def safe_addstr(y, x, line):
840             self.screen.safe_addstr(y, x, line, curses.color_pair(1))
841
842         def handle_input(msg):
843             command, args = self.parser.parse(msg)
844             command(*args)
845
846         def task_action_on(action):
847             return action_tasks[action] in self.game.tasks
848
849         def reset_screen_size():
850             self.screen.reset_size()
851             self.left_window_width = min(52, int(self.screen.size.x / 2))
852             self.right_window_width = self.screen.size.x - self.left_window_width
853
854         def recalc_input_lines():
855             if not self.mode.has_input_prompt:
856                 self.input_lines = []
857             else:
858                 self.input_lines = msg_into_lines_of_width(input_prompt
859                                                            + self.input_ + '█',
860                                                            self.right_window_width)
861
862         def move_explorer(direction):
863             target = self.game.map_geometry.move_yx(self.explorer, direction)
864             if target:
865                 self.info_cached = None
866                 self.explorer = target
867                 if self.tile_draw:
868                     self.send_tile_control_command()
869             else:
870                 self.flash = True
871
872         def draw_history():
873             lines = []
874             for line in self.log:
875                 lines += msg_into_lines_of_width(line, self.right_window_width)
876             lines.reverse()
877             height_header = 2
878             max_y = self.screen.size.y - len(self.input_lines)
879             for i in range(len(lines)):
880                 if (i >= max_y - height_header):
881                     break
882                 safe_addstr(max_y - i - 1, self.left_window_width, lines[i])
883
884         def draw_info():
885             info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
886             lines = msg_into_lines_of_width(info, self.right_window_width)
887             height_header = 2
888             for i in range(len(lines)):
889                 y = height_header + i
890                 if y >= self.screen.size.y - len(self.input_lines):
891                     break
892                 safe_addstr(y, self.left_window_width, lines[i])
893
894         def draw_input():
895             y = self.screen.size.y - len(self.input_lines)
896             for i in range(len(self.input_lines)):
897                 safe_addstr(y, self.left_window_width, self.input_lines[i])
898                 y += 1
899
900         def draw_stats():
901             stats = 'ENERGY: %s BLADDER: %s' % (self.game.energy,
902                                                 self.game.bladder_pressure)
903             safe_addstr(0, self.left_window_width, stats)
904
905         def draw_mode():
906             help = "hit [%s] for help" % self.keys['help']
907             if self.mode.has_input_prompt:
908                 help = "enter /help for help"
909             safe_addstr(1, self.left_window_width,
910                         'MODE: %s – %s' % (self.mode.short_desc, help))
911
912         def draw_map():
913             if (not self.game.turn_complete) and len(self.map_lines) == 0:
914                 return
915             if self.game.turn_complete:
916                 map_lines_split = []
917                 for y in range(self.game.map_geometry.size.y):
918                     start = self.game.map_geometry.size.x * y
919                     end = start + self.game.map_geometry.size.x
920                     if self.map_mode == 'protections':
921                         map_lines_split += [[c + ' ' for c
922                                              in self.game.map_control_content[start:end]]]
923                     else:
924                         map_lines_split += [[c + ' ' for c
925                                              in self.game.map_content[start:end]]]
926                 if self.map_mode == 'terrain + annotations':
927                     for p in self.game.annotations:
928                         map_lines_split[p.y][p.x] = 'A '
929                 elif self.map_mode == 'terrain + things':
930                     for p in self.game.portals.keys():
931                         original = map_lines_split[p.y][p.x]
932                         map_lines_split[p.y][p.x] = original[0] + 'P'
933                     used_positions = []
934
935                     def draw_thing(t, used_positions):
936                         symbol = self.game.thing_types[t.type_]
937                         meta_char = ' '
938                         if hasattr(t, 'thing_char'):
939                             meta_char = t.thing_char
940                         if t.position in used_positions:
941                             meta_char = '+'
942                         if hasattr(t, 'carrying') and t.carrying:
943                             meta_char = '$'
944                         map_lines_split[t.position.y][t.position.x] = symbol + meta_char
945                         used_positions += [t.position]
946
947                     for t in [t for t in self.game.things if t.type_ != 'Player']:
948                         draw_thing(t, used_positions)
949                     for t in [t for t in self.game.things if t.type_ == 'Player']:
950                         draw_thing(t, used_positions)
951                 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
952                     map_lines_split[self.explorer.y][self.explorer.x] = '??'
953                 elif self.map_mode != 'terrain + things':
954                     map_lines_split[self.game.player.position.y]\
955                         [self.game.player.position.x] = '??'
956                 self.map_lines = []
957                 if type(self.game.map_geometry) == MapGeometryHex:
958                     indent = 0
959                     for line in map_lines_split:
960                         self.map_lines += [indent * ' ' + ''.join(line)]
961                         indent = 0 if indent else 1
962                 else:
963                     for line in map_lines_split:
964                         self.map_lines += [''.join(line)]
965                 window_center = YX(int(self.screen.size.y / 2),
966                                    int(self.left_window_width / 2))
967                 center = self.game.player.position
968                 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
969                     center = self.explorer
970                 center = YX(center.y, center.x * 2)
971                 self.offset = center - window_center
972                 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
973                     self.offset += YX(0, 1)
974             term_y = max(0, -self.offset.y)
975             term_x = max(0, -self.offset.x)
976             map_y = max(0, self.offset.y)
977             map_x = max(0, self.offset.x)
978             while term_y < self.screen.size.y and map_y < len(self.map_lines):
979                 to_draw = self.map_lines[map_y][map_x:self.left_window_width + self.offset.x]
980                 safe_addstr(term_y, term_x, to_draw)
981                 term_y += 1
982                 map_y += 1
983
984         def draw_names():
985             players = [t for t in self.game.things if t.type_ == 'Player']
986             players.sort(key=lambda t: len(t.name))
987             players.reverse()
988             shrink_offset = max(0, (self.screen.size.y - self.left_window_width // 2) // 2)
989             y = 0
990             for t in players:
991                 offset_y = y - shrink_offset
992                 max_len = max(5, (self.left_window_width // 2) - (offset_y * 2) - 8)
993                 name = t.name[:]
994                 if len(name) > max_len:
995                     name = name[:max_len - 1] + '…'
996                 safe_addstr(y, 0, '@%s:%s' % (t.thing_char, name))
997                 y += 1
998                 if y >= self.screen.size.y:
999                     break
1000
1001         def draw_face_popup():
1002             t = self.game.get_thing(self.draw_face)
1003             if not t or not hasattr(t, 'face'):
1004                 self.draw_face = False
1005                 return
1006
1007             start_x = self.left_window_width - 10
1008             def draw_body_part(body_part, end_y):
1009                 safe_addstr(end_y - 3, start_x, '----------')
1010                 safe_addstr(end_y - 2, start_x, '| ' + body_part[0:6] + ' |')
1011                 safe_addstr(end_y - 1, start_x, '| ' + body_part[6:12] + ' |')
1012                 safe_addstr(end_y, start_x, '| ' + body_part[12:18] + ' |')
1013
1014             if hasattr(t, 'face'):
1015                 draw_body_part(t.face, self.screen.size.y - 3)
1016             if hasattr(t, 'hat'):
1017                 draw_body_part(t.hat, self.screen.size.y - 6)
1018             safe_addstr(self.screen.size.y - 2, start_x, '----------')
1019             name = t.name[:]
1020             if len(name) > 7:
1021                 name = name[:6 - 1] + '…'
1022             safe_addstr(self.screen.size.y - 1, start_x,
1023                         '@%s:%s' % (t.thing_char, name))
1024
1025         def draw_help():
1026             content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
1027                                              self.mode.help_intro)
1028             if len(self.mode.available_actions) > 0:
1029                 content += "Available actions:\n"
1030                 for action in self.mode.available_actions:
1031                     if action in action_tasks:
1032                         if action_tasks[action] not in self.game.tasks:
1033                             continue
1034                     if action == 'move_explorer':
1035                         action = 'move'
1036                     if action == 'move':
1037                         key = ','.join(self.movement_keys)
1038                     else:
1039                         key = self.keys[action]
1040                     content += '[%s] – %s\n' % (key, action_descriptions[action])
1041                 content += '\n'
1042             content += self.mode.list_available_modes(self)
1043             for i in range(self.screen.size.y):
1044                 safe_addstr(i,
1045                             self.left_window_width * (not self.mode.has_input_prompt),
1046                             ' ' * self.left_window_width)
1047             lines = []
1048             for line in content.split('\n'):
1049                 lines += msg_into_lines_of_width(line, self.right_window_width)
1050             for i in range(len(lines)):
1051                 if i >= self.screen.size.y:
1052                     break
1053                 safe_addstr(i,
1054                             self.left_window_width * (not self.mode.has_input_prompt),
1055                             lines[i])
1056
1057         def draw_screen():
1058             self.screen.stdscr.clear()
1059             self.screen.stdscr.bkgd(' ', curses.color_pair(1))
1060             recalc_input_lines()
1061             if self.mode.has_input_prompt:
1062                 draw_input()
1063             if self.mode.shows_info:
1064                 draw_info()
1065             else:
1066                 draw_history()
1067             draw_mode()
1068             if not self.mode.is_intro:
1069                 draw_stats()
1070                 draw_map()
1071             if self.show_help:
1072                 draw_help()
1073             if self.mode.name in {'chat', 'play'}:
1074                 draw_names()
1075                 if self.draw_face:
1076                     draw_face_popup()
1077
1078         def pick_selectable(task_name):
1079             try:
1080                 i = int(self.input_)
1081                 if i < 0 or i >= len(self.selectables):
1082                     self.log_msg('? invalid index, aborted')
1083                 else:
1084                     self.send('TASK:%s %s' % (task_name, self.selectables[i]))
1085             except ValueError:
1086                 self.log_msg('? invalid index, aborted')
1087             self.input_ = ''
1088             self.switch_mode('play')
1089
1090         def enter_ascii_art(command, height, width,
1091                             with_pw=False, with_size=False):
1092             if with_size and self.ascii_draw_stage == 0:
1093                 width = len(self.input_)
1094                 if width > 36:
1095                     self.log_msg('? input too long, must be max 36; try again')
1096                     # TODO: move max width mechanism server-side
1097                     return
1098                 old_size = self.game.player.carrying.design[0]
1099                 if width != old_size.x:
1100                     # TODO: save remaining design?
1101                     self.game.player.carrying.design[1] = ''
1102                     self.game.player.carrying.design[0] = YX(old_size.y, width)
1103             elif len(self.input_) > width:
1104                 self.log_msg('? input too long, '
1105                              'must be max %s; try again' % width)
1106                 return
1107             self.log_msg('  ' + self.input_)
1108             if with_size and self.input_ in {'', ' '}\
1109                and self.ascii_draw_stage > 0:
1110                 height = self.ascii_draw_stage
1111             else:
1112                 if with_size:
1113                     height = self.ascii_draw_stage + 2
1114                 if len(self.input_) < width:
1115                     self.input_ += ' ' * (width - len(self.input_))
1116                 self.full_ascii_draw += self.input_
1117             if with_size:
1118                 old_size = self.game.player.carrying.design[0]
1119                 self.game.player.carrying.design[0] = YX(height, old_size.x)
1120             self.ascii_draw_stage += 1
1121             if self.ascii_draw_stage < height:
1122                 self.restore_input_values()
1123             else:
1124                 if with_pw and with_size:
1125                     self.send('%s_SIZE %s %s' % (command, YX(height, width),
1126                                                  quote(self.password)))
1127                 if with_pw:
1128                     self.send('%s %s %s' % (command, quote(self.full_ascii_draw),
1129                                             quote(self.password)))
1130                 else:
1131                     self.send('%s %s' % (command, quote(self.full_ascii_draw)))
1132                 self.full_ascii_draw = ""
1133                 self.ascii_draw_stage = 0
1134                 self.input_ = ""
1135                 self.switch_mode('edit')
1136
1137         action_descriptions = {
1138             'move': 'move',
1139             'flatten': 'flatten surroundings',
1140             'teleport': 'teleport',
1141             'take_thing': 'pick up thing',
1142             'drop_thing': 'drop thing',
1143             'toggle_map_mode': 'toggle map view',
1144             'toggle_tile_draw': 'toggle protection character drawing',
1145             'install': '(un-)install',
1146             'wear': '(un-)wear',
1147             'door': 'open/close',
1148             'consume': 'consume',
1149             'spin': 'spin',
1150             'dance': 'dance',
1151         }
1152
1153         action_tasks = {
1154             'flatten': 'FLATTEN_SURROUNDINGS',
1155             'take_thing': 'PICK_UP',
1156             'drop_thing': 'DROP',
1157             'door': 'DOOR',
1158             'install': 'INSTALL',
1159             'wear': 'WEAR',
1160             'move': 'MOVE',
1161             'command': 'COMMAND',
1162             'consume': 'INTOXICATE',
1163             'spin': 'SPIN',
1164             'dance': 'DANCE',
1165         }
1166
1167         curses.start_color()
1168         self.set_default_colors()
1169         curses.init_pair(1, 7, 0)
1170         if not curses.can_change_color():
1171             self.log_msg('@ unfortunately, your terminal does not seem to '
1172                          'support re-definition of colors; you might miss out '
1173                          'on some color effects')
1174         reset_screen_size()
1175         self.explorer = YX(0, 0)
1176         self.input_ = ''
1177         store_widechar = False
1178         input_prompt = '> '
1179         while True:
1180             prev_disconnected = self.socket.disconnected
1181             self.socket.keep_connection_alive()
1182             if prev_disconnected and not self.socket.disconnected:
1183                 self.update_on_connect()
1184             if self.flash:
1185                 curses.flash()
1186                 self.flash = False
1187             if self.do_refresh:
1188                 draw_screen()
1189                 self.do_refresh = False
1190             for msg in self.socket.get_message():
1191                 handle_input(msg)
1192             try:
1193                 key = self.screen.stdscr.getkey()
1194                 self.do_refresh = True
1195             except curses.error:
1196                 continue
1197             keycode = None
1198             if len(key) == 1:
1199                 keycode = ord(key)
1200                 # workaround for <https://stackoverflow.com/a/56390915>
1201                 if store_widechar:
1202                     store_widechar = False
1203                     key = bytes([195, keycode]).decode()
1204                 if keycode == 195:
1205                     store_widechar = True
1206                     continue
1207             self.show_help = False
1208             self.draw_face = False
1209             if key == 'KEY_RESIZE':
1210                 reset_screen_size()
1211             elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1212                 self.input_ = self.input_[:-1]
1213             elif (((not self.mode.is_intro) and keycode == 27)  # Escape
1214                   or (self.mode.has_input_prompt and key == '\n'
1215                       and self.input_ == ''\
1216                       and self.mode.name in {'chat', 'command_thing',
1217                                              'take_thing', 'drop_thing',
1218                                              'admin_enter'})):
1219                 if self.mode.name not in {'chat', 'play', 'study', 'edit'}:
1220                     self.log_msg('@ aborted')
1221                 self.switch_mode('play')
1222             elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1223                 self.show_help = True
1224                 self.input_ = ""
1225                 self.restore_input_values()
1226             elif self.mode.has_input_prompt and key != '\n':  # Return key
1227                 self.input_ += key
1228                 max_length = self.right_window_width * self.screen.size.y - len(input_prompt) - 1
1229                 if len(self.input_) > max_length:
1230                     self.input_ = self.input_[:max_length]
1231             elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1232                 self.show_help = True
1233             elif self.mode.name == 'login' and key == '\n':
1234                 self.login_name = self.input_
1235                 self.send('LOGIN ' + quote(self.input_))
1236                 self.input_ = ""
1237             elif self.mode.name == 'enter_face' and key == '\n':
1238                 enter_ascii_art('PLAYER_FACE', 3, 6)
1239             elif self.mode.name == 'enter_design' and key == '\n':
1240                 if self.game.player.carrying.type_ == 'Hat':
1241                     enter_ascii_art('THING_DESIGN',
1242                                     self.game.player.carrying.design[0].y,
1243                                     self.game.player.carrying.design[0].x, True)
1244                 else:
1245                     enter_ascii_art('THING_DESIGN',
1246                                     self.game.player.carrying.design[0].y,
1247                                     self.game.player.carrying.design[0].x,
1248                                     True, True)
1249             elif self.mode.name == 'take_thing' and key == '\n':
1250                 pick_selectable('PICK_UP')
1251             elif self.mode.name == 'drop_thing' and key == '\n':
1252                 pick_selectable('DROP')
1253             elif self.mode.name == 'command_thing' and key == '\n':
1254                 self.send('TASK:COMMAND ' + quote(self.input_))
1255                 self.input_ = ""
1256             elif self.mode.name == 'control_pw_pw' and key == '\n':
1257                 if self.input_ == '':
1258                     self.log_msg('@ aborted')
1259                 else:
1260                     self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1261                     self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1262                 self.switch_mode('admin')
1263             elif self.mode.name == 'password' and key == '\n':
1264                 if self.input_ == '':
1265                     self.input_ = ' '
1266                 self.password = self.input_
1267                 self.switch_mode('edit')
1268             elif self.mode.name == 'admin_enter' and key == '\n':
1269                 self.send('BECOME_ADMIN ' + quote(self.input_))
1270                 self.switch_mode('play')
1271             elif self.mode.name == 'control_pw_type' and key == '\n':
1272                 if len(self.input_) != 1:
1273                     self.log_msg('@ entered non-single-char, therefore aborted')
1274                     self.switch_mode('admin')
1275                 else:
1276                     self.tile_control_char = self.input_
1277                     self.switch_mode('control_pw_pw')
1278             elif self.mode.name == 'admin_thing_protect' and key == '\n':
1279                 if len(self.input_) != 1:
1280                     self.log_msg('@ entered non-single-char, therefore aborted')
1281                 else:
1282                     self.send('THING_PROTECTION %s' % (quote(self.input_)))
1283                     self.log_msg('@ sent new protection character for thing')
1284                 self.switch_mode('admin')
1285             elif self.mode.name == 'control_tile_type' and key == '\n':
1286                 if len(self.input_) != 1:
1287                     self.log_msg('@ entered non-single-char, therefore aborted')
1288                     self.switch_mode('admin')
1289                 else:
1290                     self.tile_control_char = self.input_
1291                     self.switch_mode('control_tile_draw')
1292             elif self.mode.name == 'chat' and key == '\n':
1293                 if self.input_ == '':
1294                     continue
1295                 if self.input_[0] == '/':
1296                     if self.input_.startswith('/nick'):
1297                         tokens = self.input_.split(maxsplit=1)
1298                         if len(tokens) == 2:
1299                             self.send('NICK ' + quote(tokens[1]))
1300                         else:
1301                             self.log_msg('? need login name')
1302                     else:
1303                         self.log_msg('? unknown command')
1304                 else:
1305                     self.send('ALL ' + quote(self.input_))
1306                 self.input_ = ""
1307             elif self.mode.name == 'name_thing' and key == '\n':
1308                 if self.input_ == '':
1309                     self.input_ = ' '
1310                 self.send('THING_NAME %s %s' % (quote(self.input_),
1311                                                 quote(self.password)))
1312                 self.switch_mode('edit')
1313             elif self.mode.name == 'annotate' and key == '\n':
1314                 if self.input_ == '':
1315                     self.input_ = ' '
1316                 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1317                                                  quote(self.password)))
1318                 self.switch_mode('edit')
1319             elif self.mode.name == 'portal' and key == '\n':
1320                 if self.input_ == '':
1321                     self.input_ = ' '
1322                 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1323                                                quote(self.password)))
1324                 self.switch_mode('edit')
1325             elif self.mode.name == 'study':
1326                 if self.mode.mode_switch_on_key(self, key):
1327                     continue
1328                 elif key == self.keys['toggle_map_mode']:
1329                     self.toggle_map_mode()
1330                 elif key in self.movement_keys:
1331                     move_explorer(self.movement_keys[key])
1332             elif self.mode.name == 'play':
1333                 if self.mode.mode_switch_on_key(self, key):
1334                     continue
1335                 elif key == self.keys['door'] and task_action_on('door'):
1336                     self.send('TASK:DOOR')
1337                 elif key == self.keys['consume'] and task_action_on('consume'):
1338                     self.send('TASK:INTOXICATE')
1339                 elif key == self.keys['wear'] and task_action_on('wear'):
1340                     self.send('TASK:WEAR')
1341                 elif key == self.keys['spin'] and task_action_on('spin'):
1342                     self.send('TASK:SPIN')
1343                 elif key == self.keys['dance'] and task_action_on('dance'):
1344                     self.send('TASK:DANCE')
1345                 elif key == self.keys['teleport']:
1346                     if self.game.player.position in self.game.portals:
1347                         self.socket.host = self.game.portals[self.game.player.position]
1348                         self.reconnect()
1349                     else:
1350                         self.flash = True
1351                         self.log_msg('? not standing on portal')
1352                 elif key in self.movement_keys and task_action_on('move'):
1353                     self.send('TASK:MOVE ' + self.movement_keys[key])
1354             elif self.mode.name == 'write':
1355                 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1356                 self.switch_mode('edit')
1357             elif self.mode.name == 'control_tile_draw':
1358                 if self.mode.mode_switch_on_key(self, key):
1359                     continue
1360                 elif key in self.movement_keys:
1361                     move_explorer(self.movement_keys[key])
1362                 elif key == self.keys['toggle_tile_draw']:
1363                     self.tile_draw = False if self.tile_draw else True
1364             elif self.mode.name == 'admin':
1365                 if self.mode.mode_switch_on_key(self, key):
1366                     continue
1367                 elif key == self.keys['toggle_map_mode']:
1368                     self.toggle_map_mode()
1369                 elif key in self.movement_keys and task_action_on('move'):
1370                     self.send('TASK:MOVE ' + self.movement_keys[key])
1371             elif self.mode.name == 'edit':
1372                 if self.mode.mode_switch_on_key(self, key):
1373                     continue
1374                 elif key == self.keys['flatten'] and task_action_on('flatten'):
1375                     self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1376                 elif key == self.keys['install'] and task_action_on('install'):
1377                     self.send('TASK:INSTALL %s' % quote(self.password))
1378                 elif key == self.keys['toggle_map_mode']:
1379                     self.toggle_map_mode()
1380                 elif key in self.movement_keys and task_action_on('move'):
1381                     self.send('TASK:MOVE ' + self.movement_keys[key])
1382
1383 if len(sys.argv) != 2:
1384     raise ArgError('wrong number of arguments, need game host')
1385 host = sys.argv[1]
1386 TUI(host)