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
17 'long': 'This mode allows you to interact with the map in various ways.'
21 '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.'},
23 'short': 'world edit',
24 '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.'
27 'short': 'name thing',
28 'long': 'Give name to/change name of thing here.'
30 'admin_thing_protect': {
31 'short': 'change thing protection',
32 'long': 'Change protection character for thing here.'
35 'short': 'change terrain',
36 '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.'
39 'short': 'change protection character password',
40 '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.'
43 'short': 'change protection character password',
44 '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.'
46 'control_tile_type': {
47 'short': 'change tiles protection',
48 '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.'
50 'control_tile_draw': {
51 'short': 'change tiles protection',
52 '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.'
55 'short': 'annotate tile',
56 '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.'
59 'short': 'edit portal',
60 '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.'
64 '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:'
68 'long': 'Enter your player name.'
70 'waiting_for_server': {
71 'short': 'waiting for server response',
72 'long': 'Waiting for a server response.'
75 'short': 'waiting for server response',
76 'long': 'Waiting for a server response.'
79 'short': 'set world edit password',
80 '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.'
83 'short': 'become admin',
84 'long': 'This mode allows you to become admin if you know an admin password.'
88 'long': 'This mode allows you access to actions limited to administrators.'
92 from ws4py.client import WebSocketBaseClient
93 class WebSocketClient(WebSocketBaseClient):
95 def __init__(self, recv_handler, *args, **kwargs):
96 super().__init__(*args, **kwargs)
97 self.recv_handler = recv_handler
100 def received_message(self, message):
102 message = str(message)
103 self.recv_handler(message)
106 def plom_closed(self):
107 return self.client_terminated
109 from plomrogue.io_tcp import PlomSocket
110 class PlomSocketClient(PlomSocket):
112 def __init__(self, recv_handler, url):
114 self.recv_handler = recv_handler
115 host, port = url.split(':')
116 super().__init__(socket.create_connection((host, port)))
124 for msg in self.recv():
125 if msg == 'NEED_SSL':
126 self.socket = ssl.wrap_socket(self.socket)
128 self.recv_handler(msg)
129 except BrokenSocketConnection:
130 pass # we assume socket will be known as dead by now
132 def cmd_TURN(game, n):
138 game.turn_complete = False
139 cmd_TURN.argtypes = 'int:nonneg'
141 def cmd_LOGIN_OK(game):
142 game.tui.switch_mode('post_login_wait')
143 game.tui.send('GET_GAMESTATE')
144 game.tui.log_msg('@ welcome')
145 cmd_LOGIN_OK.argtypes = ''
147 def cmd_ADMIN_OK(game):
148 game.tui.is_admin = True
149 game.tui.log_msg('@ you now have admin rights')
150 game.tui.switch_mode('admin')
151 game.tui.do_refresh = True
152 cmd_ADMIN_OK.argtypes = ''
154 def cmd_CHAT(game, msg):
155 game.tui.log_msg('# ' + msg)
156 game.tui.do_refresh = True
157 cmd_CHAT.argtypes = 'string'
159 def cmd_PLAYER_ID(game, player_id):
160 game.player_id = player_id
161 cmd_PLAYER_ID.argtypes = 'int:nonneg'
163 def cmd_THING(game, yx, thing_type, protection, thing_id):
164 t = game.get_thing(thing_id)
166 t = ThingBase(game, thing_id)
170 t.protection = protection
171 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg'
173 def cmd_THING_NAME(game, thing_id, name):
174 t = game.get_thing(thing_id)
177 cmd_THING_NAME.argtypes = 'int:nonneg string'
179 def cmd_THING_CHAR(game, thing_id, c):
180 t = game.get_thing(thing_id)
183 cmd_THING_CHAR.argtypes = 'int:nonneg char'
185 def cmd_MAP(game, geometry, size, content):
186 map_geometry_class = globals()['MapGeometry' + geometry]
187 game.map_geometry = map_geometry_class(size)
188 game.map_content = content
189 if type(game.map_geometry) == MapGeometrySquare:
190 game.tui.movement_keys = {
191 game.tui.keys['square_move_up']: 'UP',
192 game.tui.keys['square_move_left']: 'LEFT',
193 game.tui.keys['square_move_down']: 'DOWN',
194 game.tui.keys['square_move_right']: 'RIGHT',
196 elif type(game.map_geometry) == MapGeometryHex:
197 game.tui.movement_keys = {
198 game.tui.keys['hex_move_upleft']: 'UPLEFT',
199 game.tui.keys['hex_move_upright']: 'UPRIGHT',
200 game.tui.keys['hex_move_right']: 'RIGHT',
201 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
202 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
203 game.tui.keys['hex_move_left']: 'LEFT',
205 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
207 def cmd_FOV(game, content):
209 cmd_FOV.argtypes = 'string'
211 def cmd_MAP_CONTROL(game, content):
212 game.map_control_content = content
213 cmd_MAP_CONTROL.argtypes = 'string'
215 def cmd_GAME_STATE_COMPLETE(game):
216 if game.tui.mode.name == 'post_login_wait':
217 game.tui.switch_mode('play')
218 if game.tui.mode.shows_info:
219 game.tui.query_info()
220 game.turn_complete = True
221 game.tui.do_refresh = True
222 cmd_GAME_STATE_COMPLETE.argtypes = ''
224 def cmd_PORTAL(game, position, msg):
225 game.portals[position] = msg
226 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
228 def cmd_PLAY_ERROR(game, msg):
229 game.tui.log_msg('? ' + msg)
230 game.tui.flash = True
231 game.tui.do_refresh = True
232 cmd_PLAY_ERROR.argtypes = 'string'
234 def cmd_GAME_ERROR(game, msg):
235 game.tui.log_msg('? game error: ' + msg)
236 game.tui.do_refresh = True
237 cmd_GAME_ERROR.argtypes = 'string'
239 def cmd_ARGUMENT_ERROR(game, msg):
240 game.tui.log_msg('? syntax error: ' + msg)
241 game.tui.do_refresh = True
242 cmd_ARGUMENT_ERROR.argtypes = 'string'
244 def cmd_ANNOTATION_HINT(game, position):
245 game.info_hints += [position]
246 cmd_ANNOTATION_HINT.argtypes = 'yx_tuple:nonneg'
248 def cmd_ANNOTATION(game, position, msg):
249 game.info_db[position] = msg
250 if game.tui.mode.shows_info:
251 game.tui.do_refresh = True
252 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
254 def cmd_TASKS(game, tasks_comma_separated):
255 game.tasks = tasks_comma_separated.split(',')
256 game.tui.mode_write.legal = 'WRITE' in game.tasks
257 cmd_TASKS.argtypes = 'string'
259 def cmd_THING_TYPE(game, thing_type, symbol_hint):
260 game.thing_types[thing_type] = symbol_hint
261 cmd_THING_TYPE.argtypes = 'string char'
263 def cmd_TERRAIN(game, terrain_char, terrain_desc):
264 game.terrains[terrain_char] = terrain_desc
265 cmd_TERRAIN.argtypes = 'char string'
269 cmd_PONG.argtypes = ''
271 class Game(GameBase):
272 turn_complete = False
276 def __init__(self, *args, **kwargs):
277 super().__init__(*args, **kwargs)
278 self.register_command(cmd_LOGIN_OK)
279 self.register_command(cmd_ADMIN_OK)
280 self.register_command(cmd_PONG)
281 self.register_command(cmd_CHAT)
282 self.register_command(cmd_PLAYER_ID)
283 self.register_command(cmd_TURN)
284 self.register_command(cmd_THING)
285 self.register_command(cmd_THING_TYPE)
286 self.register_command(cmd_THING_NAME)
287 self.register_command(cmd_THING_CHAR)
288 self.register_command(cmd_TERRAIN)
289 self.register_command(cmd_MAP)
290 self.register_command(cmd_MAP_CONTROL)
291 self.register_command(cmd_PORTAL)
292 self.register_command(cmd_ANNOTATION)
293 self.register_command(cmd_ANNOTATION_HINT)
294 self.register_command(cmd_GAME_STATE_COMPLETE)
295 self.register_command(cmd_ARGUMENT_ERROR)
296 self.register_command(cmd_GAME_ERROR)
297 self.register_command(cmd_PLAY_ERROR)
298 self.register_command(cmd_TASKS)
299 self.register_command(cmd_FOV)
300 self.map_content = ''
307 def get_string_options(self, string_option_type):
308 if string_option_type == 'map_geometry':
309 return ['Hex', 'Square']
310 elif string_option_type == 'thing_type':
311 return self.thing_types.keys()
314 def get_command(self, command_name):
315 from functools import partial
316 f = partial(self.commands[command_name], self)
317 f.argtypes = self.commands[command_name].argtypes
322 def __init__(self, name, has_input_prompt=False, shows_info=False,
323 is_intro=False, is_single_char_entry=False):
325 self.short_desc = mode_helps[name]['short']
326 self.available_modes = []
327 self.available_actions = []
328 self.has_input_prompt = has_input_prompt
329 self.shows_info = shows_info
330 self.is_intro = is_intro
331 self.help_intro = mode_helps[name]['long']
332 self.is_single_char_entry = is_single_char_entry
335 def iter_available_modes(self, tui):
336 for mode_name in self.available_modes:
337 mode = getattr(tui, 'mode_' + mode_name)
340 key = tui.keys['switch_to_' + mode.name]
343 def list_available_modes(self, tui):
345 if len(self.available_modes) > 0:
346 msg = 'Other modes available from here:\n'
347 for mode, key in self.iter_available_modes(tui):
348 msg += '[%s] – %s\n' % (key, mode.short_desc)
351 def mode_switch_on_key(self, tui, key_pressed):
352 for mode, key in self.iter_available_modes(tui):
353 if key_pressed == key:
354 tui.switch_mode(mode.name)
359 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
360 mode_admin = Mode('admin')
361 mode_play = Mode('play')
362 mode_study = Mode('study', shows_info=True)
363 mode_write = Mode('write', is_single_char_entry=True)
364 mode_edit = Mode('edit')
365 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
366 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
367 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
368 mode_control_tile_draw = Mode('control_tile_draw')
369 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
370 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
371 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
372 mode_chat = Mode('chat', has_input_prompt=True)
373 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
374 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
375 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
376 mode_password = Mode('password', has_input_prompt=True)
377 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
381 def __init__(self, host):
384 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter"]
385 self.mode_play.available_actions = ["move", "take_thing", "drop_thing",
387 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
388 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
389 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
390 "control_tile_type", "chat",
391 "study", "play", "edit"]
392 self.mode_admin.available_actions = ["move"]
393 self.mode_edit.available_actions = ["move", "flatten", "toggle_map_mode"]
394 self.mode_control_tile_draw.available_actions = ["toggle_tile_draw"]
395 self.mode_control_tile_draw.available_modes = ["admin_enter"]
396 self.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
397 "password", "chat", "study", "play",
403 self.parser = Parser(self.game)
405 self.do_refresh = True
406 self.queue = queue.Queue()
407 self.login_name = None
408 self.map_mode = 'terrain + things'
409 self.password = 'foo'
410 self.switch_mode('waiting_for_server')
412 'switch_to_chat': 't',
413 'switch_to_play': 'p',
414 'switch_to_password': 'P',
415 'switch_to_annotate': 'M',
416 'switch_to_portal': 'T',
417 'switch_to_study': '?',
418 'switch_to_edit': 'E',
419 'switch_to_write': 'm',
420 'switch_to_name_thing': 'N',
421 'switch_to_admin_enter': 'A',
422 'switch_to_control_pw_type': 'C',
423 'switch_to_control_tile_type': 'Q',
424 'switch_to_admin_thing_protect': 'T',
430 'toggle_map_mode': 'L',
431 'toggle_tile_draw': 'm',
432 'hex_move_upleft': 'w',
433 'hex_move_upright': 'e',
434 'hex_move_right': 'd',
435 'hex_move_downright': 'x',
436 'hex_move_downleft': 'y',
437 'hex_move_left': 'a',
438 'square_move_up': 'w',
439 'square_move_left': 'a',
440 'square_move_down': 's',
441 'square_move_right': 'd',
443 if os.path.isfile('config.json'):
444 with open('config.json', 'r') as f:
445 keys_conf = json.loads(f.read())
447 self.keys[k] = keys_conf[k]
448 self.show_help = False
449 self.disconnected = True
450 self.force_instant_connect = True
451 self.input_lines = []
454 curses.wrapper(self.loop)
458 def handle_recv(msg):
464 self.log_msg('@ attempting connect')
465 socket_client_class = PlomSocketClient
466 if self.host.startswith('ws://') or self.host.startswith('wss://'):
467 socket_client_class = WebSocketClient
469 self.socket = socket_client_class(handle_recv, self.host)
470 self.socket_thread = threading.Thread(target=self.socket.run)
471 self.socket_thread.start()
472 self.disconnected = False
473 self.game.thing_types = {}
474 self.game.terrains = {}
475 time.sleep(0.1) # give potential SSL negotation some time …
476 self.socket.send('TASKS')
477 self.socket.send('TERRAINS')
478 self.socket.send('THING_TYPES')
479 self.switch_mode('login')
480 except ConnectionRefusedError:
481 self.log_msg('@ server connect failure')
482 self.disconnected = True
483 self.switch_mode('waiting_for_server')
484 self.do_refresh = True
487 self.log_msg('@ attempting reconnect')
489 # necessitated by some strange SSL race conditions with ws4py
490 time.sleep(0.1) # FIXME find out why exactly necessary
491 self.switch_mode('waiting_for_server')
496 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
497 raise BrokenSocketConnection
498 self.socket.send(msg)
499 except (BrokenPipeError, BrokenSocketConnection):
500 self.log_msg('@ server disconnected :(')
501 self.disconnected = True
502 self.force_instant_connect = True
503 self.do_refresh = True
505 def log_msg(self, msg):
507 if len(self.log) > 100:
508 self.log = self.log[-100:]
510 def query_info(self):
511 self.send('GET_ANNOTATION ' + str(self.explorer))
513 def restore_input_values(self):
514 if self.mode.name == 'annotate' and self.explorer in self.game.info_db:
515 info = self.game.info_db[self.explorer]
518 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
519 self.input_ = self.game.portals[self.explorer]
520 elif self.mode.name == 'password':
521 self.input_ = self.password
522 elif self.mode.name == 'name_thing':
523 if hasattr(self.thing_selected, 'name'):
524 self.input_ = self.thing_selected.name
525 elif self.mode.name == 'admin_thing_protect':
526 if hasattr(self.thing_selected, 'protection'):
527 self.input_ = self.thing_selected.protection
529 def send_tile_control_command(self):
530 self.send('SET_TILE_CONTROL %s %s' %
531 (self.explorer, quote(self.tile_control_char)))
533 def toggle_map_mode(self):
534 if self.map_mode == 'terrain only':
535 self.map_mode = 'terrain + annotations'
536 elif self.map_mode == 'terrain + annotations':
537 self.map_mode = 'terrain + things'
538 elif self.map_mode == 'terrain + things':
539 self.map_mode = 'protections'
540 elif self.map_mode == 'protections':
541 self.map_mode = 'terrain only'
543 def switch_mode(self, mode_name):
544 self.tile_draw = False
545 if mode_name == 'admin_enter' and self.is_admin:
547 elif mode_name in {'name_thing', 'admin_thing_protect'}:
548 player = self.game.get_thing(self.game.player_id)
550 for t in [t for t in self.game.things if t.position == player.position
551 and t.id_ != player.id_]:
556 self.log_msg('? not standing over thing')
559 self.thing_selected = thing
560 self.mode = getattr(self, 'mode_' + mode_name)
561 if self.mode.name == 'control_tile_draw':
562 self.log_msg('@ finished tile protection drawing.')
563 if self.mode.name in {'control_tile_draw', 'control_tile_type',
565 self.map_mode = 'protections'
566 elif self.mode.name != 'edit':
567 self.map_mode = 'terrain + things'
568 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
569 player = self.game.get_thing(self.game.player_id)
570 self.explorer = YX(player.position.y, player.position.x)
571 if self.mode.shows_info:
573 if self.mode.is_single_char_entry:
574 self.show_help = True
575 if self.mode.name == 'waiting_for_server':
576 self.log_msg('@ waiting for server …')
577 elif self.mode.name == 'login':
579 self.send('LOGIN ' + quote(self.login_name))
581 self.log_msg('@ enter username')
582 elif self.mode.name == 'admin_enter':
583 self.log_msg('@ enter admin password:')
584 elif self.mode.name == 'control_pw_type':
585 self.log_msg('@ enter protection character for which you want to change the password:')
586 elif self.mode.name == 'control_tile_type':
587 self.log_msg('@ enter protection character which you want to draw:')
588 elif self.mode.name == 'admin_thing_protect':
589 self.log_msg('@ enter thing protection character:')
590 elif self.mode.name == 'control_pw_pw':
591 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
592 elif self.mode.name == 'control_tile_draw':
593 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']))
595 self.restore_input_values()
597 def loop(self, stdscr):
600 def safe_addstr(y, x, line):
601 if y < self.size.y - 1 or x + len(line) < self.size.x:
602 stdscr.addstr(y, x, line)
603 else: # workaround to <https://stackoverflow.com/q/7063128>
604 cut_i = self.size.x - x - 1
606 last_char = line[cut_i]
607 stdscr.addstr(y, self.size.x - 2, last_char)
608 stdscr.insstr(y, self.size.x - 2, ' ')
609 stdscr.addstr(y, x, cut)
611 def handle_input(msg):
612 command, args = self.parser.parse(msg)
615 def task_action_on(action):
616 return action_tasks[action] in self.game.tasks
618 def msg_into_lines_of_width(msg, width):
622 for i in range(len(msg)):
623 if x >= width or msg[i] == "\n":
635 def reset_screen_size():
636 self.size = YX(*stdscr.getmaxyx())
637 self.size = self.size - YX(self.size.y % 4, 0)
638 self.size = self.size - YX(0, self.size.x % 4)
639 self.window_width = int(self.size.x / 2)
641 def recalc_input_lines():
642 if not self.mode.has_input_prompt:
643 self.input_lines = []
645 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
648 def move_explorer(direction):
649 target = self.game.map_geometry.move_yx(self.explorer, direction)
651 self.explorer = target
652 if self.mode.shows_info:
655 self.send_tile_control_command()
661 for line in self.log:
662 lines += msg_into_lines_of_width(line, self.window_width)
665 max_y = self.size.y - len(self.input_lines)
666 for i in range(len(lines)):
667 if (i >= max_y - height_header):
669 safe_addstr(max_y - i - 1, self.window_width, lines[i])
672 if not self.game.turn_complete:
674 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
675 info = 'MAP VIEW: %s\n' % self.map_mode
676 if self.game.fov[pos_i] != '.':
677 info += 'outside field of view'
679 terrain_char = self.game.map_content[pos_i]
681 if terrain_char in self.game.terrains:
682 terrain_desc = self.game.terrains[terrain_char]
683 info += 'TERRAIN: "%s" / %s\n' % (terrain_char, terrain_desc)
684 protection = self.game.map_control_content[pos_i]
685 if protection == '.':
686 protection = 'unprotected'
687 info += 'PROTECTION: %s\n' % protection
688 for t in self.game.things:
689 if t.position == self.explorer:
690 protection = t.protection
691 if protection == '.':
692 protection = 'unprotected'
693 info += 'THING: %s / protection: %s / %s' %\
694 (t.type_, protection, self.game.thing_types[t.type_])
695 if hasattr(t, 'player_char'):
696 info += t.player_char
697 if hasattr(t, 'name'):
698 info += ' (%s)' % t.name
700 if self.explorer in self.game.portals:
701 info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
703 info += 'PORTAL: (none)\n'
704 if self.explorer in self.game.info_db:
705 info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
707 info += 'ANNOTATION: waiting …'
708 lines = msg_into_lines_of_width(info, self.window_width)
710 for i in range(len(lines)):
711 y = height_header + i
712 if y >= self.size.y - len(self.input_lines):
714 safe_addstr(y, self.window_width, lines[i])
717 y = self.size.y - len(self.input_lines)
718 for i in range(len(self.input_lines)):
719 safe_addstr(y, self.window_width, self.input_lines[i])
723 if not self.game.turn_complete:
725 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
728 help = "hit [%s] for help" % self.keys['help']
729 if self.mode.has_input_prompt:
730 help = "enter /help for help"
731 safe_addstr(1, self.window_width,
732 'MODE: %s – %s' % (self.mode.short_desc, help))
735 if not self.game.turn_complete:
738 for y in range(self.game.map_geometry.size.y):
739 start = self.game.map_geometry.size.x * y
740 end = start + self.game.map_geometry.size.x
741 if self.map_mode == 'protections':
742 map_lines_split += [[c + ' ' for c
743 in self.game.map_control_content[start:end]]]
745 map_lines_split += [[c + ' ' for c
746 in self.game.map_content[start:end]]]
747 if self.map_mode == 'terrain + annotations':
748 for p in self.game.info_hints:
749 map_lines_split[p.y][p.x] = 'A '
750 elif self.map_mode == 'terrain + things':
751 for p in self.game.portals.keys():
752 original = map_lines_split[p.y][p.x]
753 map_lines_split[p.y][p.x] = original[0] + 'P'
755 for t in self.game.things:
756 symbol = self.game.thing_types[t.type_]
758 if hasattr(t, 'player_char'):
759 meta_char = t.player_char
760 if t.position in used_positions:
762 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
763 used_positions += [t.position]
764 player = self.game.get_thing(self.game.player_id)
765 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
766 map_lines_split[self.explorer.y][self.explorer.x] = '??'
767 elif self.map_mode != 'terrain + things':
768 map_lines_split[player.position.y][player.position.x] = '??'
770 if type(self.game.map_geometry) == MapGeometryHex:
772 for line in map_lines_split:
773 map_lines += [indent * ' ' + ''.join(line)]
774 indent = 0 if indent else 1
776 for line in map_lines_split:
777 map_lines += [''.join(line)]
778 window_center = YX(int(self.size.y / 2),
779 int(self.window_width / 2))
780 center = player.position
781 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
782 center = self.explorer
783 center = YX(center.y, center.x * 2)
784 offset = center - window_center
785 if type(self.game.map_geometry) == MapGeometryHex and offset.y % 2:
787 term_y = max(0, -offset.y)
788 term_x = max(0, -offset.x)
789 map_y = max(0, offset.y)
790 map_x = max(0, offset.x)
791 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
792 to_draw = map_lines[map_y][map_x:self.window_width + offset.x]
793 safe_addstr(term_y, term_x, to_draw)
798 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
799 self.mode.help_intro)
800 if len(self.mode.available_actions) > 0:
801 content += "Available actions:\n"
802 for action in self.mode.available_actions:
803 if action in action_tasks:
804 if action_tasks[action] not in self.game.tasks:
806 if action == 'move_explorer':
809 key = ','.join(self.movement_keys)
811 key = self.keys[action]
812 content += '[%s] – %s\n' % (key, action_descriptions[action])
814 if self.mode.name == 'chat':
815 content += '/nick NAME – re-name yourself to NAME\n'
816 content += '/%s or /play – switch to play mode\n' % self.keys['switch_to_play']
817 content += '/%s or /study – switch to study mode\n' % self.keys['switch_to_study']
818 content += '/%s or /edit – switch to world edit mode\n' % self.keys['switch_to_edit']
819 content += '/%s or /admin – switch to admin mode\n' % self.keys['switch_to_admin_enter']
820 content += self.mode.list_available_modes(self)
821 for i in range(self.size.y):
823 self.window_width * (not self.mode.has_input_prompt),
824 ' ' * self.window_width)
826 for line in content.split('\n'):
827 lines += msg_into_lines_of_width(line, self.window_width)
828 for i in range(len(lines)):
832 self.window_width * (not self.mode.has_input_prompt),
838 if self.mode.has_input_prompt:
840 if self.mode.shows_info:
845 if not self.mode.is_intro:
851 action_descriptions = {
853 'flatten': 'flatten surroundings',
854 'teleport': 'teleport',
855 'take_thing': 'pick up thing',
856 'drop_thing': 'drop thing',
857 'toggle_map_mode': 'toggle map view',
858 'toggle_tile_draw': 'toggle protection character drawing',
862 'flatten': 'FLATTEN_SURROUNDINGS',
863 'take_thing': 'PICK_UP',
864 'drop_thing': 'DROP',
868 curses.curs_set(False) # hide cursor
869 curses.use_default_colors()
872 self.explorer = YX(0, 0)
875 interval = datetime.timedelta(seconds=5)
876 last_ping = datetime.datetime.now() - interval
878 if self.disconnected and self.force_instant_connect:
879 self.force_instant_connect = False
881 now = datetime.datetime.now()
882 if now - last_ping > interval:
883 if self.disconnected:
893 self.do_refresh = False
896 msg = self.queue.get(block=False)
901 key = stdscr.getkey()
902 self.do_refresh = True
905 self.show_help = False
906 if key == 'KEY_RESIZE':
908 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
909 self.input_ = self.input_[:-1]
910 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
911 self.show_help = True
913 self.restore_input_values()
914 elif self.mode.has_input_prompt and key != '\n': # Return key
916 max_length = self.window_width * self.size.y - len(input_prompt) - 1
917 if len(self.input_) > max_length:
918 self.input_ = self.input_[:max_length]
919 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
920 self.show_help = True
921 elif self.mode.name == 'login' and key == '\n':
922 self.login_name = self.input_
923 self.send('LOGIN ' + quote(self.input_))
925 elif self.mode.name == 'control_pw_pw' and key == '\n':
926 if self.input_ == '':
927 self.log_msg('@ aborted')
929 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
930 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
931 self.switch_mode('admin')
932 elif self.mode.name == 'password' and key == '\n':
933 if self.input_ == '':
935 self.password = self.input_
936 self.switch_mode('edit')
937 elif self.mode.name == 'admin_enter' and key == '\n':
938 self.send('BECOME_ADMIN ' + quote(self.input_))
939 self.switch_mode('play')
940 elif self.mode.name == 'control_pw_type' and key == '\n':
941 if len(self.input_) != 1:
942 self.log_msg('@ entered non-single-char, therefore aborted')
943 self.switch_mode('admin')
945 self.tile_control_char = self.input_
946 self.switch_mode('control_pw_pw')
947 elif self.mode.name == 'admin_thing_protect' and key == '\n':
948 if len(self.input_) != 1:
949 self.log_msg('@ entered non-single-char, therefore aborted')
951 self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
953 self.log_msg('@ sent new protection character for thing')
954 self.switch_mode('admin')
955 elif self.mode.name == 'control_tile_type' and key == '\n':
956 if len(self.input_) != 1:
957 self.log_msg('@ entered non-single-char, therefore aborted')
958 self.switch_mode('admin')
960 self.tile_control_char = self.input_
961 self.switch_mode('control_tile_draw')
962 elif self.mode.name == 'chat' and key == '\n':
963 if self.input_ == '':
965 if self.input_[0] == '/': # FIXME fails on empty input
966 if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
967 self.switch_mode('play')
968 elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
969 self.switch_mode('study')
970 elif self.input_ in {'/' + self.keys['switch_to_edit'], '/edit'}:
971 self.switch_mode('edit')
972 elif self.input_ in {'/' + self.keys['switch_to_admin_enter'], '/admin'}:
973 self.switch_mode('admin_enter')
974 elif self.input_.startswith('/nick'):
975 tokens = self.input_.split(maxsplit=1)
977 self.send('NICK ' + quote(tokens[1]))
979 self.log_msg('? need login name')
981 self.log_msg('? unknown command')
983 self.send('ALL ' + quote(self.input_))
985 elif self.mode.name == 'name_thing' and key == '\n':
986 if self.input_ == '':
988 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
990 quote(self.password)))
991 self.switch_mode('edit')
992 elif self.mode.name == 'annotate' and key == '\n':
993 if self.input_ == '':
995 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
996 quote(self.password)))
997 self.switch_mode('edit')
998 elif self.mode.name == 'portal' and key == '\n':
999 if self.input_ == '':
1001 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1002 quote(self.password)))
1003 self.switch_mode('edit')
1004 elif self.mode.name == 'study':
1005 if self.mode.mode_switch_on_key(self, key):
1007 elif key == self.keys['toggle_map_mode']:
1008 self.toggle_map_mode()
1009 elif key in self.movement_keys:
1010 move_explorer(self.movement_keys[key])
1011 elif self.mode.name == 'play':
1012 if self.mode.mode_switch_on_key(self, key):
1014 elif key == self.keys['take_thing'] and task_action_on('take_thing'):
1015 self.send('TASK:PICK_UP')
1016 elif key == self.keys['drop_thing'] and task_action_on('drop_thing'):
1017 self.send('TASK:DROP')
1018 elif key == self.keys['teleport']:
1019 player = self.game.get_thing(self.game.player_id)
1020 if player.position in self.game.portals:
1021 self.host = self.game.portals[player.position]
1025 self.log_msg('? not standing on portal')
1026 elif key in self.movement_keys and task_action_on('move'):# 'MOVE' in self.game.tasks:
1027 self.send('TASK:MOVE ' + self.movement_keys[key])
1028 elif self.mode.name == 'write':
1029 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1030 self.switch_mode('edit')
1031 elif self.mode.name == 'control_tile_draw':
1032 if self.mode.mode_switch_on_key(self, key):
1034 elif key in self.movement_keys:
1035 move_explorer(self.movement_keys[key])
1036 elif key == self.keys['toggle_tile_draw']:
1037 self.tile_draw = False if self.tile_draw else True
1038 elif self.mode.name == 'admin':
1039 if self.mode.mode_switch_on_key(self, key):
1041 elif key in self.movement_keys and task_action_on('move'):
1042 self.send('TASK:MOVE ' + self.movement_keys[key])
1043 elif self.mode.name == 'edit':
1044 if self.mode.mode_switch_on_key(self, key):
1046 elif key == self.keys['flatten'] and task_action_on('flatten'):
1047 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1048 elif key == self.keys['toggle_map_mode']:
1049 self.toggle_map_mode()
1050 elif key in self.movement_keys and task_action_on('move'):
1051 self.send('TASK:MOVE ' + self.movement_keys[key])
1053 if len(sys.argv) != 2:
1054 raise ArgError('wrong number of arguments, need game host')