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': 'change terrain',
52 '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.'
55 'short': 'change protection character password',
56 'intro': '@ enter protection character for which you want to change the password:',
57 '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.'
60 'short': 'change protection character password',
62 '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.'
64 'control_tile_type': {
65 'short': 'change tiles protection',
66 'intro': '@ enter protection character which you want to draw:',
67 '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.'
69 'control_tile_draw': {
70 'short': 'change tiles protection',
72 '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.'
75 'short': 'annotate tile',
77 '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.'
80 'short': 'edit portal',
82 '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.'
87 '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'
92 'long': 'Enter your player name.'
94 'waiting_for_server': {
95 'short': 'waiting for server response',
96 'intro': '@ waiting for server …',
97 'long': 'Waiting for a server response.'
100 'short': 'waiting for server response',
102 'long': 'Waiting for a server response.'
105 'short': 'set world edit password',
107 '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.'
110 'short': 'become admin',
111 'intro': '@ enter admin password:',
112 'long': 'This mode allows you to become admin if you know an admin password.'
117 'long': 'This mode allows you access to actions limited to administrators.'
121 from ws4py.client import WebSocketBaseClient
122 class WebSocketClient(WebSocketBaseClient):
124 def __init__(self, recv_handler, *args, **kwargs):
125 super().__init__(*args, **kwargs)
126 self.recv_handler = recv_handler
129 def received_message(self, message):
131 message = str(message)
132 self.recv_handler(message)
135 def plom_closed(self):
136 return self.client_terminated
138 from plomrogue.io_tcp import PlomSocket
139 class PlomSocketClient(PlomSocket):
141 def __init__(self, recv_handler, url):
143 self.recv_handler = recv_handler
144 host, port = url.split(':')
145 super().__init__(socket.create_connection((host, port)))
153 for msg in self.recv():
154 if msg == 'NEED_SSL':
155 self.socket = ssl.wrap_socket(self.socket)
157 self.recv_handler(msg)
158 except BrokenSocketConnection:
159 pass # we assume socket will be known as dead by now
161 def cmd_TURN(game, n):
162 game.annotations = {}
166 game.turn_complete = False
168 cmd_TURN.argtypes = 'int:nonneg'
170 def cmd_LOGIN_OK(game):
171 game.tui.switch_mode('post_login_wait')
172 game.tui.send('GET_GAMESTATE')
173 game.tui.log_msg('@ welcome')
174 cmd_LOGIN_OK.argtypes = ''
176 def cmd_ADMIN_OK(game):
177 game.tui.is_admin = True
178 game.tui.log_msg('@ you now have admin rights')
179 game.tui.switch_mode('admin')
180 game.tui.do_refresh = True
181 cmd_ADMIN_OK.argtypes = ''
183 def cmd_REPLY(game, msg):
184 game.tui.log_msg('#MUSICPLAYER: ' + msg)
185 game.tui.do_refresh = True
186 cmd_REPLY.argtypes = 'string'
188 def cmd_CHAT(game, msg):
189 game.tui.log_msg('# ' + msg)
190 game.tui.do_refresh = True
191 cmd_CHAT.argtypes = 'string'
193 def cmd_PLAYER_ID(game, player_id):
194 game.player_id = player_id
195 cmd_PLAYER_ID.argtypes = 'int:nonneg'
197 def cmd_THING(game, yx, thing_type, protection, thing_id):
198 t = game.get_thing(thing_id)
200 t = ThingBase(game, thing_id)
204 t.protection = protection
205 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg'
207 def cmd_THING_NAME(game, thing_id, name):
208 t = game.get_thing(thing_id)
211 cmd_THING_NAME.argtypes = 'int:nonneg string'
213 def cmd_THING_CHAR(game, thing_id, c):
214 t = game.get_thing(thing_id)
217 cmd_THING_CHAR.argtypes = 'int:nonneg char'
219 def cmd_MAP(game, geometry, size, content):
220 map_geometry_class = globals()['MapGeometry' + geometry]
221 game.map_geometry = map_geometry_class(size)
222 game.map_content = content
223 if type(game.map_geometry) == MapGeometrySquare:
224 game.tui.movement_keys = {
225 game.tui.keys['square_move_up']: 'UP',
226 game.tui.keys['square_move_left']: 'LEFT',
227 game.tui.keys['square_move_down']: 'DOWN',
228 game.tui.keys['square_move_right']: 'RIGHT',
230 elif type(game.map_geometry) == MapGeometryHex:
231 game.tui.movement_keys = {
232 game.tui.keys['hex_move_upleft']: 'UPLEFT',
233 game.tui.keys['hex_move_upright']: 'UPRIGHT',
234 game.tui.keys['hex_move_right']: 'RIGHT',
235 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
236 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
237 game.tui.keys['hex_move_left']: 'LEFT',
239 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
241 def cmd_FOV(game, content):
243 cmd_FOV.argtypes = 'string'
245 def cmd_MAP_CONTROL(game, content):
246 game.map_control_content = content
247 cmd_MAP_CONTROL.argtypes = 'string'
249 def cmd_GAME_STATE_COMPLETE(game):
250 if game.tui.mode.name == 'post_login_wait':
251 game.tui.switch_mode('play')
252 game.turn_complete = True
253 game.tui.do_refresh = True
254 game.tui.info_cached = None
255 cmd_GAME_STATE_COMPLETE.argtypes = ''
257 def cmd_PORTAL(game, position, msg):
258 game.portals[position] = msg
259 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
261 def cmd_PLAY_ERROR(game, msg):
262 game.tui.log_msg('? ' + msg)
263 game.tui.flash = True
264 game.tui.do_refresh = True
265 cmd_PLAY_ERROR.argtypes = 'string'
267 def cmd_GAME_ERROR(game, msg):
268 game.tui.log_msg('? game error: ' + msg)
269 game.tui.do_refresh = True
270 cmd_GAME_ERROR.argtypes = 'string'
272 def cmd_ARGUMENT_ERROR(game, msg):
273 game.tui.log_msg('? syntax error: ' + msg)
274 game.tui.do_refresh = True
275 cmd_ARGUMENT_ERROR.argtypes = 'string'
277 def cmd_ANNOTATION(game, position, msg):
278 game.annotations[position] = msg
279 if game.tui.mode.shows_info:
280 game.tui.do_refresh = True
281 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
283 def cmd_TASKS(game, tasks_comma_separated):
284 game.tasks = tasks_comma_separated.split(',')
285 game.tui.mode_write.legal = 'WRITE' in game.tasks
286 game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks
287 game.tui.mode_take_thing.legal = 'PICK_UP' in game.tasks
288 cmd_TASKS.argtypes = 'string'
290 def cmd_THING_TYPE(game, thing_type, symbol_hint):
291 game.thing_types[thing_type] = symbol_hint
292 cmd_THING_TYPE.argtypes = 'string char'
294 def cmd_TERRAIN(game, terrain_char, terrain_desc):
295 game.terrains[terrain_char] = terrain_desc
296 cmd_TERRAIN.argtypes = 'char string'
300 cmd_PONG.argtypes = ''
302 def cmd_DEFAULT_COLORS(game):
303 game.tui.set_default_colors()
304 cmd_DEFAULT_COLORS.argtypes = ''
306 def cmd_RANDOM_COLORS(game):
307 game.tui.set_random_colors()
308 cmd_RANDOM_COLORS.argtypes = ''
310 class Game(GameBase):
311 turn_complete = False
315 def __init__(self, *args, **kwargs):
316 super().__init__(*args, **kwargs)
317 self.register_command(cmd_LOGIN_OK)
318 self.register_command(cmd_ADMIN_OK)
319 self.register_command(cmd_PONG)
320 self.register_command(cmd_CHAT)
321 self.register_command(cmd_REPLY)
322 self.register_command(cmd_PLAYER_ID)
323 self.register_command(cmd_TURN)
324 self.register_command(cmd_THING)
325 self.register_command(cmd_THING_TYPE)
326 self.register_command(cmd_THING_NAME)
327 self.register_command(cmd_THING_CHAR)
328 self.register_command(cmd_TERRAIN)
329 self.register_command(cmd_MAP)
330 self.register_command(cmd_MAP_CONTROL)
331 self.register_command(cmd_PORTAL)
332 self.register_command(cmd_ANNOTATION)
333 self.register_command(cmd_GAME_STATE_COMPLETE)
334 self.register_command(cmd_ARGUMENT_ERROR)
335 self.register_command(cmd_GAME_ERROR)
336 self.register_command(cmd_PLAY_ERROR)
337 self.register_command(cmd_TASKS)
338 self.register_command(cmd_FOV)
339 self.register_command(cmd_DEFAULT_COLORS)
340 self.register_command(cmd_RANDOM_COLORS)
341 self.map_content = ''
343 self.annotations = {}
347 def get_string_options(self, string_option_type):
348 if string_option_type == 'map_geometry':
349 return ['Hex', 'Square']
350 elif string_option_type == 'thing_type':
351 return self.thing_types.keys()
354 def get_command(self, command_name):
355 from functools import partial
356 f = partial(self.commands[command_name], self)
357 f.argtypes = self.commands[command_name].argtypes
362 def __init__(self, name, has_input_prompt=False, shows_info=False,
363 is_intro=False, is_single_char_entry=False):
365 self.short_desc = mode_helps[name]['short']
366 self.available_modes = []
367 self.available_actions = []
368 self.has_input_prompt = has_input_prompt
369 self.shows_info = shows_info
370 self.is_intro = is_intro
371 self.help_intro = mode_helps[name]['long']
372 self.intro_msg = mode_helps[name]['intro']
373 self.is_single_char_entry = is_single_char_entry
376 def iter_available_modes(self, tui):
377 for mode_name in self.available_modes:
378 mode = getattr(tui, 'mode_' + mode_name)
381 key = tui.keys['switch_to_' + mode.name]
384 def list_available_modes(self, tui):
386 if len(self.available_modes) > 0:
387 msg = 'Other modes available from here:\n'
388 for mode, key in self.iter_available_modes(tui):
389 msg += '[%s] – %s\n' % (key, mode.short_desc)
392 def mode_switch_on_key(self, tui, key_pressed):
393 for mode, key in self.iter_available_modes(tui):
394 if key_pressed == key:
395 tui.switch_mode(mode.name)
400 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
401 mode_admin = Mode('admin')
402 mode_play = Mode('play')
403 mode_study = Mode('study', shows_info=True)
404 mode_write = Mode('write', is_single_char_entry=True)
405 mode_edit = Mode('edit')
406 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
407 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
408 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
409 mode_control_tile_draw = Mode('control_tile_draw')
410 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
411 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
412 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
413 mode_chat = Mode('chat', has_input_prompt=True)
414 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
415 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
416 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
417 mode_password = Mode('password', has_input_prompt=True)
418 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
419 mode_command_thing = Mode('command_thing', has_input_prompt=True)
420 mode_take_thing = Mode('take_thing', has_input_prompt=True)
424 def __init__(self, host):
427 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
428 "command_thing", "take_thing"]
429 self.mode_play.available_actions = ["move", "drop_thing",
430 "teleport", "door", "consume"]
431 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
432 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
433 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
434 "control_tile_type", "chat",
435 "study", "play", "edit"]
436 self.mode_admin.available_actions = ["move"]
437 self.mode_control_tile_draw.available_modes = ["admin_enter"]
438 self.mode_control_tile_draw.available_actions = ["move_explorer",
440 self.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
441 "password", "chat", "study", "play",
443 self.mode_edit.available_actions = ["move", "flatten", "toggle_map_mode"]
448 self.parser = Parser(self.game)
450 self.do_refresh = True
451 self.queue = queue.Queue()
452 self.login_name = None
453 self.map_mode = 'terrain + things'
454 self.password = 'foo'
455 self.switch_mode('waiting_for_server')
457 'switch_to_chat': 't',
458 'switch_to_play': 'p',
459 'switch_to_password': 'P',
460 'switch_to_annotate': 'M',
461 'switch_to_portal': 'T',
462 'switch_to_study': '?',
463 'switch_to_edit': 'E',
464 'switch_to_write': 'm',
465 'switch_to_name_thing': 'N',
466 'switch_to_command_thing': 'O',
467 'switch_to_admin_enter': 'A',
468 'switch_to_control_pw_type': 'C',
469 'switch_to_control_tile_type': 'Q',
470 'switch_to_admin_thing_protect': 'T',
472 'switch_to_take_thing': 'z',
478 'toggle_map_mode': 'L',
479 'toggle_tile_draw': 'm',
480 'hex_move_upleft': 'w',
481 'hex_move_upright': 'e',
482 'hex_move_right': 'd',
483 'hex_move_downright': 'x',
484 'hex_move_downleft': 'y',
485 'hex_move_left': 'a',
486 'square_move_up': 'w',
487 'square_move_left': 'a',
488 'square_move_down': 's',
489 'square_move_right': 'd',
491 if os.path.isfile('config.json'):
492 with open('config.json', 'r') as f:
493 keys_conf = json.loads(f.read())
495 self.keys[k] = keys_conf[k]
496 self.show_help = False
497 self.disconnected = True
498 self.force_instant_connect = True
499 self.input_lines = []
503 self.offset = YX(0,0)
504 curses.wrapper(self.loop)
508 def handle_recv(msg):
514 self.log_msg('@ attempting connect')
515 socket_client_class = PlomSocketClient
516 if self.host.startswith('ws://') or self.host.startswith('wss://'):
517 socket_client_class = WebSocketClient
519 self.socket = socket_client_class(handle_recv, self.host)
520 self.socket_thread = threading.Thread(target=self.socket.run)
521 self.socket_thread.start()
522 self.disconnected = False
523 self.game.thing_types = {}
524 self.game.terrains = {}
525 time.sleep(0.1) # give potential SSL negotation some time …
526 self.socket.send('TASKS')
527 self.socket.send('TERRAINS')
528 self.socket.send('THING_TYPES')
529 self.switch_mode('login')
530 except ConnectionRefusedError:
531 self.log_msg('@ server connect failure')
532 self.disconnected = True
533 self.switch_mode('waiting_for_server')
534 self.do_refresh = True
537 self.log_msg('@ attempting reconnect')
539 # necessitated by some strange SSL race conditions with ws4py
540 time.sleep(0.1) # FIXME find out why exactly necessary
541 self.switch_mode('waiting_for_server')
546 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
547 raise BrokenSocketConnection
548 self.socket.send(msg)
549 except (BrokenPipeError, BrokenSocketConnection):
550 self.log_msg('@ server disconnected :(')
551 self.disconnected = True
552 self.force_instant_connect = True
553 self.do_refresh = True
555 def log_msg(self, msg):
557 if len(self.log) > 100:
558 self.log = self.log[-100:]
560 def restore_input_values(self):
561 if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
562 self.input_ = self.game.annotations[self.explorer]
563 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
564 self.input_ = self.game.portals[self.explorer]
565 elif self.mode.name == 'password':
566 self.input_ = self.password
567 elif self.mode.name == 'name_thing':
568 if hasattr(self.thing_selected, 'name'):
569 self.input_ = self.thing_selected.name
570 elif self.mode.name == 'admin_thing_protect':
571 if hasattr(self.thing_selected, 'protection'):
572 self.input_ = self.thing_selected.protection
574 def send_tile_control_command(self):
575 self.send('SET_TILE_CONTROL %s %s' %
576 (self.explorer, quote(self.tile_control_char)))
578 def toggle_map_mode(self):
579 if self.map_mode == 'terrain only':
580 self.map_mode = 'terrain + annotations'
581 elif self.map_mode == 'terrain + annotations':
582 self.map_mode = 'terrain + things'
583 elif self.map_mode == 'terrain + things':
584 self.map_mode = 'protections'
585 elif self.map_mode == 'protections':
586 self.map_mode = 'terrain only'
588 def switch_mode(self, mode_name):
589 if self.mode and self.mode.name == 'control_tile_draw':
590 self.log_msg('@ finished tile protection drawing.')
591 self.tile_draw = False
592 if mode_name == 'admin_enter' and self.is_admin:
594 elif mode_name in {'name_thing', 'admin_thing_protect'}:
595 player = self.game.get_thing(self.game.player_id)
597 for t in [t for t in self.game.things if t.position == player.position
598 and t.id_ != player.id_]:
603 self.log_msg('? not standing over thing')
606 self.thing_selected = thing
607 self.mode = getattr(self, 'mode_' + mode_name)
608 if self.mode.name in {'control_tile_draw', 'control_tile_type',
610 self.map_mode = 'protections'
611 elif self.mode.name != 'edit':
612 self.map_mode = 'terrain + things'
613 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
614 player = self.game.get_thing(self.game.player_id)
615 self.explorer = YX(player.position.y, player.position.x)
616 if self.mode.is_single_char_entry:
617 self.show_help = True
618 if len(self.mode.intro_msg) > 0:
619 self.log_msg(self.mode.intro_msg)
620 if self.mode.name == 'login':
622 self.send('LOGIN ' + quote(self.login_name))
624 self.log_msg('@ enter username')
625 elif self.mode.name == 'take_thing':
626 self.log_msg('Things in reach for pick-up:')
627 player = self.game.get_thing(self.game.player_id)
628 select_range = [player.position,
629 player.position + YX(0,-1),
630 player.position + YX(0, 1),
631 player.position + YX(-1, 0),
632 player.position + YX(1, 0)]
633 if type(self.game.map_geometry) == MapGeometryHex:
634 if player.position.y % 2:
635 select_range += [player.position + YX(-1, 1),
636 player.position + YX(1, 1)]
638 select_range += [player.position + YX(-1, -1),
639 player.position + YX(1, -1)]
640 self.selectables = [t for t in self.game.things
641 if t != player and t.type_ != 'Player'
642 and t.position in select_range]
643 if len(self.selectables) == 0:
646 for i in range(len(self.selectables)):
647 t = self.selectables[i]
648 self.log_msg(str(i) + ': ' + self.get_thing_info(t))
649 elif self.mode.name == 'command_thing':
650 self.send('TASK:COMMAND ' + quote('HELP'))
651 elif self.mode.name == 'control_pw_pw':
652 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
653 elif self.mode.name == 'control_tile_draw':
654 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']))
656 self.restore_input_values()
658 def set_default_colors(self):
659 curses.init_color(1, 1000, 1000, 1000)
660 curses.init_color(2, 0, 0, 0)
661 self.do_refresh = True
663 def set_random_colors(self):
667 return int(offset + random.random()*375)
669 curses.init_color(1, rand(625), rand(625), rand(625))
670 curses.init_color(2, rand(0), rand(0), rand(0))
671 self.do_refresh = True
675 return self.info_cached
676 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
678 if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
679 info_to_cache += 'outside field of view'
681 terrain_char = self.game.map_content[pos_i]
683 if terrain_char in self.game.terrains:
684 terrain_desc = self.game.terrains[terrain_char]
685 info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
687 protection = self.game.map_control_content[pos_i]
688 if protection == '.':
689 protection = 'unprotected'
690 info_to_cache += 'PROTECTION: %s\n' % protection
691 for t in self.game.things:
692 if t.position == self.explorer:
693 info_to_cache += 'THING: %s' % self.get_thing_info(t)
694 protection = t.protection
695 if protection == '.':
697 info_to_cache += ' / protection: %s\n' % protection
698 if self.explorer in self.game.portals:
699 info_to_cache += 'PORTAL: ' +\
700 self.game.portals[self.explorer] + '\n'
702 info_to_cache += 'PORTAL: (none)\n'
703 if self.explorer in self.game.annotations:
704 info_to_cache += 'ANNOTATION: ' +\
705 self.game.annotations[self.explorer]
706 self.info_cached = info_to_cache
707 return self.info_cached
709 def get_thing_info(self, t):
711 (t.type_, self.game.thing_types[t.type_])
712 if hasattr(t, 'thing_char'):
714 if hasattr(t, 'name'):
715 info += ' (%s)' % t.name
718 def loop(self, stdscr):
721 def safe_addstr(y, x, line):
722 if y < self.size.y - 1 or x + len(line) < self.size.x:
723 stdscr.addstr(y, x, line, curses.color_pair(1))
724 else: # workaround to <https://stackoverflow.com/q/7063128>
725 cut_i = self.size.x - x - 1
727 last_char = line[cut_i]
728 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
729 stdscr.insstr(y, self.size.x - 2, ' ')
730 stdscr.addstr(y, x, cut, curses.color_pair(1))
732 def handle_input(msg):
733 command, args = self.parser.parse(msg)
736 def task_action_on(action):
737 return action_tasks[action] in self.game.tasks
739 def msg_into_lines_of_width(msg, width):
743 for i in range(len(msg)):
744 if x >= width or msg[i] == "\n":
756 def reset_screen_size():
757 self.size = YX(*stdscr.getmaxyx())
758 self.size = self.size - YX(self.size.y % 4, 0)
759 self.size = self.size - YX(0, self.size.x % 4)
760 self.window_width = int(self.size.x / 2)
762 def recalc_input_lines():
763 if not self.mode.has_input_prompt:
764 self.input_lines = []
766 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
769 def move_explorer(direction):
770 target = self.game.map_geometry.move_yx(self.explorer, direction)
772 self.info_cached = None
773 self.explorer = target
775 self.send_tile_control_command()
781 for line in self.log:
782 lines += msg_into_lines_of_width(line, self.window_width)
785 max_y = self.size.y - len(self.input_lines)
786 for i in range(len(lines)):
787 if (i >= max_y - height_header):
789 safe_addstr(max_y - i - 1, self.window_width, lines[i])
792 info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
793 lines = msg_into_lines_of_width(info, self.window_width)
795 for i in range(len(lines)):
796 y = height_header + i
797 if y >= self.size.y - len(self.input_lines):
799 safe_addstr(y, self.window_width, lines[i])
802 y = self.size.y - len(self.input_lines)
803 for i in range(len(self.input_lines)):
804 safe_addstr(y, self.window_width, self.input_lines[i])
808 if not self.game.turn_complete:
810 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
813 help = "hit [%s] for help" % self.keys['help']
814 if self.mode.has_input_prompt:
815 help = "enter /help for help"
816 safe_addstr(1, self.window_width,
817 'MODE: %s – %s' % (self.mode.short_desc, help))
820 if not self.game.turn_complete and len(self.map_lines) == 0:
822 if self.game.turn_complete:
824 for y in range(self.game.map_geometry.size.y):
825 start = self.game.map_geometry.size.x * y
826 end = start + self.game.map_geometry.size.x
827 if self.map_mode == 'protections':
828 map_lines_split += [[c + ' ' for c
829 in self.game.map_control_content[start:end]]]
831 map_lines_split += [[c + ' ' for c
832 in self.game.map_content[start:end]]]
833 if self.map_mode == 'terrain + annotations':
834 for p in self.game.annotations:
835 map_lines_split[p.y][p.x] = 'A '
836 elif self.map_mode == 'terrain + things':
837 for p in self.game.portals.keys():
838 original = map_lines_split[p.y][p.x]
839 map_lines_split[p.y][p.x] = original[0] + 'P'
842 def draw_thing(t, used_positions):
843 symbol = self.game.thing_types[t.type_]
845 if hasattr(t, 'thing_char'):
846 meta_char = t.thing_char
847 if t.position in used_positions:
849 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
850 used_positions += [t.position]
852 for t in [t for t in self.game.things if t.type_ != 'Player']:
853 draw_thing(t, used_positions)
854 for t in [t for t in self.game.things if t.type_ == 'Player']:
855 draw_thing(t, used_positions)
856 player = self.game.get_thing(self.game.player_id)
857 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
858 map_lines_split[self.explorer.y][self.explorer.x] = '??'
859 elif self.map_mode != 'terrain + things':
860 map_lines_split[player.position.y][player.position.x] = '??'
862 if type(self.game.map_geometry) == MapGeometryHex:
864 for line in map_lines_split:
865 self.map_lines += [indent * ' ' + ''.join(line)]
866 indent = 0 if indent else 1
868 for line in map_lines_split:
869 self.map_lines += [''.join(line)]
870 window_center = YX(int(self.size.y / 2),
871 int(self.window_width / 2))
872 center = player.position
873 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
874 center = self.explorer
875 center = YX(center.y, center.x * 2)
876 self.offset = center - window_center
877 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
878 self.offset += YX(0, 1)
879 term_y = max(0, -self.offset.y)
880 term_x = max(0, -self.offset.x)
881 map_y = max(0, self.offset.y)
882 map_x = max(0, self.offset.x)
883 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
884 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
885 safe_addstr(term_y, term_x, to_draw)
890 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
891 self.mode.help_intro)
892 if len(self.mode.available_actions) > 0:
893 content += "Available actions:\n"
894 for action in self.mode.available_actions:
895 if action in action_tasks:
896 if action_tasks[action] not in self.game.tasks:
898 if action == 'move_explorer':
901 key = ','.join(self.movement_keys)
903 key = self.keys[action]
904 content += '[%s] – %s\n' % (key, action_descriptions[action])
906 content += self.mode.list_available_modes(self)
907 for i in range(self.size.y):
909 self.window_width * (not self.mode.has_input_prompt),
910 ' ' * self.window_width)
912 for line in content.split('\n'):
913 lines += msg_into_lines_of_width(line, self.window_width)
914 for i in range(len(lines)):
918 self.window_width * (not self.mode.has_input_prompt),
923 stdscr.bkgd(' ', curses.color_pair(1))
925 if self.mode.has_input_prompt:
927 if self.mode.shows_info:
932 if not self.mode.is_intro:
938 action_descriptions = {
940 'flatten': 'flatten surroundings',
941 'teleport': 'teleport',
942 'take_thing': 'pick up thing',
943 'drop_thing': 'drop thing',
944 'toggle_map_mode': 'toggle map view',
945 'toggle_tile_draw': 'toggle protection character drawing',
946 'door': 'open/close',
947 'consume': 'consume',
951 'flatten': 'FLATTEN_SURROUNDINGS',
952 'take_thing': 'PICK_UP',
953 'drop_thing': 'DROP',
956 'command': 'COMMAND',
957 'consume': 'INTOXICATE',
960 curses.curs_set(False) # hide cursor
962 self.set_default_colors()
963 curses.init_pair(1, 1, 2)
966 self.explorer = YX(0, 0)
969 interval = datetime.timedelta(seconds=5)
970 last_ping = datetime.datetime.now() - interval
972 if self.disconnected and self.force_instant_connect:
973 self.force_instant_connect = False
975 now = datetime.datetime.now()
976 if now - last_ping > interval:
977 if self.disconnected:
987 self.do_refresh = False
990 msg = self.queue.get(block=False)
995 key = stdscr.getkey()
996 self.do_refresh = True
999 self.show_help = False
1000 if key == 'KEY_RESIZE':
1002 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1003 self.input_ = self.input_[:-1]
1004 elif self.mode.has_input_prompt and key == '\n' and self.input_ == ''\
1005 and self.mode.name in {'chat', 'command_thing', 'take_thing',
1007 if self.mode.name != 'chat':
1008 self.log_msg('@ aborted')
1009 self.switch_mode('play')
1010 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1011 self.show_help = True
1013 self.restore_input_values()
1014 elif self.mode.has_input_prompt and key != '\n': # Return key
1016 max_length = self.window_width * self.size.y - len(input_prompt) - 1
1017 if len(self.input_) > max_length:
1018 self.input_ = self.input_[:max_length]
1019 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1020 self.show_help = True
1021 elif self.mode.name == 'login' and key == '\n':
1022 self.login_name = self.input_
1023 self.send('LOGIN ' + quote(self.input_))
1025 elif self.mode.name == 'take_thing' and key == '\n':
1027 i = int(self.input_)
1028 if i < 0 or i >= len(self.selectables):
1029 self.log_msg('? invalid index, aborted')
1031 self.send('TASK:PICK_UP %s' % self.selectables[i].id_)
1033 self.log_msg('? invalid index, aborted')
1035 self.switch_mode('play')
1036 elif self.mode.name == 'command_thing' and key == '\n':
1037 if task_action_on('command'):
1038 self.send('TASK:COMMAND ' + quote(self.input_))
1040 elif self.mode.name == 'control_pw_pw' and key == '\n':
1041 if self.input_ == '':
1042 self.log_msg('@ aborted')
1044 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1045 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1046 self.switch_mode('admin')
1047 elif self.mode.name == 'password' and key == '\n':
1048 if self.input_ == '':
1050 self.password = self.input_
1051 self.switch_mode('edit')
1052 elif self.mode.name == 'admin_enter' and key == '\n':
1053 self.send('BECOME_ADMIN ' + quote(self.input_))
1054 self.switch_mode('play')
1055 elif self.mode.name == 'control_pw_type' and key == '\n':
1056 if len(self.input_) != 1:
1057 self.log_msg('@ entered non-single-char, therefore aborted')
1058 self.switch_mode('admin')
1060 self.tile_control_char = self.input_
1061 self.switch_mode('control_pw_pw')
1062 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1063 if len(self.input_) != 1:
1064 self.log_msg('@ entered non-single-char, therefore aborted')
1066 self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
1067 quote(self.input_)))
1068 self.log_msg('@ sent new protection character for thing')
1069 self.switch_mode('admin')
1070 elif self.mode.name == 'control_tile_type' and key == '\n':
1071 if len(self.input_) != 1:
1072 self.log_msg('@ entered non-single-char, therefore aborted')
1073 self.switch_mode('admin')
1075 self.tile_control_char = self.input_
1076 self.switch_mode('control_tile_draw')
1077 elif self.mode.name == 'chat' and key == '\n':
1078 if self.input_ == '':
1080 if self.input_[0] == '/':
1081 if self.input_.startswith('/nick'):
1082 tokens = self.input_.split(maxsplit=1)
1083 if len(tokens) == 2:
1084 self.send('NICK ' + quote(tokens[1]))
1086 self.log_msg('? need login name')
1088 self.log_msg('? unknown command')
1090 self.send('ALL ' + quote(self.input_))
1092 elif self.mode.name == 'name_thing' and key == '\n':
1093 if self.input_ == '':
1095 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
1097 quote(self.password)))
1098 self.switch_mode('edit')
1099 elif self.mode.name == 'annotate' and key == '\n':
1100 if self.input_ == '':
1102 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1103 quote(self.password)))
1104 self.switch_mode('edit')
1105 elif self.mode.name == 'portal' and key == '\n':
1106 if self.input_ == '':
1108 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1109 quote(self.password)))
1110 self.switch_mode('edit')
1111 elif self.mode.name == 'study':
1112 if self.mode.mode_switch_on_key(self, key):
1114 elif key == self.keys['toggle_map_mode']:
1115 self.toggle_map_mode()
1116 elif key in self.movement_keys:
1117 move_explorer(self.movement_keys[key])
1118 elif self.mode.name == 'play':
1119 if self.mode.mode_switch_on_key(self, key):
1121 elif key == self.keys['drop_thing'] and task_action_on('drop_thing'):
1122 self.send('TASK:DROP')
1123 elif key == self.keys['door'] and task_action_on('door'):
1124 self.send('TASK:DOOR')
1125 elif key == self.keys['consume'] and task_action_on('consume'):
1126 self.send('TASK:INTOXICATE')
1127 elif key == self.keys['teleport']:
1128 player = self.game.get_thing(self.game.player_id)
1129 if player.position in self.game.portals:
1130 self.host = self.game.portals[player.position]
1134 self.log_msg('? not standing on portal')
1135 elif key in self.movement_keys and task_action_on('move'):
1136 self.send('TASK:MOVE ' + self.movement_keys[key])
1137 elif self.mode.name == 'write':
1138 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1139 self.switch_mode('edit')
1140 elif self.mode.name == 'control_tile_draw':
1141 if self.mode.mode_switch_on_key(self, key):
1143 elif key in self.movement_keys:
1144 move_explorer(self.movement_keys[key])
1145 elif key == self.keys['toggle_tile_draw']:
1146 self.tile_draw = False if self.tile_draw else True
1147 elif self.mode.name == 'admin':
1148 if self.mode.mode_switch_on_key(self, key):
1150 elif key in self.movement_keys and task_action_on('move'):
1151 self.send('TASK:MOVE ' + self.movement_keys[key])
1152 elif self.mode.name == 'edit':
1153 if self.mode.mode_switch_on_key(self, key):
1155 elif key == self.keys['flatten'] and task_action_on('flatten'):
1156 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1157 elif key == self.keys['toggle_map_mode']:
1158 self.toggle_map_mode()
1159 elif key in self.movement_keys and task_action_on('move'):
1160 self.send('TASK:MOVE ' + self.movement_keys[key])
1162 if len(sys.argv) != 2:
1163 raise ArgError('wrong number of arguments, need game host')