7 from plomrogue.game import GameBase
8 from plomrogue.parser import Parser
9 from plomrogue.mapping import YX, MapGeometrySquare, MapGeometryHex
10 from plomrogue.things import ThingBase
11 from plomrogue.misc import quote
12 from plomrogue.errors import BrokenSocketConnection, ArgError
18 'long': 'This mode allows you to interact with the map in various ways.'
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.'},
25 'short': 'world edit',
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.'
30 'short': 'name thing',
32 'long': 'Give name to/change name of thing here.'
35 'short': 'command thing',
37 'long': 'Enter a command to the thing you carry. Enter nothing to return to play mode.'
40 'short': 'take thing',
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.'
45 'short': 'drop thing',
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..'
49 'admin_thing_protect': {
50 'short': 'change thing protection',
51 'intro': '@ enter thing protection character:',
52 'long': 'Change protection character for thing here.'
55 'short': 'enter your face',
56 'intro': '@ enter face line (enter nothing to abort):',
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..'
60 'short': 'enter your hat',
61 'intro': '@ enter hat line (enter nothing to abort):',
62 'long': 'Draw your hat 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..'
65 'short': 'change terrain',
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.'
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.'
75 'short': 'change protection character password',
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.'
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.'
84 'control_tile_draw': {
85 'short': 'change tiles protection',
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.'
90 'short': 'annotate tile',
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.'
95 'short': 'edit portal',
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.'
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'
107 'long': 'Enter your player name.'
109 'waiting_for_server': {
110 'short': 'waiting for server response',
111 'intro': '@ waiting for server …',
112 'long': 'Waiting for a server response.'
115 'short': 'waiting for server response',
117 'long': 'Waiting for a server response.'
120 'short': 'set world edit password',
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.'
125 'short': 'become admin',
126 'intro': '@ enter admin password:',
127 'long': 'This mode allows you to become admin if you know an admin password.'
132 'long': 'This mode allows you access to actions limited to administrators.'
136 from ws4py.client import WebSocketBaseClient
137 class WebSocketClient(WebSocketBaseClient):
139 def __init__(self, recv_handler, *args, **kwargs):
140 super().__init__(*args, **kwargs)
141 self.recv_handler = recv_handler
144 def received_message(self, message):
146 message = str(message)
147 self.recv_handler(message)
150 def plom_closed(self):
151 return self.client_terminated
153 from plomrogue.io_tcp import PlomSocket
154 class PlomSocketClient(PlomSocket):
156 def __init__(self, recv_handler, url):
158 self.recv_handler = recv_handler
159 host, port = url.split(':')
160 super().__init__(socket.create_connection((host, port)))
168 for msg in self.recv():
169 if msg == 'NEED_SSL':
170 self.socket = ssl.wrap_socket(self.socket)
172 self.recv_handler(msg)
173 except BrokenSocketConnection:
174 pass # we assume socket will be known as dead by now
176 def cmd_TURN(game, n):
178 game.turn_complete = False
179 cmd_TURN.argtypes = 'int:nonneg'
181 def cmd_PSEUDO_FOV_WIPE(game):
182 game.portals_new = {}
183 game.annotations_new = {}
185 cmd_PSEUDO_FOV_WIPE.argtypes = ''
187 def cmd_LOGIN_OK(game):
188 game.tui.switch_mode('post_login_wait')
189 game.tui.send('GET_GAMESTATE')
190 game.tui.log_msg('@ welcome')
191 cmd_LOGIN_OK.argtypes = ''
193 def cmd_ADMIN_OK(game):
194 game.tui.is_admin = True
195 game.tui.log_msg('@ you now have admin rights')
196 game.tui.switch_mode('admin')
197 game.tui.do_refresh = True
198 cmd_ADMIN_OK.argtypes = ''
200 def cmd_REPLY(game, msg):
201 game.tui.log_msg('#MUSICPLAYER: ' + msg)
202 game.tui.do_refresh = True
203 cmd_REPLY.argtypes = 'string'
205 def cmd_CHAT(game, msg):
206 game.tui.log_msg('# ' + msg)
207 game.tui.do_refresh = True
208 cmd_CHAT.argtypes = 'string'
210 def cmd_CHATFACE(game, thing_id):
211 game.tui.draw_face = thing_id
212 game.tui.do_refresh = True
213 cmd_CHATFACE.argtypes = 'int:pos'
215 def cmd_PLAYER_ID(game, player_id):
216 game.player_id = player_id
217 cmd_PLAYER_ID.argtypes = 'int:nonneg'
219 def cmd_PLAYERS_HAT_CHARS(game, hat_chars):
220 game.players_hat_chars_new = hat_chars
221 cmd_PLAYERS_HAT_CHARS.argtypes = 'string'
223 def cmd_THING(game, yx, thing_type, protection, thing_id, portable, commandable):
224 t = game.get_thing_temp(thing_id)
226 t = ThingBase(game, thing_id)
227 game.things_new += [t]
230 t.protection = protection
231 t.portable = portable
232 t.commandable = commandable
233 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg bool bool'
235 def cmd_THING_NAME(game, thing_id, name):
236 t = game.get_thing_temp(thing_id)
238 cmd_THING_NAME.argtypes = 'int:pos string'
240 def cmd_THING_FACE(game, thing_id, face):
241 t = game.get_thing_temp(thing_id)
243 cmd_THING_FACE.argtypes = 'int:pos string'
245 def cmd_THING_HAT(game, thing_id, hat):
246 t = game.get_thing_temp(thing_id)
248 cmd_THING_HAT.argtypes = 'int:pos string'
250 def cmd_THING_CHAR(game, thing_id, c):
251 t = game.get_thing_temp(thing_id)
253 cmd_THING_CHAR.argtypes = 'int:pos char'
255 def cmd_MAP(game, geometry, size, content):
256 map_geometry_class = globals()['MapGeometry' + geometry]
257 game.map_geometry_new = map_geometry_class(size)
258 game.map_content_new = content
259 if type(game.map_geometry) == MapGeometrySquare:
260 game.tui.movement_keys = {
261 game.tui.keys['square_move_up']: 'UP',
262 game.tui.keys['square_move_left']: 'LEFT',
263 game.tui.keys['square_move_down']: 'DOWN',
264 game.tui.keys['square_move_right']: 'RIGHT',
266 elif type(game.map_geometry) == MapGeometryHex:
267 game.tui.movement_keys = {
268 game.tui.keys['hex_move_upleft']: 'UPLEFT',
269 game.tui.keys['hex_move_upright']: 'UPRIGHT',
270 game.tui.keys['hex_move_right']: 'RIGHT',
271 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
272 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
273 game.tui.keys['hex_move_left']: 'LEFT',
275 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
277 def cmd_FOV(game, content):
278 game.fov_new = content
279 cmd_FOV.argtypes = 'string'
281 def cmd_MAP_CONTROL(game, content):
282 game.map_control_content_new = content
283 cmd_MAP_CONTROL.argtypes = 'string'
285 def cmd_GAME_STATE_COMPLETE(game):
286 game.tui.do_refresh = True
287 game.tui.info_cached = None
288 game.things = game.things_new
289 game.portals = game.portals_new
290 game.annotations = game.annotations_new
291 game.fov = game.fov_new
292 game.map_geometry = game.map_geometry_new
293 game.map_content = game.map_content_new
294 game.map_control_content = game.map_control_content_new
295 game.player = game.get_thing(game.player_id)
296 game.players_hat_chars = game.players_hat_chars_new
297 game.turn_complete = True
298 if game.tui.mode.name == 'post_login_wait':
299 game.tui.switch_mode('play')
300 cmd_GAME_STATE_COMPLETE.argtypes = ''
302 def cmd_PORTAL(game, position, msg):
303 game.portals_new[position] = msg
304 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
306 def cmd_PLAY_ERROR(game, msg):
307 game.tui.log_msg('? ' + msg)
308 game.tui.flash = True
309 game.tui.do_refresh = True
310 cmd_PLAY_ERROR.argtypes = 'string'
312 def cmd_GAME_ERROR(game, msg):
313 game.tui.log_msg('? game error: ' + msg)
314 game.tui.do_refresh = True
315 cmd_GAME_ERROR.argtypes = 'string'
317 def cmd_ARGUMENT_ERROR(game, msg):
318 game.tui.log_msg('? syntax error: ' + msg)
319 game.tui.do_refresh = True
320 cmd_ARGUMENT_ERROR.argtypes = 'string'
322 def cmd_ANNOTATION(game, position, msg):
323 game.annotations_new[position] = msg
324 if game.tui.mode.shows_info:
325 game.tui.do_refresh = True
326 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
328 def cmd_TASKS(game, tasks_comma_separated):
329 game.tasks = tasks_comma_separated.split(',')
330 game.tui.mode_write.legal = 'WRITE' in game.tasks
331 game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks
332 game.tui.mode_take_thing.legal = 'PICK_UP' in game.tasks
333 game.tui.mode_drop_thing.legal = 'DROP' in game.tasks
334 cmd_TASKS.argtypes = 'string'
336 def cmd_THING_TYPE(game, thing_type, symbol_hint):
337 game.thing_types[thing_type] = symbol_hint
338 cmd_THING_TYPE.argtypes = 'string char'
340 def cmd_THING_INSTALLED(game, thing_id):
341 game.get_thing_temp(thing_id).installed = True
342 cmd_THING_INSTALLED.argtypes = 'int:pos'
344 def cmd_THING_CARRYING(game, thing_id, carried_id):
345 game.get_thing_temp(thing_id).carrying = game.get_thing(carried_id)
346 cmd_THING_CARRYING.argtypes = 'int:pos int:pos'
348 def cmd_TERRAIN(game, terrain_char, terrain_desc):
349 game.terrains[terrain_char] = terrain_desc
350 cmd_TERRAIN.argtypes = 'char string'
354 cmd_PONG.argtypes = ''
356 def cmd_DEFAULT_COLORS(game):
357 game.tui.set_default_colors()
358 cmd_DEFAULT_COLORS.argtypes = ''
360 def cmd_RANDOM_COLORS(game):
361 game.tui.set_random_colors()
362 cmd_RANDOM_COLORS.argtypes = ''
364 class Game(GameBase):
365 turn_complete = False
370 def __init__(self, *args, **kwargs):
371 super().__init__(*args, **kwargs)
372 self.register_command(cmd_LOGIN_OK)
373 self.register_command(cmd_ADMIN_OK)
374 self.register_command(cmd_PONG)
375 self.register_command(cmd_CHAT)
376 self.register_command(cmd_CHATFACE)
377 self.register_command(cmd_REPLY)
378 self.register_command(cmd_PLAYER_ID)
379 self.register_command(cmd_TURN)
380 self.register_command(cmd_PSEUDO_FOV_WIPE)
381 self.register_command(cmd_THING)
382 self.register_command(cmd_THING_TYPE)
383 self.register_command(cmd_THING_NAME)
384 self.register_command(cmd_THING_CHAR)
385 self.register_command(cmd_THING_FACE)
386 self.register_command(cmd_THING_HAT)
387 self.register_command(cmd_THING_CARRYING)
388 self.register_command(cmd_THING_INSTALLED)
389 self.register_command(cmd_TERRAIN)
390 self.register_command(cmd_MAP)
391 self.register_command(cmd_MAP_CONTROL)
392 self.register_command(cmd_PORTAL)
393 self.register_command(cmd_ANNOTATION)
394 self.register_command(cmd_GAME_STATE_COMPLETE)
395 self.register_command(cmd_PLAYERS_HAT_CHARS)
396 self.register_command(cmd_ARGUMENT_ERROR)
397 self.register_command(cmd_GAME_ERROR)
398 self.register_command(cmd_PLAY_ERROR)
399 self.register_command(cmd_TASKS)
400 self.register_command(cmd_FOV)
401 self.register_command(cmd_DEFAULT_COLORS)
402 self.register_command(cmd_RANDOM_COLORS)
403 self.map_content = ''
404 self.players_hat_chars = ''
406 self.annotations = {}
407 self.annotations_new = {}
409 self.portals_new = {}
413 def get_string_options(self, string_option_type):
414 if string_option_type == 'map_geometry':
415 return ['Hex', 'Square']
416 elif string_option_type == 'thing_type':
417 return self.thing_types.keys()
420 def get_command(self, command_name):
421 from functools import partial
422 f = partial(self.commands[command_name], self)
423 f.argtypes = self.commands[command_name].argtypes
426 def get_thing_temp(self, id_):
427 for thing in self.things_new:
434 def __init__(self, name, has_input_prompt=False, shows_info=False,
435 is_intro=False, is_single_char_entry=False):
437 self.short_desc = mode_helps[name]['short']
438 self.available_modes = []
439 self.available_actions = []
440 self.has_input_prompt = has_input_prompt
441 self.shows_info = shows_info
442 self.is_intro = is_intro
443 self.help_intro = mode_helps[name]['long']
444 self.intro_msg = mode_helps[name]['intro']
445 self.is_single_char_entry = is_single_char_entry
448 def iter_available_modes(self, tui):
449 for mode_name in self.available_modes:
450 mode = getattr(tui, 'mode_' + mode_name)
453 key = tui.keys['switch_to_' + mode.name]
456 def list_available_modes(self, tui):
458 if len(self.available_modes) > 0:
459 msg = 'Other modes available from here:\n'
460 for mode, key in self.iter_available_modes(tui):
461 msg += '[%s] – %s\n' % (key, mode.short_desc)
464 def mode_switch_on_key(self, tui, key_pressed):
465 for mode, key in self.iter_available_modes(tui):
466 if key_pressed == key:
467 tui.switch_mode(mode.name)
472 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
473 mode_admin = Mode('admin')
474 mode_play = Mode('play')
475 mode_study = Mode('study', shows_info=True)
476 mode_write = Mode('write', is_single_char_entry=True)
477 mode_edit = Mode('edit')
478 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
479 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
480 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
481 mode_control_tile_draw = Mode('control_tile_draw')
482 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
483 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
484 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
485 mode_chat = Mode('chat', has_input_prompt=True)
486 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
487 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
488 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
489 mode_password = Mode('password', has_input_prompt=True)
490 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
491 mode_command_thing = Mode('command_thing', has_input_prompt=True)
492 mode_take_thing = Mode('take_thing', has_input_prompt=True)
493 mode_drop_thing = Mode('drop_thing', has_input_prompt=True)
494 mode_enter_face = Mode('enter_face', has_input_prompt=True)
495 mode_enter_hat = Mode('enter_hat', has_input_prompt=True)
499 def __init__(self, host):
502 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
503 "command_thing", "take_thing",
505 self.mode_play.available_actions = ["move", "teleport", "door", "consume",
506 "install", "wear", "spin"]
507 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
508 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
509 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
510 "control_tile_type", "chat",
511 "study", "play", "edit"]
512 self.mode_admin.available_actions = ["move"]
513 self.mode_control_tile_draw.available_modes = ["admin_enter"]
514 self.mode_control_tile_draw.available_actions = ["move_explorer",
516 self.mode_edit.available_modes = ["write", "annotate", "portal",
517 "name_thing", "enter_face", "enter_hat", "password",
518 "chat", "study", "play", "admin_enter"]
519 self.mode_edit.available_actions = ["move", "flatten", "install",
525 self.parser = Parser(self.game)
527 self.do_refresh = True
528 self.queue = queue.Queue()
529 self.login_name = None
530 self.map_mode = 'terrain + things'
531 self.password = 'foo'
532 self.switch_mode('waiting_for_server')
534 'switch_to_chat': 't',
535 'switch_to_play': 'p',
536 'switch_to_password': 'P',
537 'switch_to_annotate': 'M',
538 'switch_to_portal': 'T',
539 'switch_to_study': '?',
540 'switch_to_edit': 'E',
541 'switch_to_write': 'm',
542 'switch_to_name_thing': 'N',
543 'switch_to_command_thing': 'O',
544 'switch_to_admin_enter': 'A',
545 'switch_to_control_pw_type': 'C',
546 'switch_to_control_tile_type': 'Q',
547 'switch_to_admin_thing_protect': 'T',
549 'switch_to_enter_face': 'f',
550 'switch_to_enter_hat': 'H',
551 'switch_to_take_thing': 'z',
552 'switch_to_drop_thing': 'u',
560 'toggle_map_mode': 'L',
561 'toggle_tile_draw': 'm',
562 'hex_move_upleft': 'w',
563 'hex_move_upright': 'e',
564 'hex_move_right': 'd',
565 'hex_move_downright': 'x',
566 'hex_move_downleft': 'y',
567 'hex_move_left': 'a',
568 'square_move_up': 'w',
569 'square_move_left': 'a',
570 'square_move_down': 's',
571 'square_move_right': 'd',
573 if os.path.isfile('config.json'):
574 with open('config.json', 'r') as f:
575 keys_conf = json.loads(f.read())
577 self.keys[k] = keys_conf[k]
578 self.show_help = False
579 self.disconnected = True
580 self.force_instant_connect = True
581 self.input_lines = []
585 self.offset = YX(0,0)
586 curses.wrapper(self.loop)
590 def handle_recv(msg):
596 self.log_msg('@ attempting connect')
597 socket_client_class = PlomSocketClient
598 if self.host.startswith('ws://') or self.host.startswith('wss://'):
599 socket_client_class = WebSocketClient
601 self.socket = socket_client_class(handle_recv, self.host)
602 self.socket_thread = threading.Thread(target=self.socket.run)
603 self.socket_thread.start()
604 self.disconnected = False
605 self.game.thing_types = {}
606 self.game.terrains = {}
607 time.sleep(0.1) # give potential SSL negotation some time …
608 self.socket.send('TASKS')
609 self.socket.send('TERRAINS')
610 self.socket.send('THING_TYPES')
611 self.switch_mode('login')
612 except ConnectionRefusedError:
613 self.log_msg('@ server connect failure')
614 self.disconnected = True
615 self.switch_mode('waiting_for_server')
616 self.do_refresh = True
619 self.log_msg('@ attempting reconnect')
621 # necessitated by some strange SSL race conditions with ws4py
622 time.sleep(0.1) # FIXME find out why exactly necessary
623 self.switch_mode('waiting_for_server')
628 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
629 raise BrokenSocketConnection
630 self.socket.send(msg)
631 except (BrokenPipeError, BrokenSocketConnection):
632 self.log_msg('@ server disconnected :(')
633 self.disconnected = True
634 self.force_instant_connect = True
635 self.do_refresh = True
637 def log_msg(self, msg):
639 if len(self.log) > 100:
640 self.log = self.log[-100:]
642 def restore_input_values(self):
643 if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
644 self.input_ = self.game.annotations[self.explorer]
645 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
646 self.input_ = self.game.portals[self.explorer]
647 elif self.mode.name == 'password':
648 self.input_ = self.password
649 elif self.mode.name == 'name_thing':
650 if hasattr(self.thing_selected, 'name'):
651 self.input_ = self.thing_selected.name
652 elif self.mode.name == 'admin_thing_protect':
653 if hasattr(self.thing_selected, 'protection'):
654 self.input_ = self.thing_selected.protection
656 def send_tile_control_command(self):
657 self.send('SET_TILE_CONTROL %s %s' %
658 (self.explorer, quote(self.tile_control_char)))
660 def toggle_map_mode(self):
661 if self.map_mode == 'terrain only':
662 self.map_mode = 'terrain + annotations'
663 elif self.map_mode == 'terrain + annotations':
664 self.map_mode = 'terrain + things'
665 elif self.map_mode == 'terrain + things':
666 self.map_mode = 'protections'
667 elif self.map_mode == 'protections':
668 self.map_mode = 'terrain only'
670 def switch_mode(self, mode_name):
672 def fail(msg, return_mode='play'):
673 self.log_msg('? ' + msg)
675 self.switch_mode(return_mode)
677 if self.mode and self.mode.name == 'control_tile_draw':
678 self.log_msg('@ finished tile protection drawing.')
679 self.draw_face = False
680 self.tile_draw = False
681 if mode_name == 'command_thing' and\
682 (not self.game.player.carrying or
683 not self.game.player.carrying.commandable):
684 return fail('not carrying anything commandable')
685 if mode_name == 'take_thing' and self.game.player.carrying:
686 return fail('already carrying something')
687 if mode_name == 'drop_thing' and not self.game.player.carrying:
688 return fail('not carrying anything droppable')
689 if mode_name == 'admin_enter' and self.is_admin:
691 elif mode_name in {'name_thing', 'admin_thing_protect'}:
693 for t in [t for t in self.game.things
694 if t.position == self.game.player.position
695 and t.id_ != self.game.player.id_]:
699 return fail('not standing over thing', 'edit')
701 self.thing_selected = thing
702 self.mode = getattr(self, 'mode_' + mode_name)
703 if self.mode.name in {'control_tile_draw', 'control_tile_type',
705 self.map_mode = 'protections'
706 elif self.mode.name != 'edit':
707 self.map_mode = 'terrain + things'
708 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
709 self.explorer = YX(self.game.player.position.y,
710 self.game.player.position.x)
711 if self.mode.is_single_char_entry:
712 self.show_help = True
713 if len(self.mode.intro_msg) > 0:
714 self.log_msg(self.mode.intro_msg)
715 if self.mode.name == 'login':
717 self.send('LOGIN ' + quote(self.login_name))
719 self.log_msg('@ enter username')
720 elif self.mode.name == 'take_thing':
721 self.log_msg('Portable things in reach for pick-up:')
722 select_range = [self.game.player.position,
723 self.game.player.position + YX(0,-1),
724 self.game.player.position + YX(0, 1),
725 self.game.player.position + YX(-1, 0),
726 self.game.player.position + YX(1, 0)]
727 if type(self.game.map_geometry) == MapGeometryHex:
728 if self.game.player.position.y % 2:
729 select_range += [self.game.player.position + YX(-1, 1),
730 self.game.player.position + YX(1, 1)]
732 select_range += [self.game.player.position + YX(-1, -1),
733 self.game.player.position + YX(1, -1)]
734 self.selectables = [t.id_ for t in self.game.things
735 if t.portable and t.position in select_range]
736 if len(self.selectables) == 0:
737 return fail('nothing to pick-up')
739 for i in range(len(self.selectables)):
740 t = self.game.get_thing(self.selectables[i])
741 self.log_msg(str(i) + ': ' + self.get_thing_info(t))
742 elif self.mode.name == 'drop_thing':
743 self.log_msg('Direction to drop thing to:')
745 ['HERE'] + list(self.game.tui.movement_keys.values())
746 for i in range(len(self.selectables)):
747 self.log_msg(str(i) + ': ' + self.selectables[i])
748 elif self.mode.name == 'enter_hat':
749 self.log_msg('legal characters: ' + self.game.players_hat_chars)
750 elif self.mode.name == 'command_thing':
751 self.send('TASK:COMMAND ' + quote('HELP'))
752 elif self.mode.name == 'control_pw_pw':
753 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
754 elif self.mode.name == 'control_tile_draw':
755 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']))
757 self.restore_input_values()
759 def set_default_colors(self):
760 curses.init_color(1, 1000, 1000, 1000)
761 curses.init_color(2, 0, 0, 0)
762 self.do_refresh = True
764 def set_random_colors(self):
768 return int(offset + random.random()*375)
770 curses.init_color(1, rand(625), rand(625), rand(625))
771 curses.init_color(2, rand(0), rand(0), rand(0))
772 self.do_refresh = True
776 return self.info_cached
777 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
779 if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
780 info_to_cache += 'outside field of view'
782 for t in self.game.things:
783 if t.position == self.explorer:
784 info_to_cache += 'THING: %s' % self.get_thing_info(t)
785 protection = t.protection
786 if protection == '.':
788 info_to_cache += ' / protection: %s\n' % protection
789 if hasattr(t, 'hat'):
790 info_to_cache += t.hat[0:6] + '\n'
791 info_to_cache += t.hat[6:12] + '\n'
792 info_to_cache += t.hat[12:18] + '\n'
793 if hasattr(t, 'face'):
794 info_to_cache += t.face[0:6] + '\n'
795 info_to_cache += t.face[6:12] + '\n'
796 info_to_cache += t.face[12:18] + '\n'
797 terrain_char = self.game.map_content[pos_i]
799 if terrain_char in self.game.terrains:
800 terrain_desc = self.game.terrains[terrain_char]
801 info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
803 protection = self.game.map_control_content[pos_i]
804 if protection == '.':
805 protection = 'unprotected'
806 info_to_cache += 'PROTECTION: %s\n' % protection
807 if self.explorer in self.game.portals:
808 info_to_cache += 'PORTAL: ' +\
809 self.game.portals[self.explorer] + '\n'
811 info_to_cache += 'PORTAL: (none)\n'
812 if self.explorer in self.game.annotations:
813 info_to_cache += 'ANNOTATION: ' +\
814 self.game.annotations[self.explorer]
815 self.info_cached = info_to_cache
816 return self.info_cached
818 def get_thing_info(self, t):
820 (t.type_, self.game.thing_types[t.type_])
821 if hasattr(t, 'thing_char'):
823 if hasattr(t, 'name'):
824 info += ' (%s)' % t.name
825 if hasattr(t, 'installed'):
826 info += ' / installed'
829 def loop(self, stdscr):
832 def safe_addstr(y, x, line):
833 if y < self.size.y - 1 or x + len(line) < self.size.x:
834 stdscr.addstr(y, x, line, curses.color_pair(1))
835 else: # workaround to <https://stackoverflow.com/q/7063128>
836 cut_i = self.size.x - x - 1
838 last_char = line[cut_i]
839 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
840 stdscr.insstr(y, self.size.x - 2, ' ')
841 stdscr.addstr(y, x, cut, curses.color_pair(1))
843 def handle_input(msg):
844 command, args = self.parser.parse(msg)
847 def task_action_on(action):
848 return action_tasks[action] in self.game.tasks
850 def msg_into_lines_of_width(msg, width):
854 for i in range(len(msg)):
855 if x >= width or msg[i] == "\n":
867 def reset_screen_size():
868 self.size = YX(*stdscr.getmaxyx())
869 self.size = self.size - YX(self.size.y % 4, 0)
870 self.size = self.size - YX(0, self.size.x % 4)
871 self.window_width = int(self.size.x / 2)
873 def recalc_input_lines():
874 if not self.mode.has_input_prompt:
875 self.input_lines = []
877 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
880 def move_explorer(direction):
881 target = self.game.map_geometry.move_yx(self.explorer, direction)
883 self.info_cached = None
884 self.explorer = target
886 self.send_tile_control_command()
892 for line in self.log:
893 lines += msg_into_lines_of_width(line, self.window_width)
896 max_y = self.size.y - len(self.input_lines)
897 for i in range(len(lines)):
898 if (i >= max_y - height_header):
900 safe_addstr(max_y - i - 1, self.window_width, lines[i])
903 info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
904 lines = msg_into_lines_of_width(info, self.window_width)
906 for i in range(len(lines)):
907 y = height_header + i
908 if y >= self.size.y - len(self.input_lines):
910 safe_addstr(y, self.window_width, lines[i])
913 y = self.size.y - len(self.input_lines)
914 for i in range(len(self.input_lines)):
915 safe_addstr(y, self.window_width, self.input_lines[i])
919 if not self.game.turn_complete:
921 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
924 help = "hit [%s] for help" % self.keys['help']
925 if self.mode.has_input_prompt:
926 help = "enter /help for help"
927 safe_addstr(1, self.window_width,
928 'MODE: %s – %s' % (self.mode.short_desc, help))
931 if (not self.game.turn_complete) and len(self.map_lines) == 0:
933 if self.game.turn_complete:
935 for y in range(self.game.map_geometry.size.y):
936 start = self.game.map_geometry.size.x * y
937 end = start + self.game.map_geometry.size.x
938 if self.map_mode == 'protections':
939 map_lines_split += [[c + ' ' for c
940 in self.game.map_control_content[start:end]]]
942 map_lines_split += [[c + ' ' for c
943 in self.game.map_content[start:end]]]
944 if self.map_mode == 'terrain + annotations':
945 for p in self.game.annotations:
946 map_lines_split[p.y][p.x] = 'A '
947 elif self.map_mode == 'terrain + things':
948 for p in self.game.portals.keys():
949 original = map_lines_split[p.y][p.x]
950 map_lines_split[p.y][p.x] = original[0] + 'P'
953 def draw_thing(t, used_positions):
954 symbol = self.game.thing_types[t.type_]
956 if hasattr(t, 'thing_char'):
957 meta_char = t.thing_char
958 if t.position in used_positions:
960 if hasattr(t, 'carrying') and t.carrying:
962 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
963 used_positions += [t.position]
965 for t in [t for t in self.game.things if t.type_ != 'Player']:
966 draw_thing(t, used_positions)
967 for t in [t for t in self.game.things if t.type_ == 'Player']:
968 draw_thing(t, used_positions)
969 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
970 map_lines_split[self.explorer.y][self.explorer.x] = '??'
971 elif self.map_mode != 'terrain + things':
972 map_lines_split[self.game.player.position.y]\
973 [self.game.player.position.x] = '??'
975 if type(self.game.map_geometry) == MapGeometryHex:
977 for line in map_lines_split:
978 self.map_lines += [indent * ' ' + ''.join(line)]
979 indent = 0 if indent else 1
981 for line in map_lines_split:
982 self.map_lines += [''.join(line)]
983 window_center = YX(int(self.size.y / 2),
984 int(self.window_width / 2))
985 center = self.game.player.position
986 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
987 center = self.explorer
988 center = YX(center.y, center.x * 2)
989 self.offset = center - window_center
990 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
991 self.offset += YX(0, 1)
992 term_y = max(0, -self.offset.y)
993 term_x = max(0, -self.offset.x)
994 map_y = max(0, self.offset.y)
995 map_x = max(0, self.offset.x)
996 while term_y < self.size.y and map_y < len(self.map_lines):
997 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
998 safe_addstr(term_y, term_x, to_draw)
1002 def draw_face_popup():
1003 t = self.game.get_thing(self.draw_face)
1004 if not t or not hasattr(t, 'face'):
1005 self.draw_face = False
1008 start_x = self.window_width - 10
1010 if hasattr(t, 'thing_char'):
1011 t_char = t.thing_char
1012 def draw_body_part(body_part, end_y):
1013 safe_addstr(end_y - 4, start_x, ' _[ @' + t_char + ' ]_ ')
1014 safe_addstr(end_y - 3, start_x, '| |')
1015 safe_addstr(end_y - 2, start_x, '| ' + body_part[0:6] + ' |')
1016 safe_addstr(end_y - 1, start_x, '| ' + body_part[6:12] + ' |')
1017 safe_addstr(end_y, start_x, '| ' + body_part[12:18] + ' |')
1019 if hasattr(t, 'face'):
1020 draw_body_part(t.face, self.size.y - 2)
1021 if hasattr(t, 'hat'):
1022 draw_body_part(t.hat, self.size.y - 5)
1023 safe_addstr(self.size.y - 1, start_x, '| |')
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:
1034 if action == 'move_explorer':
1036 if action == 'move':
1037 key = ','.join(self.movement_keys)
1039 key = self.keys[action]
1040 content += '[%s] – %s\n' % (key, action_descriptions[action])
1042 content += self.mode.list_available_modes(self)
1043 for i in range(self.size.y):
1045 self.window_width * (not self.mode.has_input_prompt),
1046 ' ' * self.window_width)
1048 for line in content.split('\n'):
1049 lines += msg_into_lines_of_width(line, self.window_width)
1050 for i in range(len(lines)):
1051 if i >= self.size.y:
1054 self.window_width * (not self.mode.has_input_prompt),
1059 stdscr.bkgd(' ', curses.color_pair(1))
1060 recalc_input_lines()
1061 if self.mode.has_input_prompt:
1063 if self.mode.shows_info:
1068 if not self.mode.is_intro:
1073 if self.draw_face and self.mode.name in {'chat', 'play'}:
1076 def pick_selectable(task_name):
1078 i = int(self.input_)
1079 if i < 0 or i >= len(self.selectables):
1080 self.log_msg('? invalid index, aborted')
1082 self.send('TASK:%s %s' % (task_name, self.selectables[i]))
1084 self.log_msg('? invalid index, aborted')
1086 self.switch_mode('play')
1088 action_descriptions = {
1090 'flatten': 'flatten surroundings',
1091 'teleport': 'teleport',
1092 'take_thing': 'pick up thing',
1093 'drop_thing': 'drop thing',
1094 'toggle_map_mode': 'toggle map view',
1095 'toggle_tile_draw': 'toggle protection character drawing',
1096 'install': '(un-)install',
1097 'wear': '(un-)wear',
1098 'door': 'open/close',
1099 'consume': 'consume',
1104 'flatten': 'FLATTEN_SURROUNDINGS',
1105 'take_thing': 'PICK_UP',
1106 'drop_thing': 'DROP',
1108 'install': 'INSTALL',
1111 'command': 'COMMAND',
1112 'consume': 'INTOXICATE',
1116 curses.curs_set(False) # hide cursor
1117 curses.start_color()
1118 self.set_default_colors()
1119 curses.init_pair(1, 1, 2)
1122 self.explorer = YX(0, 0)
1125 interval = datetime.timedelta(seconds=5)
1126 last_ping = datetime.datetime.now() - interval
1128 if self.disconnected and self.force_instant_connect:
1129 self.force_instant_connect = False
1131 now = datetime.datetime.now()
1132 if now - last_ping > interval:
1133 if self.disconnected:
1143 self.do_refresh = False
1146 msg = self.queue.get(block=False)
1151 key = stdscr.getkey()
1152 self.do_refresh = True
1153 except curses.error:
1158 self.show_help = False
1159 self.draw_face = False
1160 if key == 'KEY_RESIZE':
1162 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1163 self.input_ = self.input_[:-1]
1164 elif (((not self.mode.is_intro) and keycode == 27) # Escape
1165 or (self.mode.has_input_prompt and key == '\n'
1166 and self.input_ == ''\
1167 and self.mode.name in {'chat', 'command_thing',
1168 'take_thing', 'drop_thing',
1170 if self.mode.name not in {'chat', 'play', 'study', 'edit'}:
1171 self.log_msg('@ aborted')
1172 self.switch_mode('play')
1173 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1174 self.show_help = True
1176 self.restore_input_values()
1177 elif self.mode.has_input_prompt and key != '\n': # Return key
1179 max_length = self.window_width * self.size.y - len(input_prompt) - 1
1180 if len(self.input_) > max_length:
1181 self.input_ = self.input_[:max_length]
1182 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1183 self.show_help = True
1184 elif self.mode.name == 'login' and key == '\n':
1185 self.login_name = self.input_
1186 self.send('LOGIN ' + quote(self.input_))
1188 elif self.mode.name == 'enter_face' and key == '\n':
1189 if len(self.input_) != 18:
1190 self.log_msg('? wrong input length, aborting')
1192 self.send('PLAYER_FACE %s' % quote(self.input_))
1194 self.switch_mode('edit')
1195 elif self.mode.name == 'enter_hat' and key == '\n':
1196 if len(self.input_) != 18:
1197 self.log_msg('? wrong input length, aborting')
1199 self.send('PLAYER_HAT %s' % quote(self.input_))
1201 self.switch_mode('edit')
1202 elif self.mode.name == 'take_thing' and key == '\n':
1203 pick_selectable('PICK_UP')
1204 elif self.mode.name == 'drop_thing' and key == '\n':
1205 pick_selectable('DROP')
1206 elif self.mode.name == 'command_thing' and key == '\n':
1207 self.send('TASK:COMMAND ' + quote(self.input_))
1209 elif self.mode.name == 'control_pw_pw' and key == '\n':
1210 if self.input_ == '':
1211 self.log_msg('@ aborted')
1213 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1214 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1215 self.switch_mode('admin')
1216 elif self.mode.name == 'password' and key == '\n':
1217 if self.input_ == '':
1219 self.password = self.input_
1220 self.switch_mode('edit')
1221 elif self.mode.name == 'admin_enter' and key == '\n':
1222 self.send('BECOME_ADMIN ' + quote(self.input_))
1223 self.switch_mode('play')
1224 elif self.mode.name == 'control_pw_type' and key == '\n':
1225 if len(self.input_) != 1:
1226 self.log_msg('@ entered non-single-char, therefore aborted')
1227 self.switch_mode('admin')
1229 self.tile_control_char = self.input_
1230 self.switch_mode('control_pw_pw')
1231 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1232 if len(self.input_) != 1:
1233 self.log_msg('@ entered non-single-char, therefore aborted')
1235 self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
1236 quote(self.input_)))
1237 self.log_msg('@ sent new protection character for thing')
1238 self.switch_mode('admin')
1239 elif self.mode.name == 'control_tile_type' and key == '\n':
1240 if len(self.input_) != 1:
1241 self.log_msg('@ entered non-single-char, therefore aborted')
1242 self.switch_mode('admin')
1244 self.tile_control_char = self.input_
1245 self.switch_mode('control_tile_draw')
1246 elif self.mode.name == 'chat' and key == '\n':
1247 if self.input_ == '':
1249 if self.input_[0] == '/':
1250 if self.input_.startswith('/nick'):
1251 tokens = self.input_.split(maxsplit=1)
1252 if len(tokens) == 2:
1253 self.send('NICK ' + quote(tokens[1]))
1255 self.log_msg('? need login name')
1257 self.log_msg('? unknown command')
1259 self.send('ALL ' + quote(self.input_))
1261 elif self.mode.name == 'name_thing' and key == '\n':
1262 if self.input_ == '':
1264 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
1266 quote(self.password)))
1267 self.switch_mode('edit')
1268 elif self.mode.name == 'annotate' and key == '\n':
1269 if self.input_ == '':
1271 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1272 quote(self.password)))
1273 self.switch_mode('edit')
1274 elif self.mode.name == 'portal' and key == '\n':
1275 if self.input_ == '':
1277 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1278 quote(self.password)))
1279 self.switch_mode('edit')
1280 elif self.mode.name == 'study':
1281 if self.mode.mode_switch_on_key(self, key):
1283 elif key == self.keys['toggle_map_mode']:
1284 self.toggle_map_mode()
1285 elif key in self.movement_keys:
1286 move_explorer(self.movement_keys[key])
1287 elif self.mode.name == 'play':
1288 if self.mode.mode_switch_on_key(self, key):
1290 elif key == self.keys['door'] and task_action_on('door'):
1291 self.send('TASK:DOOR')
1292 elif key == self.keys['consume'] and task_action_on('consume'):
1293 self.send('TASK:INTOXICATE')
1294 elif key == self.keys['wear'] and task_action_on('wear'):
1295 self.send('TASK:WEAR')
1296 elif key == self.keys['spin'] and task_action_on('spin'):
1297 self.send('TASK:SPIN')
1298 elif key == self.keys['teleport']:
1299 if self.game.player.position in self.game.portals:
1300 self.host = self.game.portals[self.game.player.position]
1304 self.log_msg('? not standing on portal')
1305 elif key in self.movement_keys and task_action_on('move'):
1306 self.send('TASK:MOVE ' + self.movement_keys[key])
1307 elif self.mode.name == 'write':
1308 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1309 self.switch_mode('edit')
1310 elif self.mode.name == 'control_tile_draw':
1311 if self.mode.mode_switch_on_key(self, key):
1313 elif key in self.movement_keys:
1314 move_explorer(self.movement_keys[key])
1315 elif key == self.keys['toggle_tile_draw']:
1316 self.tile_draw = False if self.tile_draw else True
1317 elif self.mode.name == 'admin':
1318 if self.mode.mode_switch_on_key(self, key):
1320 elif key in self.movement_keys and task_action_on('move'):
1321 self.send('TASK:MOVE ' + self.movement_keys[key])
1322 elif self.mode.name == 'edit':
1323 if self.mode.mode_switch_on_key(self, key):
1325 elif key == self.keys['flatten'] and task_action_on('flatten'):
1326 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1327 elif key == self.keys['install'] and task_action_on('install'):
1328 self.send('TASK:INSTALL %s' % quote(self.password))
1329 elif key == self.keys['toggle_map_mode']:
1330 self.toggle_map_mode()
1331 elif key in self.movement_keys and task_action_on('move'):
1332 self.send('TASK:MOVE ' + self.movement_keys[key])
1334 if len(sys.argv) != 2:
1335 raise ArgError('wrong number of arguments, need game host')