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',
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:'
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('selectable things:')
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 if self.mode.name == 'chat':
907 content += '/nick NAME – re-name yourself to NAME\n'
908 content += '/%s or /play – switch to play mode\n' % self.keys['switch_to_play']
909 content += '/%s or /study – switch to study mode\n' % self.keys['switch_to_study']
910 content += '/%s or /edit – switch to world edit mode\n' % self.keys['switch_to_edit']
911 content += '/%s or /admin – switch to admin mode\n' % self.keys['switch_to_admin_enter']
912 content += self.mode.list_available_modes(self)
913 for i in range(self.size.y):
915 self.window_width * (not self.mode.has_input_prompt),
916 ' ' * self.window_width)
918 for line in content.split('\n'):
919 lines += msg_into_lines_of_width(line, self.window_width)
920 for i in range(len(lines)):
924 self.window_width * (not self.mode.has_input_prompt),
929 stdscr.bkgd(' ', curses.color_pair(1))
931 if self.mode.has_input_prompt:
933 if self.mode.shows_info:
938 if not self.mode.is_intro:
944 action_descriptions = {
946 'flatten': 'flatten surroundings',
947 'teleport': 'teleport',
948 'take_thing': 'pick up thing',
949 'drop_thing': 'drop thing',
950 'toggle_map_mode': 'toggle map view',
951 'toggle_tile_draw': 'toggle protection character drawing',
952 'door': 'open/close',
953 'consume': 'consume',
957 'flatten': 'FLATTEN_SURROUNDINGS',
958 'take_thing': 'PICK_UP',
959 'drop_thing': 'DROP',
962 'command': 'COMMAND',
963 'consume': 'INTOXICATE',
966 curses.curs_set(False) # hide cursor
968 self.set_default_colors()
969 curses.init_pair(1, 1, 2)
972 self.explorer = YX(0, 0)
975 interval = datetime.timedelta(seconds=5)
976 last_ping = datetime.datetime.now() - interval
978 if self.disconnected and self.force_instant_connect:
979 self.force_instant_connect = False
981 now = datetime.datetime.now()
982 if now - last_ping > interval:
983 if self.disconnected:
993 self.do_refresh = False
996 msg = self.queue.get(block=False)
1001 key = stdscr.getkey()
1002 self.do_refresh = True
1003 except curses.error:
1005 self.show_help = False
1006 if key == 'KEY_RESIZE':
1008 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1009 self.input_ = self.input_[:-1]
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':
1026 if self.input_ == '':
1027 self.log_msg('@ aborted')
1030 i = int(self.input_)
1031 if i < 0 or i >= len(self.selectables):
1032 self.log_msg('? invalid index, aborted')
1034 self.send('TASK:PICK_UP %s' % self.selectables[i].id_)
1036 self.log_msg('? invalid index, aborted')
1038 self.switch_mode('play')
1039 elif self.mode.name == 'command_thing' and key == '\n':
1040 if self.input_ == '':
1041 self.log_msg('@ aborted')
1042 self.switch_mode('play')
1043 elif task_action_on('command'):
1044 self.send('TASK:COMMAND ' + quote(self.input_))
1046 elif self.mode.name == 'control_pw_pw' and key == '\n':
1047 if self.input_ == '':
1048 self.log_msg('@ aborted')
1050 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1051 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1052 self.switch_mode('admin')
1053 elif self.mode.name == 'password' and key == '\n':
1054 if self.input_ == '':
1056 self.password = self.input_
1057 self.switch_mode('edit')
1058 elif self.mode.name == 'admin_enter' and key == '\n':
1059 self.send('BECOME_ADMIN ' + quote(self.input_))
1060 self.switch_mode('play')
1061 elif self.mode.name == 'control_pw_type' and key == '\n':
1062 if len(self.input_) != 1:
1063 self.log_msg('@ entered non-single-char, therefore aborted')
1064 self.switch_mode('admin')
1066 self.tile_control_char = self.input_
1067 self.switch_mode('control_pw_pw')
1068 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1069 if len(self.input_) != 1:
1070 self.log_msg('@ entered non-single-char, therefore aborted')
1072 self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
1073 quote(self.input_)))
1074 self.log_msg('@ sent new protection character for thing')
1075 self.switch_mode('admin')
1076 elif self.mode.name == 'control_tile_type' and key == '\n':
1077 if len(self.input_) != 1:
1078 self.log_msg('@ entered non-single-char, therefore aborted')
1079 self.switch_mode('admin')
1081 self.tile_control_char = self.input_
1082 self.switch_mode('control_tile_draw')
1083 elif self.mode.name == 'chat' and key == '\n':
1084 if self.input_ == '':
1086 if self.input_[0] == '/': # FIXME fails on empty input
1087 if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
1088 self.switch_mode('play')
1089 elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
1090 self.switch_mode('study')
1091 elif self.input_ in {'/' + self.keys['switch_to_edit'], '/edit'}:
1092 self.switch_mode('edit')
1093 elif self.input_ in {'/' + self.keys['switch_to_admin_enter'], '/admin'}:
1094 self.switch_mode('admin_enter')
1095 elif self.input_.startswith('/nick'):
1096 tokens = self.input_.split(maxsplit=1)
1097 if len(tokens) == 2:
1098 self.send('NICK ' + quote(tokens[1]))
1100 self.log_msg('? need login name')
1102 self.log_msg('? unknown command')
1104 self.send('ALL ' + quote(self.input_))
1106 elif self.mode.name == 'name_thing' and key == '\n':
1107 if self.input_ == '':
1109 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
1111 quote(self.password)))
1112 self.switch_mode('edit')
1113 elif self.mode.name == 'annotate' and key == '\n':
1114 if self.input_ == '':
1116 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1117 quote(self.password)))
1118 self.switch_mode('edit')
1119 elif self.mode.name == 'portal' and key == '\n':
1120 if self.input_ == '':
1122 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1123 quote(self.password)))
1124 self.switch_mode('edit')
1125 elif self.mode.name == 'study':
1126 if self.mode.mode_switch_on_key(self, key):
1128 elif key == self.keys['toggle_map_mode']:
1129 self.toggle_map_mode()
1130 elif key in self.movement_keys:
1131 move_explorer(self.movement_keys[key])
1132 elif self.mode.name == 'play':
1133 if self.mode.mode_switch_on_key(self, key):
1135 elif key == self.keys['drop_thing'] and task_action_on('drop_thing'):
1136 self.send('TASK:DROP')
1137 elif key == self.keys['door'] and task_action_on('door'):
1138 self.send('TASK:DOOR')
1139 elif key == self.keys['consume'] and task_action_on('consume'):
1140 self.send('TASK:INTOXICATE')
1141 elif key == self.keys['teleport']:
1142 player = self.game.get_thing(self.game.player_id)
1143 if player.position in self.game.portals:
1144 self.host = self.game.portals[player.position]
1148 self.log_msg('? not standing on portal')
1149 elif key in self.movement_keys and task_action_on('move'):
1150 self.send('TASK:MOVE ' + self.movement_keys[key])
1151 elif self.mode.name == 'write':
1152 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1153 self.switch_mode('edit')
1154 elif self.mode.name == 'control_tile_draw':
1155 if self.mode.mode_switch_on_key(self, key):
1157 elif key in self.movement_keys:
1158 move_explorer(self.movement_keys[key])
1159 elif key == self.keys['toggle_tile_draw']:
1160 self.tile_draw = False if self.tile_draw else True
1161 elif self.mode.name == 'admin':
1162 if self.mode.mode_switch_on_key(self, key):
1164 elif key in self.movement_keys and task_action_on('move'):
1165 self.send('TASK:MOVE ' + self.movement_keys[key])
1166 elif self.mode.name == 'edit':
1167 if self.mode.mode_switch_on_key(self, key):
1169 elif key == self.keys['flatten'] and task_action_on('flatten'):
1170 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1171 elif key == self.keys['toggle_map_mode']:
1172 self.toggle_map_mode()
1173 elif key in self.movement_keys and task_action_on('move'):
1174 self.send('TASK:MOVE ' + self.movement_keys[key])
1176 if len(sys.argv) != 2:
1177 raise ArgError('wrong number of arguments, need game host')