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 map 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.'
31 'short': 'change terrain',
32 'long': 'This mode allows you to change the map tile you currently stand on (if your map editing password authorizes you so). Just enter any printable ASCII character to imprint it on the ground below you.'
35 'short': 'change protection character password',
36 'long': 'This mode is the first of two steps to change the password for a tile protection character. First enter the tile protection character for which you want to change the password.'
39 'short': 'change protection character password',
40 'long': 'This mode is the second of two steps to change the password for a tile protection character. Enter the new password for the tile protection character you chose.'
42 'control_tile_type': {
43 'short': 'change tiles protection',
44 'long': 'This mode is the first of two steps to change tile protection areas on the map. First enter the tile tile protection character you want to write.'
46 'control_tile_draw': {
47 'short': 'change tiles protection',
48 '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 tile protection character.'
51 'short': 'annotate tile',
52 'long': 'This mode allows you to add/edit a comment on the tile you are currently standing on (provided your map editing password authorizes you so). Hit Return to leave.'
55 'short': 'edit portal',
56 'long': 'This mode allows you to imprint/edit/remove a teleportation target on the ground you are currently standing on (provided your map 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.'
60 '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:'
64 'long': 'Enter your player name.'
66 'waiting_for_server': {
67 'short': 'waiting for server response',
68 'long': 'Waiting for a server response.'
71 'short': 'waiting for server response',
72 'long': 'Waiting for a server response.'
75 'short': 'set map edit password',
76 'long': 'This mode allows you to change the password that you send to authorize yourself for editing password-protected map tiles. Hit return to confirm and leave.'
79 'short': 'become admin',
80 'long': 'This mode allows you to become admin if you know an admin password.'
84 'long': 'This mode allows you access to actions limited to administrators.'
88 from ws4py.client import WebSocketBaseClient
89 class WebSocketClient(WebSocketBaseClient):
91 def __init__(self, recv_handler, *args, **kwargs):
92 super().__init__(*args, **kwargs)
93 self.recv_handler = recv_handler
96 def received_message(self, message):
98 message = str(message)
99 self.recv_handler(message)
102 def plom_closed(self):
103 return self.client_terminated
105 from plomrogue.io_tcp import PlomSocket
106 class PlomSocketClient(PlomSocket):
108 def __init__(self, recv_handler, url):
110 self.recv_handler = recv_handler
111 host, port = url.split(':')
112 super().__init__(socket.create_connection((host, port)))
120 for msg in self.recv():
121 if msg == 'NEED_SSL':
122 self.socket = ssl.wrap_socket(self.socket)
124 self.recv_handler(msg)
125 except BrokenSocketConnection:
126 pass # we assume socket will be known as dead by now
128 def cmd_TURN(game, n):
134 game.turn_complete = False
135 cmd_TURN.argtypes = 'int:nonneg'
137 def cmd_LOGIN_OK(game):
138 game.tui.switch_mode('post_login_wait')
139 game.tui.send('GET_GAMESTATE')
140 game.tui.log_msg('@ welcome')
141 cmd_LOGIN_OK.argtypes = ''
143 def cmd_ADMIN_OK(game):
144 game.tui.is_admin = True
145 game.tui.log_msg('@ you now have admin rights')
146 game.tui.switch_mode('admin')
147 game.tui.do_refresh = True
148 cmd_ADMIN_OK.argtypes = ''
150 def cmd_CHAT(game, msg):
151 game.tui.log_msg('# ' + msg)
152 game.tui.do_refresh = True
153 cmd_CHAT.argtypes = 'string'
155 def cmd_PLAYER_ID(game, player_id):
156 game.player_id = player_id
157 cmd_PLAYER_ID.argtypes = 'int:nonneg'
159 def cmd_THING(game, yx, thing_type, thing_id):
160 t = game.get_thing(thing_id)
162 t = ThingBase(game, thing_id)
166 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type int:nonneg'
168 def cmd_THING_NAME(game, thing_id, name):
169 t = game.get_thing(thing_id)
172 cmd_THING_NAME.argtypes = 'int:nonneg string'
174 def cmd_THING_CHAR(game, thing_id, c):
175 t = game.get_thing(thing_id)
178 cmd_THING_CHAR.argtypes = 'int:nonneg char'
180 def cmd_MAP(game, geometry, size, content):
181 map_geometry_class = globals()['MapGeometry' + geometry]
182 game.map_geometry = map_geometry_class(size)
183 game.map_content = content
184 if type(game.map_geometry) == MapGeometrySquare:
185 game.tui.movement_keys = {
186 game.tui.keys['square_move_up']: 'UP',
187 game.tui.keys['square_move_left']: 'LEFT',
188 game.tui.keys['square_move_down']: 'DOWN',
189 game.tui.keys['square_move_right']: 'RIGHT',
191 elif type(game.map_geometry) == MapGeometryHex:
192 game.tui.movement_keys = {
193 game.tui.keys['hex_move_upleft']: 'UPLEFT',
194 game.tui.keys['hex_move_upright']: 'UPRIGHT',
195 game.tui.keys['hex_move_right']: 'RIGHT',
196 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
197 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
198 game.tui.keys['hex_move_left']: 'LEFT',
200 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
202 def cmd_FOV(game, content):
204 cmd_FOV.argtypes = 'string'
206 def cmd_MAP_CONTROL(game, content):
207 game.map_control_content = content
208 cmd_MAP_CONTROL.argtypes = 'string'
210 def cmd_GAME_STATE_COMPLETE(game):
211 if game.tui.mode.name == 'post_login_wait':
212 game.tui.switch_mode('play')
213 if game.tui.mode.shows_info:
214 game.tui.query_info()
215 game.turn_complete = True
216 game.tui.do_refresh = True
217 cmd_GAME_STATE_COMPLETE.argtypes = ''
219 def cmd_PORTAL(game, position, msg):
220 game.portals[position] = msg
221 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
223 def cmd_PLAY_ERROR(game, msg):
224 game.tui.log_msg('? ' + msg)
225 game.tui.flash = True
226 game.tui.do_refresh = True
227 cmd_PLAY_ERROR.argtypes = 'string'
229 def cmd_GAME_ERROR(game, msg):
230 game.tui.log_msg('? game error: ' + msg)
231 game.tui.do_refresh = True
232 cmd_GAME_ERROR.argtypes = 'string'
234 def cmd_ARGUMENT_ERROR(game, msg):
235 game.tui.log_msg('? syntax error: ' + msg)
236 game.tui.do_refresh = True
237 cmd_ARGUMENT_ERROR.argtypes = 'string'
239 def cmd_ANNOTATION_HINT(game, position):
240 game.info_hints += [position]
241 cmd_ANNOTATION_HINT.argtypes = 'yx_tuple:nonneg'
243 def cmd_ANNOTATION(game, position, msg):
244 game.info_db[position] = msg
245 if game.tui.mode.shows_info:
246 game.tui.do_refresh = True
247 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
249 def cmd_TASKS(game, tasks_comma_separated):
250 game.tasks = tasks_comma_separated.split(',')
251 game.tui.mode_write.legal = 'WRITE' in game.tasks
252 cmd_TASKS.argtypes = 'string'
254 def cmd_THING_TYPE(game, thing_type, symbol_hint):
255 game.thing_types[thing_type] = symbol_hint
256 cmd_THING_TYPE.argtypes = 'string char'
258 def cmd_TERRAIN(game, terrain_char, terrain_desc):
259 game.terrains[terrain_char] = terrain_desc
260 cmd_TERRAIN.argtypes = 'char string'
264 cmd_PONG.argtypes = ''
266 class Game(GameBase):
267 turn_complete = False
271 def __init__(self, *args, **kwargs):
272 super().__init__(*args, **kwargs)
273 self.register_command(cmd_LOGIN_OK)
274 self.register_command(cmd_ADMIN_OK)
275 self.register_command(cmd_PONG)
276 self.register_command(cmd_CHAT)
277 self.register_command(cmd_PLAYER_ID)
278 self.register_command(cmd_TURN)
279 self.register_command(cmd_THING)
280 self.register_command(cmd_THING_TYPE)
281 self.register_command(cmd_THING_NAME)
282 self.register_command(cmd_THING_CHAR)
283 self.register_command(cmd_TERRAIN)
284 self.register_command(cmd_MAP)
285 self.register_command(cmd_MAP_CONTROL)
286 self.register_command(cmd_PORTAL)
287 self.register_command(cmd_ANNOTATION)
288 self.register_command(cmd_ANNOTATION_HINT)
289 self.register_command(cmd_GAME_STATE_COMPLETE)
290 self.register_command(cmd_ARGUMENT_ERROR)
291 self.register_command(cmd_GAME_ERROR)
292 self.register_command(cmd_PLAY_ERROR)
293 self.register_command(cmd_TASKS)
294 self.register_command(cmd_FOV)
295 self.map_content = ''
302 def get_string_options(self, string_option_type):
303 if string_option_type == 'map_geometry':
304 return ['Hex', 'Square']
305 elif string_option_type == 'thing_type':
306 return self.thing_types.keys()
309 def get_command(self, command_name):
310 from functools import partial
311 f = partial(self.commands[command_name], self)
312 f.argtypes = self.commands[command_name].argtypes
317 def __init__(self, name, has_input_prompt=False, shows_info=False,
318 is_intro=False, is_single_char_entry=False):
320 self.short_desc = mode_helps[name]['short']
321 self.available_modes = []
322 self.has_input_prompt = has_input_prompt
323 self.shows_info = shows_info
324 self.is_intro = is_intro
325 self.help_intro = mode_helps[name]['long']
326 self.is_single_char_entry = is_single_char_entry
329 def iter_available_modes(self, tui):
330 for mode_name in self.available_modes:
331 mode = getattr(tui, 'mode_' + mode_name)
334 key = tui.keys['switch_to_' + mode.name]
337 def list_available_modes(self, tui):
339 if len(self.available_modes) > 0:
340 msg = 'Other modes available from here:\n'
341 for mode, key in self.iter_available_modes(tui):
342 msg += '[%s] – %s\n' % (key, mode.short_desc)
345 def mode_switch_on_key(self, tui, key_pressed):
346 for mode, key in self.iter_available_modes(tui):
347 if key_pressed == key:
348 tui.switch_mode(mode.name)
353 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
354 mode_admin = Mode('admin')
355 mode_play = Mode('play')
356 mode_study = Mode('study', shows_info=True)
357 mode_write = Mode('write', is_single_char_entry=True)
358 mode_edit = Mode('edit')
359 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
360 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
361 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
362 mode_control_tile_draw = Mode('control_tile_draw')
363 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
364 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
365 mode_chat = Mode('chat', has_input_prompt=True)
366 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
367 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
368 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
369 mode_password = Mode('password', has_input_prompt=True)
370 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
374 def __init__(self, host):
377 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter"]
378 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
379 self.mode_admin.available_modes = ["control_pw_type",
380 "control_tile_type", "chat",
381 "study", "play", "edit"]
382 self.mode_control_tile_draw.available_modes = ["admin_enter"]
383 self.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
384 "password", "chat", "study", "play",
390 self.parser = Parser(self.game)
392 self.do_refresh = True
393 self.queue = queue.Queue()
394 self.login_name = None
395 self.map_mode = 'terrain + things'
396 self.password = 'foo'
397 self.switch_mode('waiting_for_server')
399 'switch_to_chat': 't',
400 'switch_to_play': 'p',
401 'switch_to_password': 'P',
402 'switch_to_annotate': 'M',
403 'switch_to_portal': 'T',
404 'switch_to_study': '?',
405 'switch_to_edit': 'E',
406 'switch_to_write': 'm',
407 'switch_to_name_thing': 'N',
408 'switch_to_admin_enter': 'A',
409 'switch_to_control_pw_type': 'C',
410 'switch_to_control_tile_type': 'Q',
416 'toggle_map_mode': 'L',
417 'toggle_tile_draw': 'm',
418 'hex_move_upleft': 'w',
419 'hex_move_upright': 'e',
420 'hex_move_right': 'd',
421 'hex_move_downright': 'x',
422 'hex_move_downleft': 'y',
423 'hex_move_left': 'a',
424 'square_move_up': 'w',
425 'square_move_left': 'a',
426 'square_move_down': 's',
427 'square_move_right': 'd',
429 if os.path.isfile('config.json'):
430 with open('config.json', 'r') as f:
431 keys_conf = json.loads(f.read())
433 self.keys[k] = keys_conf[k]
434 self.show_help = False
435 self.disconnected = True
436 self.force_instant_connect = True
437 self.input_lines = []
440 curses.wrapper(self.loop)
444 def handle_recv(msg):
450 self.log_msg('@ attempting connect')
451 socket_client_class = PlomSocketClient
452 if self.host.startswith('ws://') or self.host.startswith('wss://'):
453 socket_client_class = WebSocketClient
455 self.socket = socket_client_class(handle_recv, self.host)
456 self.socket_thread = threading.Thread(target=self.socket.run)
457 self.socket_thread.start()
458 self.disconnected = False
459 self.game.thing_types = {}
460 self.game.terrains = {}
461 time.sleep(0.1) # give potential SSL negotation some time …
462 self.socket.send('TASKS')
463 self.socket.send('TERRAINS')
464 self.socket.send('THING_TYPES')
465 self.switch_mode('login')
466 except ConnectionRefusedError:
467 self.log_msg('@ server connect failure')
468 self.disconnected = True
469 self.switch_mode('waiting_for_server')
470 self.do_refresh = True
473 self.log_msg('@ attempting reconnect')
475 # necessitated by some strange SSL race conditions with ws4py
476 time.sleep(0.1) # FIXME find out why exactly necessary
477 self.switch_mode('waiting_for_server')
482 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
483 raise BrokenSocketConnection
484 self.socket.send(msg)
485 except (BrokenPipeError, BrokenSocketConnection):
486 self.log_msg('@ server disconnected :(')
487 self.disconnected = True
488 self.force_instant_connect = True
489 self.do_refresh = True
491 def log_msg(self, msg):
493 if len(self.log) > 100:
494 self.log = self.log[-100:]
496 def query_info(self):
497 self.send('GET_ANNOTATION ' + str(self.explorer))
499 def restore_input_values(self):
500 if self.mode.name == 'annotate' and self.explorer in self.game.info_db:
501 info = self.game.info_db[self.explorer]
504 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
505 self.input_ = self.game.portals[self.explorer]
506 elif self.mode.name == 'password':
507 self.input_ = self.password
508 elif self.mode.name == 'name_thing':
509 if hasattr(self.thing_selected, 'name'):
510 self.input_ = self.thing_selected.name
512 def send_tile_control_command(self):
513 self.send('SET_TILE_CONTROL %s %s' %
514 (self.explorer, quote(self.tile_control_char)))
516 def toggle_map_mode(self):
517 if self.map_mode == 'terrain only':
518 self.map_mode = 'terrain + annotations'
519 elif self.map_mode == 'terrain + annotations':
520 self.map_mode = 'terrain + things'
521 elif self.map_mode == 'terrain + things':
522 self.map_mode = 'protections'
523 elif self.map_mode == 'protections':
524 self.map_mode = 'terrain only'
526 def switch_mode(self, mode_name):
527 self.tile_draw = False
528 if mode_name == 'admin_enter' and self.is_admin:
530 elif mode_name == 'name_thing':
531 player = self.game.get_thing(self.game.player_id)
533 for t in [t for t in self.game.things if t.position == player.position
534 and t.id_ != player.id_]:
539 self.log_msg('? not standing over thing')
542 self.thing_selected = thing
543 self.mode = getattr(self, 'mode_' + mode_name)
544 if self.mode.name == 'control_tile_draw':
545 self.log_msg('@ finished tile protection drawing.')
546 if self.mode.name in {'control_tile_draw', 'control_tile_type',
548 self.map_mode = 'protections'
549 elif self.mode.name != 'edit':
550 self.map_mode = 'terrain + things'
551 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
552 player = self.game.get_thing(self.game.player_id)
553 self.explorer = YX(player.position.y, player.position.x)
554 if self.mode.shows_info:
556 if self.mode.is_single_char_entry:
557 self.show_help = True
558 if self.mode.name == 'waiting_for_server':
559 self.log_msg('@ waiting for server …')
560 elif self.mode.name == 'login':
562 self.send('LOGIN ' + quote(self.login_name))
564 self.log_msg('@ enter username')
565 elif self.mode.name == 'admin_enter':
566 self.log_msg('@ enter admin password:')
567 elif self.mode.name == 'control_pw_type':
568 self.log_msg('@ enter tile protection character for which you want to change the password:')
569 elif self.mode.name == 'control_tile_type':
570 self.log_msg('@ enter tile protection character which you want to draw:')
571 elif self.mode.name == 'control_pw_pw':
572 self.log_msg('@ enter tile protection password for "%s":' % self.tile_control_char)
573 elif self.mode.name == 'control_tile_draw':
574 self.log_msg('@ can draw tile 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']))
576 self.restore_input_values()
578 def loop(self, stdscr):
581 def safe_addstr(y, x, line):
582 if y < self.size.y - 1 or x + len(line) < self.size.x:
583 stdscr.addstr(y, x, line)
584 else: # workaround to <https://stackoverflow.com/q/7063128>
585 cut_i = self.size.x - x - 1
587 last_char = line[cut_i]
588 stdscr.addstr(y, self.size.x - 2, last_char)
589 stdscr.insstr(y, self.size.x - 2, ' ')
590 stdscr.addstr(y, x, cut)
592 def handle_input(msg):
593 command, args = self.parser.parse(msg)
596 def msg_into_lines_of_width(msg, width):
600 for i in range(len(msg)):
601 if x >= width or msg[i] == "\n":
613 def reset_screen_size():
614 self.size = YX(*stdscr.getmaxyx())
615 self.size = self.size - YX(self.size.y % 4, 0)
616 self.size = self.size - YX(0, self.size.x % 4)
617 self.window_width = int(self.size.x / 2)
619 def recalc_input_lines():
620 if not self.mode.has_input_prompt:
621 self.input_lines = []
623 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
626 def move_explorer(direction):
627 target = self.game.map_geometry.move_yx(self.explorer, direction)
629 self.explorer = target
630 if self.mode.shows_info:
633 self.send_tile_control_command()
639 for line in self.log:
640 lines += msg_into_lines_of_width(line, self.window_width)
643 max_y = self.size.y - len(self.input_lines)
644 for i in range(len(lines)):
645 if (i >= max_y - height_header):
647 safe_addstr(max_y - i - 1, self.window_width, lines[i])
650 if not self.game.turn_complete:
652 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
653 info = 'MAP VIEW: %s\n' % self.map_mode
654 if self.game.fov[pos_i] != '.':
655 info += 'outside field of view'
657 terrain_char = self.game.map_content[pos_i]
659 if terrain_char in self.game.terrains:
660 terrain_desc = self.game.terrains[terrain_char]
661 info += 'TERRAIN: "%s" / %s\n' % (terrain_char, terrain_desc)
662 protection = self.game.map_control_content[pos_i]
663 if protection == '.':
664 protection = 'unprotected'
665 info += 'PROTECTION: %s\n' % protection
666 for t in self.game.things:
667 if t.position == self.explorer:
668 info += 'THING: %s / %s' % (t.type_,
669 self.game.thing_types[t.type_])
670 if hasattr(t, 'player_char'):
671 info += t.player_char
672 if hasattr(t, 'name'):
673 info += ' (%s)' % t.name
675 if self.explorer in self.game.portals:
676 info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
678 info += 'PORTAL: (none)\n'
679 if self.explorer in self.game.info_db:
680 info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
682 info += 'ANNOTATION: waiting …'
683 lines = msg_into_lines_of_width(info, self.window_width)
685 for i in range(len(lines)):
686 y = height_header + i
687 if y >= self.size.y - len(self.input_lines):
689 safe_addstr(y, self.window_width, lines[i])
692 y = self.size.y - len(self.input_lines)
693 for i in range(len(self.input_lines)):
694 safe_addstr(y, self.window_width, self.input_lines[i])
698 if not self.game.turn_complete:
700 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
703 help = "hit [%s] for help" % self.keys['help']
704 if self.mode.has_input_prompt:
705 help = "enter /help for help"
706 safe_addstr(1, self.window_width,
707 'MODE: %s – %s' % (self.mode.short_desc, help))
710 if not self.game.turn_complete:
713 for y in range(self.game.map_geometry.size.y):
714 start = self.game.map_geometry.size.x * y
715 end = start + self.game.map_geometry.size.x
716 if self.map_mode == 'protections':
717 map_lines_split += [[c + ' ' for c
718 in self.game.map_control_content[start:end]]]
720 map_lines_split += [[c + ' ' for c
721 in self.game.map_content[start:end]]]
722 if self.map_mode == 'terrain + annotations':
723 for p in self.game.info_hints:
724 map_lines_split[p.y][p.x] = 'A '
725 elif self.map_mode == 'terrain + things':
726 for p in self.game.portals.keys():
727 original = map_lines_split[p.y][p.x]
728 map_lines_split[p.y][p.x] = original[0] + 'P'
730 for t in self.game.things:
731 symbol = self.game.thing_types[t.type_]
733 if hasattr(t, 'player_char'):
734 meta_char = t.player_char
735 if t.position in used_positions:
737 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
738 used_positions += [t.position]
739 player = self.game.get_thing(self.game.player_id)
740 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
741 map_lines_split[self.explorer.y][self.explorer.x] = '??'
742 elif self.map_mode != 'terrain + things':
743 map_lines_split[player.position.y][player.position.x] = '??'
745 if type(self.game.map_geometry) == MapGeometryHex:
747 for line in map_lines_split:
748 map_lines += [indent * ' ' + ''.join(line)]
749 indent = 0 if indent else 1
751 for line in map_lines_split:
752 map_lines += [''.join(line)]
753 window_center = YX(int(self.size.y / 2),
754 int(self.window_width / 2))
755 center = player.position
756 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
757 center = self.explorer
758 center = YX(center.y, center.x * 2)
759 offset = center - window_center
760 if type(self.game.map_geometry) == MapGeometryHex and offset.y % 2:
762 term_y = max(0, -offset.y)
763 term_x = max(0, -offset.x)
764 map_y = max(0, offset.y)
765 map_x = max(0, offset.x)
766 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
767 to_draw = map_lines[map_y][map_x:self.window_width + offset.x]
768 safe_addstr(term_y, term_x, to_draw)
773 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
774 self.mode.help_intro)
775 if self.mode.name == 'play':
776 content += "Available actions:\n"
777 if 'MOVE' in self.game.tasks:
778 content += "[%s] – move player\n" % ','.join(self.movement_keys)
779 if 'PICK_UP' in self.game.tasks:
780 content += "[%s] – pick up thing\n" % self.keys['take_thing']
781 if 'DROP' in self.game.tasks:
782 content += "[%s] – drop thing\n" % self.keys['drop_thing']
783 content += '[%s] – teleport\n' % self.keys['teleport']
785 elif self.mode.name == 'study':
786 content += 'Available actions:\n'
787 content += '[%s] – move question mark\n' % ','.join(self.movement_keys)
788 content += '[%s] – toggle map view\n' % self.keys['toggle_map_mode']
790 elif self.mode.name == 'edit':
791 content += "Available actions:\n"
792 if 'MOVE' in self.game.tasks:
793 content += "[%s] – move player\n" % ','.join(self.movement_keys)
794 if 'FLATTEN_SURROUNDINGS' in self.game.tasks:
795 content += "[%s] – flatten surroundings\n" % self.keys['flatten']
796 content += '[%s] – toggle map view\n' % self.keys['toggle_map_mode']
798 elif self.mode.name == 'control_tile_draw':
799 content += "Available actions:\n"
800 content += "[%s] – toggle tile protection drawing\n" % self.keys['toggle_tile_draw']
802 elif self.mode.name == 'chat':
803 content += '/nick NAME – re-name yourself to NAME\n'
804 content += '/%s or /play – switch to play mode\n' % self.keys['switch_to_play']
805 content += '/%s or /study – switch to study mode\n' % self.keys['switch_to_study']
806 content += '/%s or /edit – switch to map edit mode\n' % self.keys['switch_to_edit']
807 content += '/%s or /admin – switch to admin mode\n' % self.keys['switch_to_admin_enter']
808 content += self.mode.list_available_modes(self)
809 for i in range(self.size.y):
811 self.window_width * (not self.mode.has_input_prompt),
812 ' ' * self.window_width)
814 for line in content.split('\n'):
815 lines += msg_into_lines_of_width(line, self.window_width)
816 for i in range(len(lines)):
820 self.window_width * (not self.mode.has_input_prompt),
826 if self.mode.has_input_prompt:
828 if self.mode.shows_info:
833 if not self.mode.is_intro:
839 curses.curs_set(False) # hide cursor
840 curses.use_default_colors()
843 self.explorer = YX(0, 0)
846 interval = datetime.timedelta(seconds=5)
847 last_ping = datetime.datetime.now() - interval
849 if self.disconnected and self.force_instant_connect:
850 self.force_instant_connect = False
852 now = datetime.datetime.now()
853 if now - last_ping > interval:
854 if self.disconnected:
864 self.do_refresh = False
867 msg = self.queue.get(block=False)
872 key = stdscr.getkey()
873 self.do_refresh = True
876 self.show_help = False
877 if key == 'KEY_RESIZE':
879 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
880 self.input_ = self.input_[:-1]
881 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
882 self.show_help = True
884 self.restore_input_values()
885 elif self.mode.has_input_prompt and key != '\n': # Return key
887 max_length = self.window_width * self.size.y - len(input_prompt) - 1
888 if len(self.input_) > max_length:
889 self.input_ = self.input_[:max_length]
890 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
891 self.show_help = True
892 elif self.mode.name == 'login' and key == '\n':
893 self.login_name = self.input_
894 self.send('LOGIN ' + quote(self.input_))
896 elif self.mode.name == 'control_pw_pw' and key == '\n':
897 if self.input_ == '':
898 self.log_msg('@ aborted')
900 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
901 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
902 self.switch_mode('admin')
903 elif self.mode.name == 'password' and key == '\n':
904 if self.input_ == '':
906 self.password = self.input_
907 self.switch_mode('edit')
908 elif self.mode.name == 'admin_enter' and key == '\n':
909 self.send('BECOME_ADMIN ' + quote(self.input_))
910 self.switch_mode('play')
911 elif self.mode.name == 'control_pw_type' and key == '\n':
912 if len(self.input_) != 1:
913 self.log_msg('@ entered non-single-char, therefore aborted')
914 self.switch_mode('admin')
916 self.tile_control_char = self.input_
917 self.switch_mode('control_pw_pw')
918 elif self.mode.name == 'control_tile_type' and key == '\n':
919 if len(self.input_) != 1:
920 self.log_msg('@ entered non-single-char, therefore aborted')
921 self.switch_mode('admin')
923 self.tile_control_char = self.input_
924 self.switch_mode('control_tile_draw')
925 elif self.mode.name == 'chat' and key == '\n':
926 if self.input_ == '':
928 if self.input_[0] == '/': # FIXME fails on empty input
929 if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
930 self.switch_mode('play')
931 elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
932 self.switch_mode('study')
933 elif self.input_ in {'/' + self.keys['switch_to_edit'], '/edit'}:
934 self.switch_mode('edit')
935 elif self.input_ in {'/' + self.keys['switch_to_admin_enter'], '/admin'}:
936 self.switch_mode('admin_enter')
937 elif self.input_.startswith('/nick'):
938 tokens = self.input_.split(maxsplit=1)
940 self.send('NICK ' + quote(tokens[1]))
942 self.log_msg('? need login name')
944 self.log_msg('? unknown command')
946 self.send('ALL ' + quote(self.input_))
948 elif self.mode.name == 'name_thing' and key == '\n':
949 if self.input_ == '':
951 self.send('THING_NAME %s %s' % (self.thing_selected.id_,
953 self.switch_mode('edit')
954 elif self.mode.name == 'annotate' and key == '\n':
955 if self.input_ == '':
957 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
958 quote(self.password)))
959 self.switch_mode('edit')
960 elif self.mode.name == 'portal' and key == '\n':
961 if self.input_ == '':
963 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
964 quote(self.password)))
965 self.switch_mode('edit')
966 elif self.mode.name == 'study':
967 if self.mode.mode_switch_on_key(self, key):
969 elif key == self.keys['toggle_map_mode']:
970 self.toggle_map_mode()
971 elif key in self.movement_keys:
972 move_explorer(self.movement_keys[key])
973 elif self.mode.name == 'play':
974 if self.mode.mode_switch_on_key(self, key):
976 elif key == self.keys['take_thing'] and 'PICK_UP' in self.game.tasks:
977 self.send('TASK:PICK_UP')
978 elif key == self.keys['drop_thing'] and 'DROP' in self.game.tasks:
979 self.send('TASK:DROP')
980 elif key == self.keys['teleport']:
981 player = self.game.get_thing(self.game.player_id)
982 if player.position in self.game.portals:
983 self.host = self.game.portals[player.position]
987 self.log_msg('? not standing on portal')
988 elif key in self.movement_keys and 'MOVE' in self.game.tasks:
989 self.send('TASK:MOVE ' + self.movement_keys[key])
990 elif self.mode.name == 'write':
991 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
992 self.switch_mode('edit')
993 elif self.mode.name == 'control_tile_draw':
994 if self.mode.mode_switch_on_key(self, key):
996 elif key in self.movement_keys:
997 move_explorer(self.movement_keys[key])
998 elif key == self.keys['toggle_tile_draw']:
999 self.tile_draw = False if self.tile_draw else True
1000 elif self.mode.name == 'admin':
1001 if self.mode.mode_switch_on_key(self, key):
1003 elif self.mode.name == 'edit':
1004 if self.mode.mode_switch_on_key(self, key):
1006 elif key == self.keys['flatten'] and\
1007 'FLATTEN_SURROUNDINGS' in self.game.tasks:
1008 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1009 elif key == self.keys['toggle_map_mode']:
1010 self.toggle_map_mode()
1011 elif key in self.movement_keys and 'MOVE' in self.game.tasks:
1012 self.send('TASK:MOVE ' + self.movement_keys[key])
1014 if len(sys.argv) != 2:
1015 raise ArgError('wrong number of arguments, need game host')