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