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': 'change terrain',
62 '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.'
65 'short': 'change protection character password',
66 'intro': '@ enter protection character for which you want to change the password:',
67 '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.'
70 'short': 'change protection character password',
72 '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.'
74 'control_tile_type': {
75 'short': 'change tiles protection',
76 'intro': '@ enter protection character which you want to draw:',
77 '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.'
79 'control_tile_draw': {
80 'short': 'change tiles protection',
82 '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.'
85 'short': 'annotate tile',
87 '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.'
90 'short': 'edit portal',
92 '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.'
97 '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'
102 'long': 'Enter your player name.'
104 'waiting_for_server': {
105 'short': 'waiting for server response',
106 'intro': '@ waiting for server …',
107 'long': 'Waiting for a server response.'
110 'short': 'waiting for server response',
112 'long': 'Waiting for a server response.'
115 'short': 'set world edit password',
117 '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.'
120 'short': 'become admin',
121 'intro': '@ enter admin password:',
122 'long': 'This mode allows you to become admin if you know an admin password.'
127 'long': 'This mode allows you access to actions limited to administrators.'
131 from ws4py.client import WebSocketBaseClient
132 class WebSocketClient(WebSocketBaseClient):
134 def __init__(self, recv_handler, *args, **kwargs):
135 super().__init__(*args, **kwargs)
136 self.recv_handler = recv_handler
139 def received_message(self, message):
141 message = str(message)
142 self.recv_handler(message)
145 def plom_closed(self):
146 return self.client_terminated
148 from plomrogue.io_tcp import PlomSocket
149 class PlomSocketClient(PlomSocket):
151 def __init__(self, recv_handler, url):
153 self.recv_handler = recv_handler
154 host, port = url.split(':')
155 super().__init__(socket.create_connection((host, port)))
163 for msg in self.recv():
164 if msg == 'NEED_SSL':
165 self.socket = ssl.wrap_socket(self.socket)
167 self.recv_handler(msg)
168 except BrokenSocketConnection:
169 pass # we assume socket will be known as dead by now
171 def cmd_TURN(game, n):
172 game.annotations = {}
176 game.turn_complete = False
178 cmd_TURN.argtypes = 'int:nonneg'
180 def cmd_LOGIN_OK(game):
181 game.tui.switch_mode('post_login_wait')
182 game.tui.send('GET_GAMESTATE')
183 game.tui.log_msg('@ welcome')
184 cmd_LOGIN_OK.argtypes = ''
186 def cmd_ADMIN_OK(game):
187 game.tui.is_admin = True
188 game.tui.log_msg('@ you now have admin rights')
189 game.tui.switch_mode('admin')
190 game.tui.do_refresh = True
191 cmd_ADMIN_OK.argtypes = ''
193 def cmd_REPLY(game, msg):
194 game.tui.log_msg('#MUSICPLAYER: ' + msg)
195 game.tui.do_refresh = True
196 cmd_REPLY.argtypes = 'string'
198 def cmd_CHAT(game, msg):
199 game.tui.log_msg('# ' + msg)
200 game.tui.do_refresh = True
201 cmd_CHAT.argtypes = 'string'
203 def cmd_PLAYER_ID(game, player_id):
204 game.player_id = player_id
205 cmd_PLAYER_ID.argtypes = 'int:nonneg'
207 def cmd_THING(game, yx, thing_type, protection, thing_id, portable, commandable):
208 t = game.get_thing(thing_id)
210 t = ThingBase(game, thing_id)
214 t.protection = protection
215 t.portable = portable
216 t.commandable = commandable
217 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg bool bool'
219 def cmd_THING_NAME(game, thing_id, name):
220 t = game.get_thing(thing_id)
222 cmd_THING_NAME.argtypes = 'int:pos string'
224 def cmd_THING_FACE(game, thing_id, face):
225 t = game.get_thing(thing_id)
227 cmd_THING_FACE.argtypes = 'int:pos string'
229 def cmd_THING_HAT(game, thing_id, hat):
230 t = game.get_thing(thing_id)
232 cmd_THING_HAT.argtypes = 'int:pos string'
234 def cmd_THING_CHAR(game, thing_id, c):
235 t = game.get_thing(thing_id)
237 cmd_THING_CHAR.argtypes = 'int:pos char'
239 def cmd_MAP(game, geometry, size, content):
240 map_geometry_class = globals()['MapGeometry' + geometry]
241 game.map_geometry = map_geometry_class(size)
242 game.map_content = content
243 if type(game.map_geometry) == MapGeometrySquare:
244 game.tui.movement_keys = {
245 game.tui.keys['square_move_up']: 'UP',
246 game.tui.keys['square_move_left']: 'LEFT',
247 game.tui.keys['square_move_down']: 'DOWN',
248 game.tui.keys['square_move_right']: 'RIGHT',
250 elif type(game.map_geometry) == MapGeometryHex:
251 game.tui.movement_keys = {
252 game.tui.keys['hex_move_upleft']: 'UPLEFT',
253 game.tui.keys['hex_move_upright']: 'UPRIGHT',
254 game.tui.keys['hex_move_right']: 'RIGHT',
255 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
256 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
257 game.tui.keys['hex_move_left']: 'LEFT',
259 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
261 def cmd_FOV(game, content):
263 cmd_FOV.argtypes = 'string'
265 def cmd_MAP_CONTROL(game, content):
266 game.map_control_content = content
267 cmd_MAP_CONTROL.argtypes = 'string'
269 def cmd_GAME_STATE_COMPLETE(game):
270 if game.tui.mode.name == 'post_login_wait':
271 game.tui.switch_mode('play')
272 game.turn_complete = True
273 game.tui.do_refresh = True
274 game.tui.info_cached = None
275 cmd_GAME_STATE_COMPLETE.argtypes = ''
277 def cmd_PORTAL(game, position, msg):
278 game.portals[position] = msg
279 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
281 def cmd_PLAY_ERROR(game, msg):
282 game.tui.log_msg('? ' + msg)
283 game.tui.flash = True
284 game.tui.do_refresh = True
285 cmd_PLAY_ERROR.argtypes = 'string'
287 def cmd_GAME_ERROR(game, msg):
288 game.tui.log_msg('? game error: ' + msg)
289 game.tui.do_refresh = True
290 cmd_GAME_ERROR.argtypes = 'string'
292 def cmd_ARGUMENT_ERROR(game, msg):
293 game.tui.log_msg('? syntax error: ' + msg)
294 game.tui.do_refresh = True
295 cmd_ARGUMENT_ERROR.argtypes = 'string'
297 def cmd_ANNOTATION(game, position, msg):
298 game.annotations[position] = msg
299 if game.tui.mode.shows_info:
300 game.tui.do_refresh = True
301 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
303 def cmd_TASKS(game, tasks_comma_separated):
304 game.tasks = tasks_comma_separated.split(',')
305 game.tui.mode_write.legal = 'WRITE' in game.tasks
306 game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks
307 game.tui.mode_take_thing.legal = 'PICK_UP' in game.tasks
308 game.tui.mode_drop_thing.legal = 'DROP' in game.tasks
309 cmd_TASKS.argtypes = 'string'
311 def cmd_THING_TYPE(game, thing_type, symbol_hint):
312 game.thing_types[thing_type] = symbol_hint
313 cmd_THING_TYPE.argtypes = 'string char'
315 def cmd_THING_INSTALLED(game, thing_id):
316 game.get_thing(thing_id).installed = True
317 cmd_THING_INSTALLED.argtypes = 'int:pos'
319 def cmd_THING_CARRYING(game, thing_id, carried_id):
320 game.get_thing(thing_id).carrying = game.get_thing(carried_id)
321 cmd_THING_CARRYING.argtypes = 'int:pos int:pos'
323 def cmd_TERRAIN(game, terrain_char, terrain_desc):
324 game.terrains[terrain_char] = terrain_desc
325 cmd_TERRAIN.argtypes = 'char string'
329 cmd_PONG.argtypes = ''
331 def cmd_DEFAULT_COLORS(game):
332 game.tui.set_default_colors()
333 cmd_DEFAULT_COLORS.argtypes = ''
335 def cmd_RANDOM_COLORS(game):
336 game.tui.set_random_colors()
337 cmd_RANDOM_COLORS.argtypes = ''
339 class Game(GameBase):
340 turn_complete = False
344 def __init__(self, *args, **kwargs):
345 super().__init__(*args, **kwargs)
346 self.register_command(cmd_LOGIN_OK)
347 self.register_command(cmd_ADMIN_OK)
348 self.register_command(cmd_PONG)
349 self.register_command(cmd_CHAT)
350 self.register_command(cmd_REPLY)
351 self.register_command(cmd_PLAYER_ID)
352 self.register_command(cmd_TURN)
353 self.register_command(cmd_THING)
354 self.register_command(cmd_THING_TYPE)
355 self.register_command(cmd_THING_NAME)
356 self.register_command(cmd_THING_CHAR)
357 self.register_command(cmd_THING_FACE)
358 self.register_command(cmd_THING_HAT)
359 self.register_command(cmd_THING_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_ARGUMENT_ERROR)
368 self.register_command(cmd_GAME_ERROR)
369 self.register_command(cmd_PLAY_ERROR)
370 self.register_command(cmd_TASKS)
371 self.register_command(cmd_FOV)
372 self.register_command(cmd_DEFAULT_COLORS)
373 self.register_command(cmd_RANDOM_COLORS)
374 self.map_content = ''
376 self.annotations = {}
380 def get_string_options(self, string_option_type):
381 if string_option_type == 'map_geometry':
382 return ['Hex', 'Square']
383 elif string_option_type == 'thing_type':
384 return self.thing_types.keys()
387 def get_command(self, command_name):
388 from functools import partial
389 f = partial(self.commands[command_name], self)
390 f.argtypes = self.commands[command_name].argtypes
395 def __init__(self, name, has_input_prompt=False, shows_info=False,
396 is_intro=False, is_single_char_entry=False):
398 self.short_desc = mode_helps[name]['short']
399 self.available_modes = []
400 self.available_actions = []
401 self.has_input_prompt = has_input_prompt
402 self.shows_info = shows_info
403 self.is_intro = is_intro
404 self.help_intro = mode_helps[name]['long']
405 self.intro_msg = mode_helps[name]['intro']
406 self.is_single_char_entry = is_single_char_entry
409 def iter_available_modes(self, tui):
410 for mode_name in self.available_modes:
411 mode = getattr(tui, 'mode_' + mode_name)
414 key = tui.keys['switch_to_' + mode.name]
417 def list_available_modes(self, tui):
419 if len(self.available_modes) > 0:
420 msg = 'Other modes available from here:\n'
421 for mode, key in self.iter_available_modes(tui):
422 msg += '[%s] – %s\n' % (key, mode.short_desc)
425 def mode_switch_on_key(self, tui, key_pressed):
426 for mode, key in self.iter_available_modes(tui):
427 if key_pressed == key:
428 tui.switch_mode(mode.name)
433 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
434 mode_admin = Mode('admin')
435 mode_play = Mode('play')
436 mode_study = Mode('study', shows_info=True)
437 mode_write = Mode('write', is_single_char_entry=True)
438 mode_edit = Mode('edit')
439 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
440 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
441 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
442 mode_control_tile_draw = Mode('control_tile_draw')
443 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
444 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
445 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
446 mode_chat = Mode('chat', has_input_prompt=True)
447 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
448 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
449 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
450 mode_password = Mode('password', has_input_prompt=True)
451 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
452 mode_command_thing = Mode('command_thing', has_input_prompt=True)
453 mode_take_thing = Mode('take_thing', has_input_prompt=True)
454 mode_drop_thing = Mode('drop_thing', has_input_prompt=True)
455 mode_enter_face = Mode('enter_face', has_input_prompt=True)
459 def __init__(self, host):
462 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
463 "command_thing", "take_thing",
465 self.mode_play.available_actions = ["move", "teleport", "door", "consume",
466 "install", "wear", "spin"]
467 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
468 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
469 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
470 "control_tile_type", "chat",
471 "study", "play", "edit"]
472 self.mode_admin.available_actions = ["move"]
473 self.mode_control_tile_draw.available_modes = ["admin_enter"]
474 self.mode_control_tile_draw.available_actions = ["move_explorer",
476 self.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
477 "password", "chat", "study", "play",
478 "admin_enter", "enter_face"]
479 self.mode_edit.available_actions = ["move", "flatten", "toggle_map_mode"]
484 self.parser = Parser(self.game)
486 self.do_refresh = True
487 self.queue = queue.Queue()
488 self.login_name = None
489 self.map_mode = 'terrain + things'
490 self.password = 'foo'
491 self.switch_mode('waiting_for_server')
493 'switch_to_chat': 't',
494 'switch_to_play': 'p',
495 'switch_to_password': 'P',
496 'switch_to_annotate': 'M',
497 'switch_to_portal': 'T',
498 'switch_to_study': '?',
499 'switch_to_edit': 'E',
500 'switch_to_write': 'm',
501 'switch_to_name_thing': 'N',
502 'switch_to_command_thing': 'O',
503 'switch_to_admin_enter': 'A',
504 'switch_to_control_pw_type': 'C',
505 'switch_to_control_tile_type': 'Q',
506 'switch_to_admin_thing_protect': 'T',
508 'switch_to_enter_face': 'f',
509 'switch_to_take_thing': 'z',
510 'switch_to_drop_thing': 'u',
518 'toggle_map_mode': 'L',
519 'toggle_tile_draw': 'm',
520 'hex_move_upleft': 'w',
521 'hex_move_upright': 'e',
522 'hex_move_right': 'd',
523 'hex_move_downright': 'x',
524 'hex_move_downleft': 'y',
525 'hex_move_left': 'a',
526 'square_move_up': 'w',
527 'square_move_left': 'a',
528 'square_move_down': 's',
529 'square_move_right': 'd',
531 if os.path.isfile('config.json'):
532 with open('config.json', 'r') as f:
533 keys_conf = json.loads(f.read())
535 self.keys[k] = keys_conf[k]
536 self.show_help = False
537 self.disconnected = True
538 self.force_instant_connect = True
539 self.input_lines = []
543 self.offset = YX(0,0)
544 curses.wrapper(self.loop)
548 def handle_recv(msg):
554 self.log_msg('@ attempting connect')
555 socket_client_class = PlomSocketClient
556 if self.host.startswith('ws://') or self.host.startswith('wss://'):
557 socket_client_class = WebSocketClient
559 self.socket = socket_client_class(handle_recv, self.host)
560 self.socket_thread = threading.Thread(target=self.socket.run)
561 self.socket_thread.start()
562 self.disconnected = False
563 self.game.thing_types = {}
564 self.game.terrains = {}
565 time.sleep(0.1) # give potential SSL negotation some time …
566 self.socket.send('TASKS')
567 self.socket.send('TERRAINS')
568 self.socket.send('THING_TYPES')
569 self.switch_mode('login')
570 except ConnectionRefusedError:
571 self.log_msg('@ server connect failure')
572 self.disconnected = True
573 self.switch_mode('waiting_for_server')
574 self.do_refresh = True
577 self.log_msg('@ attempting reconnect')
579 # necessitated by some strange SSL race conditions with ws4py
580 time.sleep(0.1) # FIXME find out why exactly necessary
581 self.switch_mode('waiting_for_server')
586 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
587 raise BrokenSocketConnection
588 self.socket.send(msg)
589 except (BrokenPipeError, BrokenSocketConnection):
590 self.log_msg('@ server disconnected :(')
591 self.disconnected = True
592 self.force_instant_connect = True
593 self.do_refresh = True
595 def log_msg(self, msg):
597 if len(self.log) > 100:
598 self.log = self.log[-100:]
600 def restore_input_values(self):
601 if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
602 self.input_ = self.game.annotations[self.explorer]
603 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
604 self.input_ = self.game.portals[self.explorer]
605 elif self.mode.name == 'password':
606 self.input_ = self.password
607 elif self.mode.name == 'name_thing':
608 if hasattr(self.thing_selected, 'name'):
609 self.input_ = self.thing_selected.name
610 elif self.mode.name == 'admin_thing_protect':
611 if hasattr(self.thing_selected, 'protection'):
612 self.input_ = self.thing_selected.protection
614 def send_tile_control_command(self):
615 self.send('SET_TILE_CONTROL %s %s' %
616 (self.explorer, quote(self.tile_control_char)))
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'
628 def switch_mode(self, mode_name):
629 if self.mode and self.mode.name == 'control_tile_draw':
630 self.log_msg('@ finished tile protection drawing.')
631 self.tile_draw = False
632 player = self.game.get_thing(self.game.player_id)
633 if mode_name == 'command_thing' and\
634 (not hasattr(player, 'carrying') or not player.carrying.commandable):
635 self.log_msg('? not carrying anything commandable')
637 self.switch_mode('play')
639 if mode_name == 'drop_thing' and\
640 not (hasattr(player, 'carrying' or player.carrying)):
641 self.log_msg('? not carrying anything droppable')
643 self.switch_mode('play')
645 if mode_name == 'admin_enter' and self.is_admin:
647 elif mode_name in {'name_thing', 'admin_thing_protect'}:
649 for t in [t for t in self.game.things if t.position == player.position
650 and t.id_ != player.id_]:
655 self.log_msg('? not standing over thing')
658 self.thing_selected = thing
659 self.mode = getattr(self, 'mode_' + mode_name)
660 if self.mode.name in {'control_tile_draw', 'control_tile_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 player = self.game.get_thing(self.game.player_id)
667 self.explorer = YX(player.position.y, 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':
674 self.send('LOGIN ' + quote(self.login_name))
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 player = self.game.get_thing(self.game.player_id)
680 select_range = [player.position,
681 player.position + YX(0,-1),
682 player.position + YX(0, 1),
683 player.position + YX(-1, 0),
684 player.position + YX(1, 0)]
685 if type(self.game.map_geometry) == MapGeometryHex:
686 if player.position.y % 2:
687 select_range += [player.position + YX(-1, 1),
688 player.position + YX(1, 1)]
690 select_range += [player.position + YX(-1, -1),
691 player.position + YX(1, -1)]
692 self.selectables = [t.id_ for t in self.game.things
693 if t.portable and t.position in select_range]
694 if len(self.selectables) == 0:
697 self.switch_mode('play')
700 for i in range(len(self.selectables)):
701 t = self.game.get_thing(self.selectables[i])
702 self.log_msg(str(i) + ': ' + self.get_thing_info(t))
703 elif self.mode.name == 'drop_thing':
704 self.log_msg('Direction to drop thing to:')
706 ['HERE'] + list(self.game.tui.movement_keys.values())
707 for i in range(len(self.selectables)):
708 self.log_msg(str(i) + ': ' + self.selectables[i])
709 elif self.mode.name == 'command_thing':
710 self.send('TASK:COMMAND ' + quote('HELP'))
711 elif self.mode.name == 'control_pw_pw':
712 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
713 elif self.mode.name == 'control_tile_draw':
714 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']))
716 self.restore_input_values()
718 def set_default_colors(self):
719 curses.init_color(1, 1000, 1000, 1000)
720 curses.init_color(2, 0, 0, 0)
721 self.do_refresh = True
723 def set_random_colors(self):
727 return int(offset + random.random()*375)
729 curses.init_color(1, rand(625), rand(625), rand(625))
730 curses.init_color(2, rand(0), rand(0), rand(0))
731 self.do_refresh = True
735 return self.info_cached
736 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
738 if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
739 info_to_cache += 'outside field of view'
741 for t in self.game.things:
742 if t.position == self.explorer:
743 info_to_cache += 'THING: %s' % self.get_thing_info(t)
744 protection = t.protection
745 if protection == '.':
747 info_to_cache += ' / protection: %s\n' % protection
748 if hasattr(t, 'hat'):
749 info_to_cache += t.hat[0:6] + '\n'
750 info_to_cache += t.hat[6:12] + '\n'
751 info_to_cache += t.hat[12:18] + '\n'
752 if hasattr(t, 'face'):
753 info_to_cache += t.face[0:6] + '\n'
754 info_to_cache += t.face[6:12] + '\n'
755 info_to_cache += t.face[12:18] + '\n'
756 terrain_char = self.game.map_content[pos_i]
758 if terrain_char in self.game.terrains:
759 terrain_desc = self.game.terrains[terrain_char]
760 info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
762 protection = self.game.map_control_content[pos_i]
763 if protection == '.':
764 protection = 'unprotected'
765 info_to_cache += 'PROTECTION: %s\n' % protection
766 if self.explorer in self.game.portals:
767 info_to_cache += 'PORTAL: ' +\
768 self.game.portals[self.explorer] + '\n'
770 info_to_cache += 'PORTAL: (none)\n'
771 if self.explorer in self.game.annotations:
772 info_to_cache += 'ANNOTATION: ' +\
773 self.game.annotations[self.explorer]
774 self.info_cached = info_to_cache
775 return self.info_cached
777 def get_thing_info(self, t):
779 (t.type_, self.game.thing_types[t.type_])
780 if hasattr(t, 'thing_char'):
782 if hasattr(t, 'name'):
783 info += ' (%s)' % t.name
784 if hasattr(t, 'installed'):
785 info += ' / installed'
788 def loop(self, stdscr):
791 def safe_addstr(y, x, line):
792 if y < self.size.y - 1 or x + len(line) < self.size.x:
793 stdscr.addstr(y, x, line, curses.color_pair(1))
794 else: # workaround to <https://stackoverflow.com/q/7063128>
795 cut_i = self.size.x - x - 1
797 last_char = line[cut_i]
798 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
799 stdscr.insstr(y, self.size.x - 2, ' ')
800 stdscr.addstr(y, x, cut, curses.color_pair(1))
802 def handle_input(msg):
803 command, args = self.parser.parse(msg)
806 def task_action_on(action):
807 return action_tasks[action] in self.game.tasks
809 def msg_into_lines_of_width(msg, width):
813 for i in range(len(msg)):
814 if x >= width or msg[i] == "\n":
826 def reset_screen_size():
827 self.size = YX(*stdscr.getmaxyx())
828 self.size = self.size - YX(self.size.y % 4, 0)
829 self.size = self.size - YX(0, self.size.x % 4)
830 self.window_width = int(self.size.x / 2)
832 def recalc_input_lines():
833 if not self.mode.has_input_prompt:
834 self.input_lines = []
836 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
839 def move_explorer(direction):
840 target = self.game.map_geometry.move_yx(self.explorer, direction)
842 self.info_cached = None
843 self.explorer = target
845 self.send_tile_control_command()
851 for line in self.log:
852 lines += msg_into_lines_of_width(line, self.window_width)
855 max_y = self.size.y - len(self.input_lines)
856 for i in range(len(lines)):
857 if (i >= max_y - height_header):
859 safe_addstr(max_y - i - 1, self.window_width, lines[i])
862 info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
863 lines = msg_into_lines_of_width(info, self.window_width)
865 for i in range(len(lines)):
866 y = height_header + i
867 if y >= self.size.y - len(self.input_lines):
869 safe_addstr(y, self.window_width, lines[i])
872 y = self.size.y - len(self.input_lines)
873 for i in range(len(self.input_lines)):
874 safe_addstr(y, self.window_width, self.input_lines[i])
878 if not self.game.turn_complete:
880 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
883 help = "hit [%s] for help" % self.keys['help']
884 if self.mode.has_input_prompt:
885 help = "enter /help for help"
886 safe_addstr(1, self.window_width,
887 'MODE: %s – %s' % (self.mode.short_desc, help))
890 if not self.game.turn_complete and len(self.map_lines) == 0:
892 if self.game.turn_complete:
894 for y in range(self.game.map_geometry.size.y):
895 start = self.game.map_geometry.size.x * y
896 end = start + self.game.map_geometry.size.x
897 if self.map_mode == 'protections':
898 map_lines_split += [[c + ' ' for c
899 in self.game.map_control_content[start:end]]]
901 map_lines_split += [[c + ' ' for c
902 in self.game.map_content[start:end]]]
903 if self.map_mode == 'terrain + annotations':
904 for p in self.game.annotations:
905 map_lines_split[p.y][p.x] = 'A '
906 elif self.map_mode == 'terrain + things':
907 for p in self.game.portals.keys():
908 original = map_lines_split[p.y][p.x]
909 map_lines_split[p.y][p.x] = original[0] + 'P'
912 def draw_thing(t, used_positions):
913 symbol = self.game.thing_types[t.type_]
915 if hasattr(t, 'thing_char'):
916 meta_char = t.thing_char
917 if t.position in used_positions:
919 if hasattr(t, 'carrying') and t.carrying:
921 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
922 used_positions += [t.position]
924 for t in [t for t in self.game.things if t.type_ != 'Player']:
925 draw_thing(t, used_positions)
926 for t in [t for t in self.game.things if t.type_ == 'Player']:
927 draw_thing(t, used_positions)
928 player = self.game.get_thing(self.game.player_id)
929 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
930 map_lines_split[self.explorer.y][self.explorer.x] = '??'
931 elif self.map_mode != 'terrain + things':
932 map_lines_split[player.position.y][player.position.x] = '??'
934 if type(self.game.map_geometry) == MapGeometryHex:
936 for line in map_lines_split:
937 self.map_lines += [indent * ' ' + ''.join(line)]
938 indent = 0 if indent else 1
940 for line in map_lines_split:
941 self.map_lines += [''.join(line)]
942 window_center = YX(int(self.size.y / 2),
943 int(self.window_width / 2))
944 center = player.position
945 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
946 center = self.explorer
947 center = YX(center.y, center.x * 2)
948 self.offset = center - window_center
949 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
950 self.offset += YX(0, 1)
951 term_y = max(0, -self.offset.y)
952 term_x = max(0, -self.offset.x)
953 map_y = max(0, self.offset.y)
954 map_x = max(0, self.offset.x)
955 while term_y < self.size.y and map_y < len(self.map_lines):
956 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
957 safe_addstr(term_y, term_x, to_draw)
962 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
963 self.mode.help_intro)
964 if len(self.mode.available_actions) > 0:
965 content += "Available actions:\n"
966 for action in self.mode.available_actions:
967 if action in action_tasks:
968 if action_tasks[action] not in self.game.tasks:
970 if action == 'move_explorer':
973 key = ','.join(self.movement_keys)
975 key = self.keys[action]
976 content += '[%s] – %s\n' % (key, action_descriptions[action])
978 content += self.mode.list_available_modes(self)
979 for i in range(self.size.y):
981 self.window_width * (not self.mode.has_input_prompt),
982 ' ' * self.window_width)
984 for line in content.split('\n'):
985 lines += msg_into_lines_of_width(line, self.window_width)
986 for i in range(len(lines)):
990 self.window_width * (not self.mode.has_input_prompt),
995 stdscr.bkgd(' ', curses.color_pair(1))
997 if self.mode.has_input_prompt:
999 if self.mode.shows_info:
1004 if not self.mode.is_intro:
1010 def pick_selectable(task_name):
1012 i = int(self.input_)
1013 if i < 0 or i >= len(self.selectables):
1014 self.log_msg('? invalid index, aborted')
1016 self.send('TASK:%s %s' % (task_name, self.selectables[i]))
1018 self.log_msg('? invalid index, aborted')
1020 self.switch_mode('play')
1022 action_descriptions = {
1024 'flatten': 'flatten surroundings',
1025 'teleport': 'teleport',
1026 'take_thing': 'pick up thing',
1027 'drop_thing': 'drop thing',
1028 'toggle_map_mode': 'toggle map view',
1029 'toggle_tile_draw': 'toggle protection character drawing',
1030 'install': '(un-)install',
1031 'wear': '(un-)wear',
1032 'door': 'open/close',
1033 'consume': 'consume',
1038 'flatten': 'FLATTEN_SURROUNDINGS',
1039 'take_thing': 'PICK_UP',
1040 'drop_thing': 'DROP',
1042 'install': 'INSTALL',
1045 'command': 'COMMAND',
1046 'consume': 'INTOXICATE',
1050 curses.curs_set(False) # hide cursor
1051 curses.start_color()
1052 self.set_default_colors()
1053 curses.init_pair(1, 1, 2)
1056 self.explorer = YX(0, 0)
1059 interval = datetime.timedelta(seconds=5)
1060 last_ping = datetime.datetime.now() - interval
1062 if self.disconnected and self.force_instant_connect:
1063 self.force_instant_connect = False
1065 now = datetime.datetime.now()
1066 if now - last_ping > interval:
1067 if self.disconnected:
1077 self.do_refresh = False
1080 msg = self.queue.get(block=False)
1085 key = stdscr.getkey()
1086 self.do_refresh = True
1087 except curses.error:
1092 if key == 'KEY_RESIZE':
1094 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1095 self.input_ = self.input_[:-1]
1096 elif (((not self.mode.is_intro) and keycode == 27) # Escape
1097 or (self.mode.has_input_prompt and key == '\n'
1098 and self.input_ == ''\
1099 and self.mode.name in {'chat', 'command_thing',
1100 'take_thing', 'drop_thing',
1102 if self.mode.name not in {'chat', 'play', 'study', 'edit'}:
1103 self.log_msg('@ aborted')
1104 self.switch_mode('play')
1105 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1106 self.show_help = True
1108 self.restore_input_values()
1109 elif self.mode.has_input_prompt and key != '\n': # Return key
1111 max_length = self.window_width * self.size.y - len(input_prompt) - 1
1112 if len(self.input_) > max_length:
1113 self.input_ = self.input_[:max_length]
1114 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1115 self.show_help = True
1116 elif self.mode.name == 'login' and key == '\n':
1117 self.login_name = self.input_
1118 self.send('LOGIN ' + quote(self.input_))
1120 elif self.mode.name == 'enter_face' and key == '\n':
1121 if len(self.input_) != 18:
1122 self.log_msg('? wrong input length, aborting')
1124 self.send('PLAYER_FACE %s' % quote(self.input_))
1126 self.switch_mode('edit')
1127 elif self.mode.name == 'take_thing' and key == '\n':
1128 pick_selectable('PICK_UP')
1129 elif self.mode.name == 'drop_thing' and key == '\n':
1130 pick_selectable('DROP')
1131 elif self.mode.name == 'command_thing' and key == '\n':
1132 self.send('TASK:COMMAND ' + quote(self.input_))
1134 elif self.mode.name == 'control_pw_pw' and key == '\n':
1135 if self.input_ == '':
1136 self.log_msg('@ aborted')
1138 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1139 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1140 self.switch_mode('admin')
1141 elif self.mode.name == 'password' and key == '\n':
1142 if self.input_ == '':
1144 self.password = self.input_
1145 self.switch_mode('edit')
1146 elif self.mode.name == 'admin_enter' and key == '\n':
1147 self.send('BECOME_ADMIN ' + quote(self.input_))
1148 self.switch_mode('play')
1149 elif self.mode.name == 'control_pw_type' and key == '\n':
1150 if len(self.input_) != 1:
1151 self.log_msg('@ entered non-single-char, therefore aborted')
1152 self.switch_mode('admin')
1154 self.tile_control_char = self.input_
1155 self.switch_mode('control_pw_pw')
1156 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1157 if len(self.input_) != 1:
1158 self.log_msg('@ entered non-single-char, therefore aborted')
1160 self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
1161 quote(self.input_)))
1162 self.log_msg('@ sent new protection character for thing')
1163 self.switch_mode('admin')
1164 elif self.mode.name == 'control_tile_type' and key == '\n':
1165 if len(self.input_) != 1:
1166 self.log_msg('@ entered non-single-char, therefore aborted')
1167 self.switch_mode('admin')
1169 self.tile_control_char = self.input_
1170 self.switch_mode('control_tile_draw')
1171 elif self.mode.name == 'chat' and key == '\n':
1172 if self.input_ == '':
1174 if self.input_[0] == '/':
1175 if self.input_.startswith('/nick'):
1176 tokens = self.input_.split(maxsplit=1)
1177 if len(tokens) == 2:
1178 self.send('NICK ' + quote(tokens[1]))
1180 self.log_msg('? need login name')
1182 self.log_msg('? unknown command')
1184 self.send('ALL ' + quote(self.input_))
1186 elif self.mode.name == 'name_thing' and key == '\n':
1187 if self.input_ == '':
1189 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
1191 quote(self.password)))
1192 self.switch_mode('edit')
1193 elif self.mode.name == 'annotate' and key == '\n':
1194 if self.input_ == '':
1196 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1197 quote(self.password)))
1198 self.switch_mode('edit')
1199 elif self.mode.name == 'portal' and key == '\n':
1200 if self.input_ == '':
1202 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1203 quote(self.password)))
1204 self.switch_mode('edit')
1205 elif self.mode.name == 'study':
1206 if self.mode.mode_switch_on_key(self, key):
1208 elif key == self.keys['toggle_map_mode']:
1209 self.toggle_map_mode()
1210 elif key in self.movement_keys:
1211 move_explorer(self.movement_keys[key])
1212 elif self.mode.name == 'play':
1213 if self.mode.mode_switch_on_key(self, key):
1215 elif key == self.keys['door'] and task_action_on('door'):
1216 self.send('TASK:DOOR')
1217 elif key == self.keys['consume'] and task_action_on('consume'):
1218 self.send('TASK:INTOXICATE')
1219 elif key == self.keys['install'] and task_action_on('install'):
1220 self.send('TASK:INSTALL')
1221 elif key == self.keys['wear'] and task_action_on('wear'):
1222 self.send('TASK:WEAR')
1223 elif key == self.keys['spin'] and task_action_on('spin'):
1224 self.send('TASK:SPIN')
1225 elif key == self.keys['teleport']:
1226 player = self.game.get_thing(self.game.player_id)
1227 if player.position in self.game.portals:
1228 self.host = self.game.portals[player.position]
1232 self.log_msg('? not standing on portal')
1233 elif key in self.movement_keys and task_action_on('move'):
1234 self.send('TASK:MOVE ' + self.movement_keys[key])
1235 elif self.mode.name == 'write':
1236 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1237 self.switch_mode('edit')
1238 elif self.mode.name == 'control_tile_draw':
1239 if self.mode.mode_switch_on_key(self, key):
1241 elif key in self.movement_keys:
1242 move_explorer(self.movement_keys[key])
1243 elif key == self.keys['toggle_tile_draw']:
1244 self.tile_draw = False if self.tile_draw else True
1245 elif self.mode.name == 'admin':
1246 if self.mode.mode_switch_on_key(self, key):
1248 elif key in self.movement_keys and task_action_on('move'):
1249 self.send('TASK:MOVE ' + self.movement_keys[key])
1250 elif self.mode.name == 'edit':
1251 if self.mode.mode_switch_on_key(self, key):
1253 elif key == self.keys['flatten'] and task_action_on('flatten'):
1254 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1255 elif key == self.keys['toggle_map_mode']:
1256 self.toggle_map_mode()
1257 elif key in self.movement_keys and task_action_on('move'):
1258 self.send('TASK:MOVE ' + self.movement_keys[key])
1260 if len(sys.argv) != 2:
1261 raise ArgError('wrong number of arguments, need game host')