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.'
44 'admin_thing_protect': {
45 'short': 'change thing protection',
46 'intro': '@ enter thing protection character:',
47 'long': 'Change protection character for thing here.'
50 'short': 'enter your face',
51 'intro': '@ enter face line (enter nothing to abort):',
52 '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..'
55 'short': 'change terrain',
57 '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.'
60 'short': 'change protection character password',
61 'intro': '@ enter protection character for which you want to change the password:',
62 '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.'
65 'short': 'change protection character password',
67 '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.'
69 'control_tile_type': {
70 'short': 'change tiles protection',
71 'intro': '@ enter protection character which you want to draw:',
72 '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.'
74 'control_tile_draw': {
75 'short': 'change tiles protection',
77 '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.'
80 'short': 'annotate tile',
82 '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.'
85 'short': 'edit portal',
87 '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.'
92 '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'
97 'long': 'Enter your player name.'
99 'waiting_for_server': {
100 'short': 'waiting for server response',
101 'intro': '@ waiting for server …',
102 'long': 'Waiting for a server response.'
105 'short': 'waiting for server response',
107 'long': 'Waiting for a server response.'
110 'short': 'set world edit password',
112 '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.'
115 'short': 'become admin',
116 'intro': '@ enter admin password:',
117 'long': 'This mode allows you to become admin if you know an admin password.'
122 'long': 'This mode allows you access to actions limited to administrators.'
126 from ws4py.client import WebSocketBaseClient
127 class WebSocketClient(WebSocketBaseClient):
129 def __init__(self, recv_handler, *args, **kwargs):
130 super().__init__(*args, **kwargs)
131 self.recv_handler = recv_handler
134 def received_message(self, message):
136 message = str(message)
137 self.recv_handler(message)
140 def plom_closed(self):
141 return self.client_terminated
143 from plomrogue.io_tcp import PlomSocket
144 class PlomSocketClient(PlomSocket):
146 def __init__(self, recv_handler, url):
148 self.recv_handler = recv_handler
149 host, port = url.split(':')
150 super().__init__(socket.create_connection((host, port)))
158 for msg in self.recv():
159 if msg == 'NEED_SSL':
160 self.socket = ssl.wrap_socket(self.socket)
162 self.recv_handler(msg)
163 except BrokenSocketConnection:
164 pass # we assume socket will be known as dead by now
166 def cmd_TURN(game, n):
167 game.annotations = {}
171 game.turn_complete = False
173 cmd_TURN.argtypes = 'int:nonneg'
175 def cmd_LOGIN_OK(game):
176 game.tui.switch_mode('post_login_wait')
177 game.tui.send('GET_GAMESTATE')
178 game.tui.log_msg('@ welcome')
179 cmd_LOGIN_OK.argtypes = ''
181 def cmd_ADMIN_OK(game):
182 game.tui.is_admin = True
183 game.tui.log_msg('@ you now have admin rights')
184 game.tui.switch_mode('admin')
185 game.tui.do_refresh = True
186 cmd_ADMIN_OK.argtypes = ''
188 def cmd_REPLY(game, msg):
189 game.tui.log_msg('#MUSICPLAYER: ' + msg)
190 game.tui.do_refresh = True
191 cmd_REPLY.argtypes = 'string'
193 def cmd_CHAT(game, msg):
194 game.tui.log_msg('# ' + msg)
195 game.tui.do_refresh = True
196 cmd_CHAT.argtypes = 'string'
198 def cmd_PLAYER_ID(game, player_id):
199 game.player_id = player_id
200 cmd_PLAYER_ID.argtypes = 'int:nonneg'
202 def cmd_THING(game, yx, thing_type, protection, thing_id, portable, commandable):
203 t = game.get_thing(thing_id)
205 t = ThingBase(game, thing_id)
209 t.protection = protection
210 t.portable = portable
211 t.commandable = commandable
212 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg bool bool'
214 def cmd_THING_NAME(game, thing_id, name):
215 t = game.get_thing(thing_id)
217 cmd_THING_NAME.argtypes = 'int:pos string'
219 def cmd_THING_FACE(game, thing_id, face):
220 t = game.get_thing(thing_id)
222 cmd_THING_FACE.argtypes = 'int:pos string'
224 def cmd_THING_HAT(game, thing_id, hat):
225 t = game.get_thing(thing_id)
227 cmd_THING_HAT.argtypes = 'int:pos string'
229 def cmd_THING_CHAR(game, thing_id, c):
230 t = game.get_thing(thing_id)
232 cmd_THING_CHAR.argtypes = 'int:pos char'
234 def cmd_MAP(game, geometry, size, content):
235 map_geometry_class = globals()['MapGeometry' + geometry]
236 game.map_geometry = map_geometry_class(size)
237 game.map_content = content
238 if type(game.map_geometry) == MapGeometrySquare:
239 game.tui.movement_keys = {
240 game.tui.keys['square_move_up']: 'UP',
241 game.tui.keys['square_move_left']: 'LEFT',
242 game.tui.keys['square_move_down']: 'DOWN',
243 game.tui.keys['square_move_right']: 'RIGHT',
245 elif type(game.map_geometry) == MapGeometryHex:
246 game.tui.movement_keys = {
247 game.tui.keys['hex_move_upleft']: 'UPLEFT',
248 game.tui.keys['hex_move_upright']: 'UPRIGHT',
249 game.tui.keys['hex_move_right']: 'RIGHT',
250 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
251 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
252 game.tui.keys['hex_move_left']: 'LEFT',
254 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
256 def cmd_FOV(game, content):
258 cmd_FOV.argtypes = 'string'
260 def cmd_MAP_CONTROL(game, content):
261 game.map_control_content = content
262 cmd_MAP_CONTROL.argtypes = 'string'
264 def cmd_GAME_STATE_COMPLETE(game):
265 if game.tui.mode.name == 'post_login_wait':
266 game.tui.switch_mode('play')
267 game.turn_complete = True
268 game.tui.do_refresh = True
269 game.tui.info_cached = None
270 cmd_GAME_STATE_COMPLETE.argtypes = ''
272 def cmd_PORTAL(game, position, msg):
273 game.portals[position] = msg
274 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
276 def cmd_PLAY_ERROR(game, msg):
277 game.tui.log_msg('? ' + msg)
278 game.tui.flash = True
279 game.tui.do_refresh = True
280 cmd_PLAY_ERROR.argtypes = 'string'
282 def cmd_GAME_ERROR(game, msg):
283 game.tui.log_msg('? game error: ' + msg)
284 game.tui.do_refresh = True
285 cmd_GAME_ERROR.argtypes = 'string'
287 def cmd_ARGUMENT_ERROR(game, msg):
288 game.tui.log_msg('? syntax error: ' + msg)
289 game.tui.do_refresh = True
290 cmd_ARGUMENT_ERROR.argtypes = 'string'
292 def cmd_ANNOTATION(game, position, msg):
293 game.annotations[position] = msg
294 if game.tui.mode.shows_info:
295 game.tui.do_refresh = True
296 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
298 def cmd_TASKS(game, tasks_comma_separated):
299 game.tasks = tasks_comma_separated.split(',')
300 game.tui.mode_write.legal = 'WRITE' in game.tasks
301 game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks
302 game.tui.mode_take_thing.legal = 'PICK_UP' in game.tasks
303 cmd_TASKS.argtypes = 'string'
305 def cmd_THING_TYPE(game, thing_type, symbol_hint):
306 game.thing_types[thing_type] = symbol_hint
307 cmd_THING_TYPE.argtypes = 'string char'
309 def cmd_THING_INSTALLED(game, thing_id):
310 game.get_thing(thing_id).installed = True
311 cmd_THING_INSTALLED.argtypes = 'int:pos'
313 def cmd_THING_CARRYING(game, thing_id, carried_id):
314 game.get_thing(thing_id).carrying = game.get_thing(carried_id)
315 cmd_THING_CARRYING.argtypes = 'int:pos int:pos'
317 def cmd_TERRAIN(game, terrain_char, terrain_desc):
318 game.terrains[terrain_char] = terrain_desc
319 cmd_TERRAIN.argtypes = 'char string'
323 cmd_PONG.argtypes = ''
325 def cmd_DEFAULT_COLORS(game):
326 game.tui.set_default_colors()
327 cmd_DEFAULT_COLORS.argtypes = ''
329 def cmd_RANDOM_COLORS(game):
330 game.tui.set_random_colors()
331 cmd_RANDOM_COLORS.argtypes = ''
333 class Game(GameBase):
334 turn_complete = False
338 def __init__(self, *args, **kwargs):
339 super().__init__(*args, **kwargs)
340 self.register_command(cmd_LOGIN_OK)
341 self.register_command(cmd_ADMIN_OK)
342 self.register_command(cmd_PONG)
343 self.register_command(cmd_CHAT)
344 self.register_command(cmd_REPLY)
345 self.register_command(cmd_PLAYER_ID)
346 self.register_command(cmd_TURN)
347 self.register_command(cmd_THING)
348 self.register_command(cmd_THING_TYPE)
349 self.register_command(cmd_THING_NAME)
350 self.register_command(cmd_THING_CHAR)
351 self.register_command(cmd_THING_FACE)
352 self.register_command(cmd_THING_HAT)
353 self.register_command(cmd_THING_CARRYING)
354 self.register_command(cmd_THING_INSTALLED)
355 self.register_command(cmd_TERRAIN)
356 self.register_command(cmd_MAP)
357 self.register_command(cmd_MAP_CONTROL)
358 self.register_command(cmd_PORTAL)
359 self.register_command(cmd_ANNOTATION)
360 self.register_command(cmd_GAME_STATE_COMPLETE)
361 self.register_command(cmd_ARGUMENT_ERROR)
362 self.register_command(cmd_GAME_ERROR)
363 self.register_command(cmd_PLAY_ERROR)
364 self.register_command(cmd_TASKS)
365 self.register_command(cmd_FOV)
366 self.register_command(cmd_DEFAULT_COLORS)
367 self.register_command(cmd_RANDOM_COLORS)
368 self.map_content = ''
370 self.annotations = {}
374 def get_string_options(self, string_option_type):
375 if string_option_type == 'map_geometry':
376 return ['Hex', 'Square']
377 elif string_option_type == 'thing_type':
378 return self.thing_types.keys()
381 def get_command(self, command_name):
382 from functools import partial
383 f = partial(self.commands[command_name], self)
384 f.argtypes = self.commands[command_name].argtypes
389 def __init__(self, name, has_input_prompt=False, shows_info=False,
390 is_intro=False, is_single_char_entry=False):
392 self.short_desc = mode_helps[name]['short']
393 self.available_modes = []
394 self.available_actions = []
395 self.has_input_prompt = has_input_prompt
396 self.shows_info = shows_info
397 self.is_intro = is_intro
398 self.help_intro = mode_helps[name]['long']
399 self.intro_msg = mode_helps[name]['intro']
400 self.is_single_char_entry = is_single_char_entry
403 def iter_available_modes(self, tui):
404 for mode_name in self.available_modes:
405 mode = getattr(tui, 'mode_' + mode_name)
408 key = tui.keys['switch_to_' + mode.name]
411 def list_available_modes(self, tui):
413 if len(self.available_modes) > 0:
414 msg = 'Other modes available from here:\n'
415 for mode, key in self.iter_available_modes(tui):
416 msg += '[%s] – %s\n' % (key, mode.short_desc)
419 def mode_switch_on_key(self, tui, key_pressed):
420 for mode, key in self.iter_available_modes(tui):
421 if key_pressed == key:
422 tui.switch_mode(mode.name)
427 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
428 mode_admin = Mode('admin')
429 mode_play = Mode('play')
430 mode_study = Mode('study', shows_info=True)
431 mode_write = Mode('write', is_single_char_entry=True)
432 mode_edit = Mode('edit')
433 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
434 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
435 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
436 mode_control_tile_draw = Mode('control_tile_draw')
437 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
438 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
439 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
440 mode_chat = Mode('chat', has_input_prompt=True)
441 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
442 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
443 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
444 mode_password = Mode('password', has_input_prompt=True)
445 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
446 mode_command_thing = Mode('command_thing', has_input_prompt=True)
447 mode_take_thing = Mode('take_thing', has_input_prompt=True)
448 mode_enter_face = Mode('enter_face', has_input_prompt=True)
452 def __init__(self, host):
455 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
456 "command_thing", "take_thing"]
457 self.mode_play.available_actions = ["move", "drop_thing",
458 "teleport", "door", "consume",
460 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
461 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
462 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
463 "control_tile_type", "chat",
464 "study", "play", "edit"]
465 self.mode_admin.available_actions = ["move"]
466 self.mode_control_tile_draw.available_modes = ["admin_enter"]
467 self.mode_control_tile_draw.available_actions = ["move_explorer",
469 self.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
470 "password", "chat", "study", "play",
471 "admin_enter", "enter_face"]
472 self.mode_edit.available_actions = ["move", "flatten", "toggle_map_mode"]
477 self.parser = Parser(self.game)
479 self.do_refresh = True
480 self.queue = queue.Queue()
481 self.login_name = None
482 self.map_mode = 'terrain + things'
483 self.password = 'foo'
484 self.switch_mode('waiting_for_server')
486 'switch_to_chat': 't',
487 'switch_to_play': 'p',
488 'switch_to_password': 'P',
489 'switch_to_annotate': 'M',
490 'switch_to_portal': 'T',
491 'switch_to_study': '?',
492 'switch_to_edit': 'E',
493 'switch_to_write': 'm',
494 'switch_to_name_thing': 'N',
495 'switch_to_command_thing': 'O',
496 'switch_to_admin_enter': 'A',
497 'switch_to_control_pw_type': 'C',
498 'switch_to_control_tile_type': 'Q',
499 'switch_to_admin_thing_protect': 'T',
501 'switch_to_enter_face': 'f',
502 'switch_to_take_thing': 'z',
510 'toggle_map_mode': 'L',
511 'toggle_tile_draw': 'm',
512 'hex_move_upleft': 'w',
513 'hex_move_upright': 'e',
514 'hex_move_right': 'd',
515 'hex_move_downright': 'x',
516 'hex_move_downleft': 'y',
517 'hex_move_left': 'a',
518 'square_move_up': 'w',
519 'square_move_left': 'a',
520 'square_move_down': 's',
521 'square_move_right': 'd',
523 if os.path.isfile('config.json'):
524 with open('config.json', 'r') as f:
525 keys_conf = json.loads(f.read())
527 self.keys[k] = keys_conf[k]
528 self.show_help = False
529 self.disconnected = True
530 self.force_instant_connect = True
531 self.input_lines = []
535 self.offset = YX(0,0)
536 curses.wrapper(self.loop)
540 def handle_recv(msg):
546 self.log_msg('@ attempting connect')
547 socket_client_class = PlomSocketClient
548 if self.host.startswith('ws://') or self.host.startswith('wss://'):
549 socket_client_class = WebSocketClient
551 self.socket = socket_client_class(handle_recv, self.host)
552 self.socket_thread = threading.Thread(target=self.socket.run)
553 self.socket_thread.start()
554 self.disconnected = False
555 self.game.thing_types = {}
556 self.game.terrains = {}
557 time.sleep(0.1) # give potential SSL negotation some time …
558 self.socket.send('TASKS')
559 self.socket.send('TERRAINS')
560 self.socket.send('THING_TYPES')
561 self.switch_mode('login')
562 except ConnectionRefusedError:
563 self.log_msg('@ server connect failure')
564 self.disconnected = True
565 self.switch_mode('waiting_for_server')
566 self.do_refresh = True
569 self.log_msg('@ attempting reconnect')
571 # necessitated by some strange SSL race conditions with ws4py
572 time.sleep(0.1) # FIXME find out why exactly necessary
573 self.switch_mode('waiting_for_server')
578 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
579 raise BrokenSocketConnection
580 self.socket.send(msg)
581 except (BrokenPipeError, BrokenSocketConnection):
582 self.log_msg('@ server disconnected :(')
583 self.disconnected = True
584 self.force_instant_connect = True
585 self.do_refresh = True
587 def log_msg(self, msg):
589 if len(self.log) > 100:
590 self.log = self.log[-100:]
592 def restore_input_values(self):
593 if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
594 self.input_ = self.game.annotations[self.explorer]
595 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
596 self.input_ = self.game.portals[self.explorer]
597 elif self.mode.name == 'password':
598 self.input_ = self.password
599 elif self.mode.name == 'name_thing':
600 if hasattr(self.thing_selected, 'name'):
601 self.input_ = self.thing_selected.name
602 elif self.mode.name == 'admin_thing_protect':
603 if hasattr(self.thing_selected, 'protection'):
604 self.input_ = self.thing_selected.protection
606 def send_tile_control_command(self):
607 self.send('SET_TILE_CONTROL %s %s' %
608 (self.explorer, quote(self.tile_control_char)))
610 def toggle_map_mode(self):
611 if self.map_mode == 'terrain only':
612 self.map_mode = 'terrain + annotations'
613 elif self.map_mode == 'terrain + annotations':
614 self.map_mode = 'terrain + things'
615 elif self.map_mode == 'terrain + things':
616 self.map_mode = 'protections'
617 elif self.map_mode == 'protections':
618 self.map_mode = 'terrain only'
620 def switch_mode(self, mode_name):
621 if self.mode and self.mode.name == 'control_tile_draw':
622 self.log_msg('@ finished tile protection drawing.')
623 self.tile_draw = False
624 player = self.game.get_thing(self.game.player_id)
625 if mode_name == 'command_thing' and\
626 (not hasattr(player, 'carrying') or not player.carrying.commandable):
627 self.log_msg('? not carrying anything commandable')
629 self.switch_mode('play')
631 if mode_name == 'admin_enter' and self.is_admin:
633 elif mode_name in {'name_thing', 'admin_thing_protect'}:
635 for t in [t for t in self.game.things if t.position == player.position
636 and t.id_ != player.id_]:
641 self.log_msg('? not standing over thing')
644 self.thing_selected = thing
645 self.mode = getattr(self, 'mode_' + mode_name)
646 if self.mode.name in {'control_tile_draw', 'control_tile_type',
648 self.map_mode = 'protections'
649 elif self.mode.name != 'edit':
650 self.map_mode = 'terrain + things'
651 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
652 player = self.game.get_thing(self.game.player_id)
653 self.explorer = YX(player.position.y, player.position.x)
654 if self.mode.is_single_char_entry:
655 self.show_help = True
656 if len(self.mode.intro_msg) > 0:
657 self.log_msg(self.mode.intro_msg)
658 if self.mode.name == 'login':
660 self.send('LOGIN ' + quote(self.login_name))
662 self.log_msg('@ enter username')
663 elif self.mode.name == 'take_thing':
664 self.log_msg('Portable things in reach for pick-up:')
665 player = self.game.get_thing(self.game.player_id)
666 select_range = [player.position,
667 player.position + YX(0,-1),
668 player.position + YX(0, 1),
669 player.position + YX(-1, 0),
670 player.position + YX(1, 0)]
671 if type(self.game.map_geometry) == MapGeometryHex:
672 if player.position.y % 2:
673 select_range += [player.position + YX(-1, 1),
674 player.position + YX(1, 1)]
676 select_range += [player.position + YX(-1, -1),
677 player.position + YX(1, -1)]
678 self.selectables = [t for t in self.game.things
679 if t.portable and t.position in select_range]
680 if len(self.selectables) == 0:
683 self.switch_mode('play')
686 for i in range(len(self.selectables)):
687 t = self.selectables[i]
688 self.log_msg(str(i) + ': ' + self.get_thing_info(t))
689 elif self.mode.name == 'command_thing':
690 self.send('TASK:COMMAND ' + quote('HELP'))
691 elif self.mode.name == 'control_pw_pw':
692 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
693 elif self.mode.name == 'control_tile_draw':
694 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']))
696 self.restore_input_values()
698 def set_default_colors(self):
699 curses.init_color(1, 1000, 1000, 1000)
700 curses.init_color(2, 0, 0, 0)
701 self.do_refresh = True
703 def set_random_colors(self):
707 return int(offset + random.random()*375)
709 curses.init_color(1, rand(625), rand(625), rand(625))
710 curses.init_color(2, rand(0), rand(0), rand(0))
711 self.do_refresh = True
715 return self.info_cached
716 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
718 if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
719 info_to_cache += 'outside field of view'
721 for t in self.game.things:
722 if t.position == self.explorer:
723 info_to_cache += 'THING: %s' % self.get_thing_info(t)
724 protection = t.protection
725 if protection == '.':
727 info_to_cache += ' / protection: %s\n' % protection
728 if hasattr(t, 'hat'):
729 info_to_cache += t.hat[0:6] + '\n'
730 info_to_cache += t.hat[6:12] + '\n'
731 info_to_cache += t.hat[12:18] + '\n'
732 if hasattr(t, 'face'):
733 info_to_cache += t.face[0:6] + '\n'
734 info_to_cache += t.face[6:12] + '\n'
735 info_to_cache += t.face[12:18] + '\n'
736 terrain_char = self.game.map_content[pos_i]
738 if terrain_char in self.game.terrains:
739 terrain_desc = self.game.terrains[terrain_char]
740 info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
742 protection = self.game.map_control_content[pos_i]
743 if protection == '.':
744 protection = 'unprotected'
745 info_to_cache += 'PROTECTION: %s\n' % protection
746 if self.explorer in self.game.portals:
747 info_to_cache += 'PORTAL: ' +\
748 self.game.portals[self.explorer] + '\n'
750 info_to_cache += 'PORTAL: (none)\n'
751 if self.explorer in self.game.annotations:
752 info_to_cache += 'ANNOTATION: ' +\
753 self.game.annotations[self.explorer]
754 self.info_cached = info_to_cache
755 return self.info_cached
757 def get_thing_info(self, t):
759 (t.type_, self.game.thing_types[t.type_])
760 if hasattr(t, 'thing_char'):
762 if hasattr(t, 'name'):
763 info += ' (%s)' % t.name
764 if hasattr(t, 'installed'):
765 info += ' / installed'
768 def loop(self, stdscr):
771 def safe_addstr(y, x, line):
772 if y < self.size.y - 1 or x + len(line) < self.size.x:
773 stdscr.addstr(y, x, line, curses.color_pair(1))
774 else: # workaround to <https://stackoverflow.com/q/7063128>
775 cut_i = self.size.x - x - 1
777 last_char = line[cut_i]
778 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
779 stdscr.insstr(y, self.size.x - 2, ' ')
780 stdscr.addstr(y, x, cut, curses.color_pair(1))
782 def handle_input(msg):
783 command, args = self.parser.parse(msg)
786 def task_action_on(action):
787 return action_tasks[action] in self.game.tasks
789 def msg_into_lines_of_width(msg, width):
793 for i in range(len(msg)):
794 if x >= width or msg[i] == "\n":
806 def reset_screen_size():
807 self.size = YX(*stdscr.getmaxyx())
808 self.size = self.size - YX(self.size.y % 4, 0)
809 self.size = self.size - YX(0, self.size.x % 4)
810 self.window_width = int(self.size.x / 2)
812 def recalc_input_lines():
813 if not self.mode.has_input_prompt:
814 self.input_lines = []
816 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
819 def move_explorer(direction):
820 target = self.game.map_geometry.move_yx(self.explorer, direction)
822 self.info_cached = None
823 self.explorer = target
825 self.send_tile_control_command()
831 for line in self.log:
832 lines += msg_into_lines_of_width(line, self.window_width)
835 max_y = self.size.y - len(self.input_lines)
836 for i in range(len(lines)):
837 if (i >= max_y - height_header):
839 safe_addstr(max_y - i - 1, self.window_width, lines[i])
842 info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
843 lines = msg_into_lines_of_width(info, self.window_width)
845 for i in range(len(lines)):
846 y = height_header + i
847 if y >= self.size.y - len(self.input_lines):
849 safe_addstr(y, self.window_width, lines[i])
852 y = self.size.y - len(self.input_lines)
853 for i in range(len(self.input_lines)):
854 safe_addstr(y, self.window_width, self.input_lines[i])
858 if not self.game.turn_complete:
860 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
863 help = "hit [%s] for help" % self.keys['help']
864 if self.mode.has_input_prompt:
865 help = "enter /help for help"
866 safe_addstr(1, self.window_width,
867 'MODE: %s – %s' % (self.mode.short_desc, help))
870 if not self.game.turn_complete and len(self.map_lines) == 0:
872 if self.game.turn_complete:
874 for y in range(self.game.map_geometry.size.y):
875 start = self.game.map_geometry.size.x * y
876 end = start + self.game.map_geometry.size.x
877 if self.map_mode == 'protections':
878 map_lines_split += [[c + ' ' for c
879 in self.game.map_control_content[start:end]]]
881 map_lines_split += [[c + ' ' for c
882 in self.game.map_content[start:end]]]
883 if self.map_mode == 'terrain + annotations':
884 for p in self.game.annotations:
885 map_lines_split[p.y][p.x] = 'A '
886 elif self.map_mode == 'terrain + things':
887 for p in self.game.portals.keys():
888 original = map_lines_split[p.y][p.x]
889 map_lines_split[p.y][p.x] = original[0] + 'P'
892 def draw_thing(t, used_positions):
893 symbol = self.game.thing_types[t.type_]
895 if hasattr(t, 'thing_char'):
896 meta_char = t.thing_char
897 if t.position in used_positions:
899 if hasattr(t, 'carrying') and t.carrying:
901 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
902 used_positions += [t.position]
904 for t in [t for t in self.game.things if t.type_ != 'Player']:
905 draw_thing(t, used_positions)
906 for t in [t for t in self.game.things if t.type_ == 'Player']:
907 draw_thing(t, used_positions)
908 player = self.game.get_thing(self.game.player_id)
909 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
910 map_lines_split[self.explorer.y][self.explorer.x] = '??'
911 elif self.map_mode != 'terrain + things':
912 map_lines_split[player.position.y][player.position.x] = '??'
914 if type(self.game.map_geometry) == MapGeometryHex:
916 for line in map_lines_split:
917 self.map_lines += [indent * ' ' + ''.join(line)]
918 indent = 0 if indent else 1
920 for line in map_lines_split:
921 self.map_lines += [''.join(line)]
922 window_center = YX(int(self.size.y / 2),
923 int(self.window_width / 2))
924 center = player.position
925 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
926 center = self.explorer
927 center = YX(center.y, center.x * 2)
928 self.offset = center - window_center
929 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
930 self.offset += YX(0, 1)
931 term_y = max(0, -self.offset.y)
932 term_x = max(0, -self.offset.x)
933 map_y = max(0, self.offset.y)
934 map_x = max(0, self.offset.x)
935 while term_y < self.size.y and map_y < len(self.map_lines):
936 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
937 safe_addstr(term_y, term_x, to_draw)
942 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
943 self.mode.help_intro)
944 if len(self.mode.available_actions) > 0:
945 content += "Available actions:\n"
946 for action in self.mode.available_actions:
947 if action in action_tasks:
948 if action_tasks[action] not in self.game.tasks:
950 if action == 'move_explorer':
953 key = ','.join(self.movement_keys)
955 key = self.keys[action]
956 content += '[%s] – %s\n' % (key, action_descriptions[action])
958 content += self.mode.list_available_modes(self)
959 for i in range(self.size.y):
961 self.window_width * (not self.mode.has_input_prompt),
962 ' ' * self.window_width)
964 for line in content.split('\n'):
965 lines += msg_into_lines_of_width(line, self.window_width)
966 for i in range(len(lines)):
970 self.window_width * (not self.mode.has_input_prompt),
975 stdscr.bkgd(' ', curses.color_pair(1))
977 if self.mode.has_input_prompt:
979 if self.mode.shows_info:
984 if not self.mode.is_intro:
990 action_descriptions = {
992 'flatten': 'flatten surroundings',
993 'teleport': 'teleport',
994 'take_thing': 'pick up thing',
995 'drop_thing': 'drop thing',
996 'toggle_map_mode': 'toggle map view',
997 'toggle_tile_draw': 'toggle protection character drawing',
998 'install': '(un-)install',
1000 'door': 'open/close',
1001 'consume': 'consume',
1005 'flatten': 'FLATTEN_SURROUNDINGS',
1006 'take_thing': 'PICK_UP',
1007 'drop_thing': 'DROP',
1009 'install': 'INSTALL',
1012 'command': 'COMMAND',
1013 'consume': 'INTOXICATE',
1016 curses.curs_set(False) # hide cursor
1017 curses.start_color()
1018 self.set_default_colors()
1019 curses.init_pair(1, 1, 2)
1022 self.explorer = YX(0, 0)
1025 interval = datetime.timedelta(seconds=5)
1026 last_ping = datetime.datetime.now() - interval
1028 if self.disconnected and self.force_instant_connect:
1029 self.force_instant_connect = False
1031 now = datetime.datetime.now()
1032 if now - last_ping > interval:
1033 if self.disconnected:
1043 self.do_refresh = False
1046 msg = self.queue.get(block=False)
1051 key = stdscr.getkey()
1052 self.do_refresh = True
1053 except curses.error:
1055 self.show_help = False
1056 if key == 'KEY_RESIZE':
1058 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1059 self.input_ = self.input_[:-1]
1060 elif self.mode.has_input_prompt and key == '\n' and self.input_ == ''\
1061 and self.mode.name in {'chat', 'command_thing', 'take_thing',
1063 if self.mode.name != 'chat':
1064 self.log_msg('@ aborted')
1065 self.switch_mode('play')
1066 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1067 self.show_help = True
1069 self.restore_input_values()
1070 elif self.mode.has_input_prompt and key != '\n': # Return key
1072 max_length = self.window_width * self.size.y - len(input_prompt) - 1
1073 if len(self.input_) > max_length:
1074 self.input_ = self.input_[:max_length]
1075 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1076 self.show_help = True
1077 elif self.mode.name == 'login' and key == '\n':
1078 self.login_name = self.input_
1079 self.send('LOGIN ' + quote(self.input_))
1081 elif self.mode.name == 'enter_face' and key == '\n':
1082 if len(self.input_) != 18:
1083 self.log_msg('? wrong input length, aborting')
1085 self.send('PLAYER_FACE %s' % quote(self.input_))
1087 self.switch_mode('edit')
1088 elif self.mode.name == 'take_thing' and key == '\n':
1090 i = int(self.input_)
1091 if i < 0 or i >= len(self.selectables):
1092 self.log_msg('? invalid index, aborted')
1094 self.send('TASK:PICK_UP %s' % self.selectables[i].id_)
1096 self.log_msg('? invalid index, aborted')
1098 self.switch_mode('play')
1099 elif self.mode.name == 'command_thing' and key == '\n':
1100 self.send('TASK:COMMAND ' + quote(self.input_))
1102 elif self.mode.name == 'control_pw_pw' and key == '\n':
1103 if self.input_ == '':
1104 self.log_msg('@ aborted')
1106 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1107 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1108 self.switch_mode('admin')
1109 elif self.mode.name == 'password' and key == '\n':
1110 if self.input_ == '':
1112 self.password = self.input_
1113 self.switch_mode('edit')
1114 elif self.mode.name == 'admin_enter' and key == '\n':
1115 self.send('BECOME_ADMIN ' + quote(self.input_))
1116 self.switch_mode('play')
1117 elif self.mode.name == 'control_pw_type' and key == '\n':
1118 if len(self.input_) != 1:
1119 self.log_msg('@ entered non-single-char, therefore aborted')
1120 self.switch_mode('admin')
1122 self.tile_control_char = self.input_
1123 self.switch_mode('control_pw_pw')
1124 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1125 if len(self.input_) != 1:
1126 self.log_msg('@ entered non-single-char, therefore aborted')
1128 self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
1129 quote(self.input_)))
1130 self.log_msg('@ sent new protection character for thing')
1131 self.switch_mode('admin')
1132 elif self.mode.name == 'control_tile_type' and key == '\n':
1133 if len(self.input_) != 1:
1134 self.log_msg('@ entered non-single-char, therefore aborted')
1135 self.switch_mode('admin')
1137 self.tile_control_char = self.input_
1138 self.switch_mode('control_tile_draw')
1139 elif self.mode.name == 'chat' and key == '\n':
1140 if self.input_ == '':
1142 if self.input_[0] == '/':
1143 if self.input_.startswith('/nick'):
1144 tokens = self.input_.split(maxsplit=1)
1145 if len(tokens) == 2:
1146 self.send('NICK ' + quote(tokens[1]))
1148 self.log_msg('? need login name')
1150 self.log_msg('? unknown command')
1152 self.send('ALL ' + quote(self.input_))
1154 elif self.mode.name == 'name_thing' and key == '\n':
1155 if self.input_ == '':
1157 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
1159 quote(self.password)))
1160 self.switch_mode('edit')
1161 elif self.mode.name == 'annotate' and key == '\n':
1162 if self.input_ == '':
1164 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1165 quote(self.password)))
1166 self.switch_mode('edit')
1167 elif self.mode.name == 'portal' and key == '\n':
1168 if self.input_ == '':
1170 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1171 quote(self.password)))
1172 self.switch_mode('edit')
1173 elif self.mode.name == 'study':
1174 if self.mode.mode_switch_on_key(self, key):
1176 elif key == self.keys['toggle_map_mode']:
1177 self.toggle_map_mode()
1178 elif key in self.movement_keys:
1179 move_explorer(self.movement_keys[key])
1180 elif self.mode.name == 'play':
1181 if self.mode.mode_switch_on_key(self, key):
1183 elif key == self.keys['drop_thing'] and task_action_on('drop_thing'):
1184 self.send('TASK:DROP')
1185 elif key == self.keys['door'] and task_action_on('door'):
1186 self.send('TASK:DOOR')
1187 elif key == self.keys['consume'] and task_action_on('consume'):
1188 self.send('TASK:INTOXICATE')
1189 elif key == self.keys['install'] and task_action_on('install'):
1190 self.send('TASK:INSTALL')
1191 elif key == self.keys['wear'] and task_action_on('wear'):
1192 self.send('TASK:WEAR')
1193 elif key == self.keys['teleport']:
1194 player = self.game.get_thing(self.game.player_id)
1195 if player.position in self.game.portals:
1196 self.host = self.game.portals[player.position]
1200 self.log_msg('? not standing on portal')
1201 elif key in self.movement_keys and task_action_on('move'):
1202 self.send('TASK:MOVE ' + self.movement_keys[key])
1203 elif self.mode.name == 'write':
1204 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1205 self.switch_mode('edit')
1206 elif self.mode.name == 'control_tile_draw':
1207 if self.mode.mode_switch_on_key(self, key):
1209 elif key in self.movement_keys:
1210 move_explorer(self.movement_keys[key])
1211 elif key == self.keys['toggle_tile_draw']:
1212 self.tile_draw = False if self.tile_draw else True
1213 elif self.mode.name == 'admin':
1214 if self.mode.mode_switch_on_key(self, key):
1216 elif key in self.movement_keys and task_action_on('move'):
1217 self.send('TASK:MOVE ' + self.movement_keys[key])
1218 elif self.mode.name == 'edit':
1219 if self.mode.mode_switch_on_key(self, key):
1221 elif key == self.keys['flatten'] and task_action_on('flatten'):
1222 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1223 elif key == self.keys['toggle_map_mode']:
1224 self.toggle_map_mode()
1225 elif key in self.movement_keys and task_action_on('move'):
1226 self.send('TASK:MOVE ' + self.movement_keys[key])
1228 if len(sys.argv) != 2:
1229 raise ArgError('wrong number of arguments, need game host')