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.'
37 'long': 'Enter a command to the thing you carry. Enter nothing to return to play mode.'
41 'intro': 'Pick up a thing in reach by entering its index number. Enter nothing to abort.',
42 'long': 'You see a list of things which you could pick up. Enter the target thing\'s index, or, to leave, nothing.'
46 'intro': 'Enter number of direction to which you want to drop thing.',
47 'long': 'Drop currently carried thing by entering the target direction index. Enter nothing to return to play mode..'
49 'admin_thing_protect': {
50 'short': 'change thing protection',
51 'intro': '@ enter thing protection character:',
52 'long': 'Change protection character for thing here.'
56 'intro': '@ enter face line (enter nothing to abort):',
57 'long': 'Draw your face as ASCII art. The string you enter must be 18 characters long, and will be divided on display into 3 lines of 6 characters each, from top to bottom..'
61 'intro': '@ enter hat line (enter nothing to abort):',
62 'long': 'Draw your face as ASCII art. The string you enter must be 18 characters long, and will be divided on display into 3 lines of 6 characters each, from top to bottom. Eat cookies to extend the ASCII characters available for drawing.'
67 '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.'
70 'short': 'change protection character password',
71 'intro': '@ enter protection character for which you want to change the password:',
72 '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.'
75 'short': 'change protection character password',
77 '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.'
79 'control_tile_type': {
80 'short': 'change tiles protection',
81 'intro': '@ enter protection character which you want to draw:',
82 '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.'
84 'control_tile_draw': {
85 'short': 'change tiles protection',
87 '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.'
90 'short': 'annotate tile',
92 '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.'
95 'short': 'edit portal',
97 '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.'
102 'long': 'This mode allows you to engage in chit-chat with other users. Any line you enter into the input prompt that does not start with a "/" will be sent out to nearby players – but barriers and distance will reduce what they can read, so stand close to them to ensure they get your message. Lines that start with a "/" are used for commands like:\n\n/nick NAME – re-name yourself to NAME'
107 'long': 'Enter your player name.'
109 'waiting_for_server': {
110 'short': 'waiting for server response',
111 'intro': '@ waiting for server …',
112 'long': 'Waiting for a server response.'
115 'short': 'waiting for server response',
117 'long': 'Waiting for a server response.'
120 'short': 'set world edit password',
122 '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.'
125 'short': 'become admin',
126 'intro': '@ enter admin password:',
127 'long': 'This mode allows you to become admin if you know an admin password.'
132 'long': 'This mode allows you access to actions limited to administrators.'
136 from ws4py.client import WebSocketBaseClient
137 class WebSocketClient(WebSocketBaseClient):
139 def __init__(self, recv_handler, *args, **kwargs):
140 super().__init__(*args, **kwargs)
141 self.recv_handler = recv_handler
144 def received_message(self, message):
146 message = str(message)
147 self.recv_handler(message)
150 def plom_closed(self):
151 return self.client_terminated
153 from plomrogue.io_tcp import PlomSocket
154 class PlomSocketClient(PlomSocket):
156 def __init__(self, recv_handler, url):
158 self.recv_handler = recv_handler
159 host, port = url.split(':')
160 super().__init__(socket.create_connection((host, port)))
168 for msg in self.recv():
169 if msg == 'NEED_SSL':
170 self.socket = ssl.wrap_socket(self.socket)
172 self.recv_handler(msg)
173 except BrokenSocketConnection:
174 pass # we assume socket will be known as dead by now
176 def cmd_TURN(game, n):
178 game.turn_complete = False
179 cmd_TURN.argtypes = 'int:nonneg'
181 def cmd_OTHER_WIPE(game):
182 game.portals_new = {}
183 game.annotations_new = {}
185 cmd_OTHER_WIPE.argtypes = ''
187 def cmd_LOGIN_OK(game):
188 game.tui.switch_mode('post_login_wait')
189 game.tui.send('GET_GAMESTATE')
190 game.tui.log_msg('@ welcome')
191 cmd_LOGIN_OK.argtypes = ''
193 def cmd_ADMIN_OK(game):
194 game.tui.is_admin = True
195 game.tui.log_msg('@ you now have admin rights')
196 game.tui.switch_mode('admin')
197 game.tui.do_refresh = True
198 cmd_ADMIN_OK.argtypes = ''
200 def cmd_REPLY(game, msg):
201 game.tui.log_msg('#MUSICPLAYER: ' + msg)
202 game.tui.do_refresh = True
203 cmd_REPLY.argtypes = 'string'
205 def cmd_CHAT(game, msg):
206 game.tui.log_msg('# ' + msg)
207 game.tui.do_refresh = True
208 cmd_CHAT.argtypes = 'string'
210 def cmd_CHATFACE(game, thing_id):
211 game.tui.draw_face = thing_id
212 game.tui.do_refresh = True
213 cmd_CHATFACE.argtypes = 'int:pos'
215 def cmd_PLAYER_ID(game, player_id):
216 game.player_id = player_id
217 cmd_PLAYER_ID.argtypes = 'int:nonneg'
219 def cmd_PLAYERS_HAT_CHARS(game, hat_chars):
220 game.players_hat_chars_new = hat_chars
221 cmd_PLAYERS_HAT_CHARS.argtypes = 'string'
223 def cmd_THING(game, yx, thing_type, protection, thing_id, portable, commandable):
224 t = game.get_thing_temp(thing_id)
226 t = ThingBase(game, thing_id)
227 game.things_new += [t]
230 t.protection = protection
231 t.portable = portable
232 t.commandable = commandable
233 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg bool bool'
235 def cmd_THING_NAME(game, thing_id, name):
236 t = game.get_thing_temp(thing_id)
238 cmd_THING_NAME.argtypes = 'int:pos string'
240 def cmd_THING_FACE(game, thing_id, face):
241 t = game.get_thing_temp(thing_id)
243 cmd_THING_FACE.argtypes = 'int:pos string'
245 def cmd_THING_HAT(game, thing_id, hat):
246 t = game.get_thing_temp(thing_id)
248 cmd_THING_HAT.argtypes = 'int:pos string'
250 def cmd_THING_CHAR(game, thing_id, c):
251 t = game.get_thing_temp(thing_id)
253 cmd_THING_CHAR.argtypes = 'int:pos char'
255 def cmd_MAP(game, geometry, size, content):
256 map_geometry_class = globals()['MapGeometry' + geometry]
257 game.map_geometry_new = map_geometry_class(size)
258 game.map_content_new = content
259 if type(game.map_geometry) == MapGeometrySquare:
260 game.tui.movement_keys = {
261 game.tui.keys['square_move_up']: 'UP',
262 game.tui.keys['square_move_left']: 'LEFT',
263 game.tui.keys['square_move_down']: 'DOWN',
264 game.tui.keys['square_move_right']: 'RIGHT',
266 elif type(game.map_geometry) == MapGeometryHex:
267 game.tui.movement_keys = {
268 game.tui.keys['hex_move_upleft']: 'UPLEFT',
269 game.tui.keys['hex_move_upright']: 'UPRIGHT',
270 game.tui.keys['hex_move_right']: 'RIGHT',
271 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
272 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
273 game.tui.keys['hex_move_left']: 'LEFT',
275 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
277 def cmd_FOV(game, content):
278 game.fov_new = content
279 cmd_FOV.argtypes = 'string'
281 def cmd_MAP_CONTROL(game, content):
282 game.map_control_content_new = content
283 cmd_MAP_CONTROL.argtypes = 'string'
285 def cmd_GAME_STATE_COMPLETE(game):
286 game.tui.do_refresh = True
287 game.tui.info_cached = None
288 game.things = game.things_new
289 game.portals = game.portals_new
290 game.annotations = game.annotations_new
291 game.fov = game.fov_new
292 game.map_geometry = game.map_geometry_new
293 game.map_content = game.map_content_new
294 game.map_control_content = game.map_control_content_new
295 game.player = game.get_thing(game.player_id)
296 game.players_hat_chars = game.players_hat_chars_new
297 game.turn_complete = True
298 if game.tui.mode.name == 'post_login_wait':
299 game.tui.switch_mode('play')
300 cmd_GAME_STATE_COMPLETE.argtypes = ''
302 def cmd_PORTAL(game, position, msg):
303 game.portals_new[position] = msg
304 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
306 def cmd_PLAY_ERROR(game, msg):
307 game.tui.log_msg('? ' + msg)
308 game.tui.flash = True
309 game.tui.do_refresh = True
310 cmd_PLAY_ERROR.argtypes = 'string'
312 def cmd_GAME_ERROR(game, msg):
313 game.tui.log_msg('? game error: ' + msg)
314 game.tui.do_refresh = True
315 cmd_GAME_ERROR.argtypes = 'string'
317 def cmd_ARGUMENT_ERROR(game, msg):
318 game.tui.log_msg('? syntax error: ' + msg)
319 game.tui.do_refresh = True
320 cmd_ARGUMENT_ERROR.argtypes = 'string'
322 def cmd_ANNOTATION(game, position, msg):
323 game.annotations_new[position] = msg
324 if game.tui.mode.shows_info:
325 game.tui.do_refresh = True
326 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
328 def cmd_TASKS(game, tasks_comma_separated):
329 game.tasks = tasks_comma_separated.split(',')
330 game.tui.mode_write.legal = 'WRITE' in game.tasks
331 game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks
332 game.tui.mode_take_thing.legal = 'PICK_UP' in game.tasks
333 game.tui.mode_drop_thing.legal = 'DROP' in game.tasks
334 cmd_TASKS.argtypes = 'string'
336 def cmd_THING_TYPE(game, thing_type, symbol_hint):
337 game.thing_types[thing_type] = symbol_hint
338 cmd_THING_TYPE.argtypes = 'string char'
340 def cmd_THING_INSTALLED(game, thing_id):
341 game.get_thing_temp(thing_id).installed = True
342 cmd_THING_INSTALLED.argtypes = 'int:pos'
344 def cmd_THING_CARRYING(game, thing_id, carried_id):
345 game.get_thing_temp(thing_id).carrying = game.get_thing(carried_id)
346 cmd_THING_CARRYING.argtypes = 'int:pos int:pos'
348 def cmd_TERRAIN(game, terrain_char, terrain_desc):
349 game.terrains[terrain_char] = terrain_desc
350 cmd_TERRAIN.argtypes = 'char string'
354 cmd_PONG.argtypes = ''
356 def cmd_DEFAULT_COLORS(game):
357 game.tui.set_default_colors()
358 cmd_DEFAULT_COLORS.argtypes = ''
360 def cmd_RANDOM_COLORS(game):
361 game.tui.set_random_colors()
362 cmd_RANDOM_COLORS.argtypes = ''
364 class Game(GameBase):
365 turn_complete = False
370 def __init__(self, *args, **kwargs):
371 super().__init__(*args, **kwargs)
372 self.register_command(cmd_LOGIN_OK)
373 self.register_command(cmd_ADMIN_OK)
374 self.register_command(cmd_PONG)
375 self.register_command(cmd_CHAT)
376 self.register_command(cmd_CHATFACE)
377 self.register_command(cmd_REPLY)
378 self.register_command(cmd_PLAYER_ID)
379 self.register_command(cmd_TURN)
380 self.register_command(cmd_OTHER_WIPE)
381 self.register_command(cmd_THING)
382 self.register_command(cmd_THING_TYPE)
383 self.register_command(cmd_THING_NAME)
384 self.register_command(cmd_THING_CHAR)
385 self.register_command(cmd_THING_FACE)
386 self.register_command(cmd_THING_HAT)
387 self.register_command(cmd_THING_CARRYING)
388 self.register_command(cmd_THING_INSTALLED)
389 self.register_command(cmd_TERRAIN)
390 self.register_command(cmd_MAP)
391 self.register_command(cmd_MAP_CONTROL)
392 self.register_command(cmd_PORTAL)
393 self.register_command(cmd_ANNOTATION)
394 self.register_command(cmd_GAME_STATE_COMPLETE)
395 self.register_command(cmd_PLAYERS_HAT_CHARS)
396 self.register_command(cmd_ARGUMENT_ERROR)
397 self.register_command(cmd_GAME_ERROR)
398 self.register_command(cmd_PLAY_ERROR)
399 self.register_command(cmd_TASKS)
400 self.register_command(cmd_FOV)
401 self.register_command(cmd_DEFAULT_COLORS)
402 self.register_command(cmd_RANDOM_COLORS)
403 self.map_content = ''
404 self.players_hat_chars = ''
406 self.annotations = {}
407 self.annotations_new = {}
409 self.portals_new = {}
413 def get_string_options(self, string_option_type):
414 if string_option_type == 'map_geometry':
415 return ['Hex', 'Square']
416 elif string_option_type == 'thing_type':
417 return self.thing_types.keys()
420 def get_command(self, command_name):
421 from functools import partial
422 f = partial(self.commands[command_name], self)
423 f.argtypes = self.commands[command_name].argtypes
426 def get_thing_temp(self, id_):
427 for thing in self.things_new:
434 def __init__(self, name, has_input_prompt=False, shows_info=False,
435 is_intro=False, is_single_char_entry=False):
437 self.short_desc = mode_helps[name]['short']
438 self.available_modes = []
439 self.available_actions = []
440 self.has_input_prompt = has_input_prompt
441 self.shows_info = shows_info
442 self.is_intro = is_intro
443 self.help_intro = mode_helps[name]['long']
444 self.intro_msg = mode_helps[name]['intro']
445 self.is_single_char_entry = is_single_char_entry
448 def iter_available_modes(self, tui):
449 for mode_name in self.available_modes:
450 mode = getattr(tui, 'mode_' + mode_name)
453 key = tui.keys['switch_to_' + mode.name]
456 def list_available_modes(self, tui):
458 if len(self.available_modes) > 0:
459 msg = 'Other modes available from here:\n'
460 for mode, key in self.iter_available_modes(tui):
461 msg += '[%s] – %s\n' % (key, mode.short_desc)
464 def mode_switch_on_key(self, tui, key_pressed):
465 for mode, key in self.iter_available_modes(tui):
466 if key_pressed == key:
467 tui.switch_mode(mode.name)
472 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
473 mode_admin = Mode('admin')
474 mode_play = Mode('play')
475 mode_study = Mode('study', shows_info=True)
476 mode_write = Mode('write', is_single_char_entry=True)
477 mode_edit = Mode('edit')
478 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
479 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
480 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
481 mode_control_tile_draw = Mode('control_tile_draw')
482 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
483 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
484 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
485 mode_chat = Mode('chat', has_input_prompt=True)
486 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
487 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
488 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
489 mode_password = Mode('password', has_input_prompt=True)
490 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
491 mode_command_thing = Mode('command_thing', has_input_prompt=True)
492 mode_take_thing = Mode('take_thing', has_input_prompt=True)
493 mode_drop_thing = Mode('drop_thing', has_input_prompt=True)
494 mode_enter_face = Mode('enter_face', has_input_prompt=True)
495 mode_enter_hat = Mode('enter_hat', has_input_prompt=True)
499 def __init__(self, host):
502 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
503 "command_thing", "take_thing",
505 self.mode_play.available_actions = ["move", "teleport", "door", "consume",
506 "install", "wear", "spin"]
507 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
508 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
509 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
510 "control_tile_type", "chat",
511 "study", "play", "edit"]
512 self.mode_admin.available_actions = ["move"]
513 self.mode_control_tile_draw.available_modes = ["admin_enter"]
514 self.mode_control_tile_draw.available_actions = ["move_explorer",
516 self.mode_edit.available_modes = ["write", "annotate", "portal",
517 "name_thing", "enter_face", "enter_hat", "password",
518 "chat", "study", "play", "admin_enter"]
519 self.mode_edit.available_actions = ["move", "flatten", "install",
525 self.parser = Parser(self.game)
527 self.do_refresh = True
528 self.queue = queue.Queue()
529 self.login_name = None
530 self.map_mode = 'terrain + things'
531 self.password = 'foo'
532 self.switch_mode('waiting_for_server')
534 'switch_to_chat': 't',
535 'switch_to_play': 'p',
536 'switch_to_password': 'P',
537 'switch_to_annotate': 'M',
538 'switch_to_portal': 'T',
539 'switch_to_study': '?',
540 'switch_to_edit': 'E',
541 'switch_to_write': 'm',
542 'switch_to_name_thing': 'N',
543 'switch_to_command_thing': 'O',
544 'switch_to_admin_enter': 'A',
545 'switch_to_control_pw_type': 'C',
546 'switch_to_control_tile_type': 'Q',
547 'switch_to_admin_thing_protect': 'T',
549 'switch_to_enter_face': 'f',
550 'switch_to_enter_hat': 'H',
551 'switch_to_take_thing': 'z',
552 'switch_to_drop_thing': 'u',
560 'toggle_map_mode': 'L',
561 'toggle_tile_draw': 'm',
562 'hex_move_upleft': 'w',
563 'hex_move_upright': 'e',
564 'hex_move_right': 'd',
565 'hex_move_downright': 'x',
566 'hex_move_downleft': 'y',
567 'hex_move_left': 'a',
568 'square_move_up': 'w',
569 'square_move_left': 'a',
570 'square_move_down': 's',
571 'square_move_right': 'd',
573 if os.path.isfile('config.json'):
574 with open('config.json', 'r') as f:
575 keys_conf = json.loads(f.read())
577 self.keys[k] = keys_conf[k]
578 self.show_help = False
579 self.disconnected = True
580 self.force_instant_connect = True
581 self.input_lines = []
585 self.ascii_draw_stage = 0
586 self.full_ascii_draw = ''
587 self.offset = YX(0,0)
588 curses.wrapper(self.loop)
592 def handle_recv(msg):
598 self.log_msg('@ attempting connect')
599 socket_client_class = PlomSocketClient
600 if self.host.startswith('ws://') or self.host.startswith('wss://'):
601 socket_client_class = WebSocketClient
603 self.socket = socket_client_class(handle_recv, self.host)
604 self.socket_thread = threading.Thread(target=self.socket.run)
605 self.socket_thread.start()
606 self.disconnected = False
607 self.game.thing_types = {}
608 self.game.terrains = {}
609 time.sleep(0.1) # give potential SSL negotation some time …
610 self.socket.send('TASKS')
611 self.socket.send('TERRAINS')
612 self.socket.send('THING_TYPES')
613 self.switch_mode('login')
614 except ConnectionRefusedError:
615 self.log_msg('@ server connect failure')
616 self.disconnected = True
617 self.switch_mode('waiting_for_server')
618 self.do_refresh = True
621 self.log_msg('@ attempting reconnect')
623 # necessitated by some strange SSL race conditions with ws4py
624 time.sleep(0.1) # FIXME find out why exactly necessary
625 self.switch_mode('waiting_for_server')
630 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
631 raise BrokenSocketConnection
632 self.socket.send(msg)
633 except (BrokenPipeError, BrokenSocketConnection):
634 self.log_msg('@ server disconnected :(')
635 self.disconnected = True
636 self.force_instant_connect = True
637 self.do_refresh = True
639 def log_msg(self, msg):
641 if len(self.log) > 100:
642 self.log = self.log[-100:]
644 def restore_input_values(self):
645 if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
646 self.input_ = self.game.annotations[self.explorer]
647 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
648 self.input_ = self.game.portals[self.explorer]
649 elif self.mode.name == 'password':
650 self.input_ = self.password
651 elif self.mode.name == 'name_thing':
652 if hasattr(self.thing_selected, 'name'):
653 self.input_ = self.thing_selected.name
654 elif self.mode.name == 'admin_thing_protect':
655 if hasattr(self.thing_selected, 'protection'):
656 self.input_ = self.thing_selected.protection
657 elif self.mode.name in {'enter_face', 'enter_hat'}:
658 start = self.ascii_draw_stage * 6
659 end = (self.ascii_draw_stage + 1) * 6
660 if self.mode.name == 'enter_face':
661 self.input_ = self.game.player.face[start:end]
662 elif self.mode.name == 'enter_hat':
663 self.input_ = self.game.player.hat[start:end]
665 def send_tile_control_command(self):
666 self.send('SET_TILE_CONTROL %s %s' %
667 (self.explorer, quote(self.tile_control_char)))
669 def toggle_map_mode(self):
670 if self.map_mode == 'terrain only':
671 self.map_mode = 'terrain + annotations'
672 elif self.map_mode == 'terrain + annotations':
673 self.map_mode = 'terrain + things'
674 elif self.map_mode == 'terrain + things':
675 self.map_mode = 'protections'
676 elif self.map_mode == 'protections':
677 self.map_mode = 'terrain only'
679 def switch_mode(self, mode_name):
681 def fail(msg, return_mode='play'):
682 self.log_msg('? ' + msg)
684 self.switch_mode(return_mode)
686 if self.mode and self.mode.name == 'control_tile_draw':
687 self.log_msg('@ finished tile protection drawing.')
688 self.draw_face = False
689 self.tile_draw = False
690 if mode_name == 'command_thing' and\
691 (not self.game.player.carrying or
692 not self.game.player.carrying.commandable):
693 return fail('not carrying anything commandable')
694 if mode_name == 'take_thing' and self.game.player.carrying:
695 return fail('already carrying something')
696 if mode_name == 'drop_thing' and not self.game.player.carrying:
697 return fail('not carrying anything droppable')
698 if mode_name == 'enter_hat' and not hasattr(self.game.player, 'hat'):
699 return fail('not wearing hat to edit', 'edit')
700 if mode_name == 'admin_enter' and self.is_admin:
702 elif mode_name in {'name_thing', 'admin_thing_protect'}:
704 for t in [t for t in self.game.things
705 if t.position == self.game.player.position
706 and t.id_ != self.game.player.id_]:
710 return fail('not standing over thing', 'edit')
712 self.thing_selected = thing
713 self.mode = getattr(self, 'mode_' + mode_name)
714 if self.mode.name in {'control_tile_draw', 'control_tile_type',
716 self.map_mode = 'protections'
717 elif self.mode.name != 'edit':
718 self.map_mode = 'terrain + things'
719 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
720 self.explorer = YX(self.game.player.position.y,
721 self.game.player.position.x)
722 if self.mode.is_single_char_entry:
723 self.show_help = True
724 if len(self.mode.intro_msg) > 0:
725 self.log_msg(self.mode.intro_msg)
726 if self.mode.name == 'login':
728 self.send('LOGIN ' + quote(self.login_name))
730 self.log_msg('@ enter username')
731 elif self.mode.name == 'take_thing':
732 self.log_msg('Portable things in reach for pick-up:')
733 select_range = [self.game.player.position,
734 self.game.player.position + YX(0,-1),
735 self.game.player.position + YX(0, 1),
736 self.game.player.position + YX(-1, 0),
737 self.game.player.position + YX(1, 0)]
738 if type(self.game.map_geometry) == MapGeometryHex:
739 if self.game.player.position.y % 2:
740 select_range += [self.game.player.position + YX(-1, 1),
741 self.game.player.position + YX(1, 1)]
743 select_range += [self.game.player.position + YX(-1, -1),
744 self.game.player.position + YX(1, -1)]
745 self.selectables = [t.id_ for t in self.game.things
746 if t.portable and t.position in select_range]
747 if len(self.selectables) == 0:
748 return fail('nothing to pick-up')
750 for i in range(len(self.selectables)):
751 t = self.game.get_thing(self.selectables[i])
752 self.log_msg(str(i) + ': ' + self.get_thing_info(t))
753 elif self.mode.name == 'drop_thing':
754 self.log_msg('Direction to drop thing to:')
756 ['HERE'] + list(self.game.tui.movement_keys.values())
757 for i in range(len(self.selectables)):
758 self.log_msg(str(i) + ': ' + self.selectables[i])
759 elif self.mode.name == 'enter_hat':
760 self.log_msg('legal characters: ' + self.game.players_hat_chars)
761 elif self.mode.name == 'command_thing':
762 self.send('TASK:COMMAND ' + quote('HELP'))
763 elif self.mode.name == 'control_pw_pw':
764 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
765 elif self.mode.name == 'control_tile_draw':
766 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']))
768 self.restore_input_values()
770 def set_default_colors(self):
771 curses.init_color(1, 1000, 1000, 1000)
772 curses.init_color(2, 0, 0, 0)
773 self.do_refresh = True
775 def set_random_colors(self):
779 return int(offset + random.random()*375)
781 curses.init_color(1, rand(625), rand(625), rand(625))
782 curses.init_color(2, rand(0), rand(0), rand(0))
783 self.do_refresh = True
787 return self.info_cached
788 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
790 if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
791 info_to_cache += 'outside field of view'
793 for t in self.game.things:
794 if t.position == self.explorer:
795 info_to_cache += 'THING: %s' % self.get_thing_info(t)
796 protection = t.protection
797 if protection == '.':
799 info_to_cache += ' / protection: %s\n' % protection
800 if hasattr(t, 'hat'):
801 info_to_cache += t.hat[0:6] + '\n'
802 info_to_cache += t.hat[6:12] + '\n'
803 info_to_cache += t.hat[12:18] + '\n'
804 if hasattr(t, 'face'):
805 info_to_cache += t.face[0:6] + '\n'
806 info_to_cache += t.face[6:12] + '\n'
807 info_to_cache += t.face[12:18] + '\n'
808 terrain_char = self.game.map_content[pos_i]
810 if terrain_char in self.game.terrains:
811 terrain_desc = self.game.terrains[terrain_char]
812 info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
814 protection = self.game.map_control_content[pos_i]
815 if protection == '.':
816 protection = 'unprotected'
817 info_to_cache += 'PROTECTION: %s\n' % protection
818 if self.explorer in self.game.portals:
819 info_to_cache += 'PORTAL: ' +\
820 self.game.portals[self.explorer] + '\n'
822 info_to_cache += 'PORTAL: (none)\n'
823 if self.explorer in self.game.annotations:
824 info_to_cache += 'ANNOTATION: ' +\
825 self.game.annotations[self.explorer]
826 self.info_cached = info_to_cache
827 return self.info_cached
829 def get_thing_info(self, t):
831 (t.type_, self.game.thing_types[t.type_])
832 if hasattr(t, 'thing_char'):
834 if hasattr(t, 'name'):
835 info += ' (%s)' % t.name
836 if hasattr(t, 'installed'):
837 info += ' / installed'
840 def loop(self, stdscr):
843 def safe_addstr(y, x, line):
844 if y < self.size.y - 1 or x + len(line) < self.size.x:
845 stdscr.addstr(y, x, line, curses.color_pair(1))
846 else: # workaround to <https://stackoverflow.com/q/7063128>
847 cut_i = self.size.x - x - 1
849 last_char = line[cut_i]
850 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
851 stdscr.insstr(y, self.size.x - 2, ' ')
852 stdscr.addstr(y, x, cut, curses.color_pair(1))
854 def handle_input(msg):
855 command, args = self.parser.parse(msg)
858 def task_action_on(action):
859 return action_tasks[action] in self.game.tasks
861 def msg_into_lines_of_width(msg, width):
865 for i in range(len(msg)):
866 if x >= width or msg[i] == "\n":
878 def reset_screen_size():
879 self.size = YX(*stdscr.getmaxyx())
880 self.size = self.size - YX(self.size.y % 4, 0)
881 self.size = self.size - YX(0, self.size.x % 4)
882 self.window_width = int(self.size.x / 2)
884 def recalc_input_lines():
885 if not self.mode.has_input_prompt:
886 self.input_lines = []
888 self.input_lines = msg_into_lines_of_width(input_prompt
892 def move_explorer(direction):
893 target = self.game.map_geometry.move_yx(self.explorer, direction)
895 self.info_cached = None
896 self.explorer = target
898 self.send_tile_control_command()
904 for line in self.log:
905 lines += msg_into_lines_of_width(line, self.window_width)
908 max_y = self.size.y - len(self.input_lines)
909 for i in range(len(lines)):
910 if (i >= max_y - height_header):
912 safe_addstr(max_y - i - 1, self.window_width, lines[i])
915 info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
916 lines = msg_into_lines_of_width(info, self.window_width)
918 for i in range(len(lines)):
919 y = height_header + i
920 if y >= self.size.y - len(self.input_lines):
922 safe_addstr(y, self.window_width, lines[i])
925 y = self.size.y - len(self.input_lines)
926 for i in range(len(self.input_lines)):
927 safe_addstr(y, self.window_width, self.input_lines[i])
931 if not self.game.turn_complete:
933 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
936 help = "hit [%s] for help" % self.keys['help']
937 if self.mode.has_input_prompt:
938 help = "enter /help for help"
939 safe_addstr(1, self.window_width,
940 'MODE: %s – %s' % (self.mode.short_desc, help))
943 if (not self.game.turn_complete) and len(self.map_lines) == 0:
945 if self.game.turn_complete:
947 for y in range(self.game.map_geometry.size.y):
948 start = self.game.map_geometry.size.x * y
949 end = start + self.game.map_geometry.size.x
950 if self.map_mode == 'protections':
951 map_lines_split += [[c + ' ' for c
952 in self.game.map_control_content[start:end]]]
954 map_lines_split += [[c + ' ' for c
955 in self.game.map_content[start:end]]]
956 if self.map_mode == 'terrain + annotations':
957 for p in self.game.annotations:
958 map_lines_split[p.y][p.x] = 'A '
959 elif self.map_mode == 'terrain + things':
960 for p in self.game.portals.keys():
961 original = map_lines_split[p.y][p.x]
962 map_lines_split[p.y][p.x] = original[0] + 'P'
965 def draw_thing(t, used_positions):
966 symbol = self.game.thing_types[t.type_]
968 if hasattr(t, 'thing_char'):
969 meta_char = t.thing_char
970 if t.position in used_positions:
972 if hasattr(t, 'carrying') and t.carrying:
974 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
975 used_positions += [t.position]
977 for t in [t for t in self.game.things if t.type_ != 'Player']:
978 draw_thing(t, used_positions)
979 for t in [t for t in self.game.things if t.type_ == 'Player']:
980 draw_thing(t, used_positions)
981 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
982 map_lines_split[self.explorer.y][self.explorer.x] = '??'
983 elif self.map_mode != 'terrain + things':
984 map_lines_split[self.game.player.position.y]\
985 [self.game.player.position.x] = '??'
987 if type(self.game.map_geometry) == MapGeometryHex:
989 for line in map_lines_split:
990 self.map_lines += [indent * ' ' + ''.join(line)]
991 indent = 0 if indent else 1
993 for line in map_lines_split:
994 self.map_lines += [''.join(line)]
995 window_center = YX(int(self.size.y / 2),
996 int(self.window_width / 2))
997 center = self.game.player.position
998 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
999 center = self.explorer
1000 center = YX(center.y, center.x * 2)
1001 self.offset = center - window_center
1002 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
1003 self.offset += YX(0, 1)
1004 term_y = max(0, -self.offset.y)
1005 term_x = max(0, -self.offset.x)
1006 map_y = max(0, self.offset.y)
1007 map_x = max(0, self.offset.x)
1008 while term_y < self.size.y and map_y < len(self.map_lines):
1009 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
1010 safe_addstr(term_y, term_x, to_draw)
1014 def draw_face_popup():
1015 t = self.game.get_thing(self.draw_face)
1016 if not t or not hasattr(t, 'face'):
1017 self.draw_face = False
1020 start_x = self.window_width - 10
1022 if hasattr(t, 'thing_char'):
1023 t_char = t.thing_char
1024 def draw_body_part(body_part, end_y):
1025 safe_addstr(end_y - 4, start_x, ' _[ @' + t_char + ' ]_ ')
1026 safe_addstr(end_y - 3, start_x, '| |')
1027 safe_addstr(end_y - 2, start_x, '| ' + body_part[0:6] + ' |')
1028 safe_addstr(end_y - 1, start_x, '| ' + body_part[6:12] + ' |')
1029 safe_addstr(end_y, start_x, '| ' + body_part[12:18] + ' |')
1031 if hasattr(t, 'face'):
1032 draw_body_part(t.face, self.size.y - 2)
1033 if hasattr(t, 'hat'):
1034 draw_body_part(t.hat, self.size.y - 5)
1035 safe_addstr(self.size.y - 1, start_x, '| |')
1038 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
1039 self.mode.help_intro)
1040 if len(self.mode.available_actions) > 0:
1041 content += "Available actions:\n"
1042 for action in self.mode.available_actions:
1043 if action in action_tasks:
1044 if action_tasks[action] not in self.game.tasks:
1046 if action == 'move_explorer':
1048 if action == 'move':
1049 key = ','.join(self.movement_keys)
1051 key = self.keys[action]
1052 content += '[%s] – %s\n' % (key, action_descriptions[action])
1054 content += self.mode.list_available_modes(self)
1055 for i in range(self.size.y):
1057 self.window_width * (not self.mode.has_input_prompt),
1058 ' ' * self.window_width)
1060 for line in content.split('\n'):
1061 lines += msg_into_lines_of_width(line, self.window_width)
1062 for i in range(len(lines)):
1063 if i >= self.size.y:
1066 self.window_width * (not self.mode.has_input_prompt),
1071 stdscr.bkgd(' ', curses.color_pair(1))
1072 recalc_input_lines()
1073 if self.mode.has_input_prompt:
1075 if self.mode.shows_info:
1080 if not self.mode.is_intro:
1085 if self.draw_face and self.mode.name in {'chat', 'play'}:
1088 def pick_selectable(task_name):
1090 i = int(self.input_)
1091 if i < 0 or i >= len(self.selectables):
1092 self.log_msg('? invalid index, aborted')
1094 self.send('TASK:%s %s' % (task_name, self.selectables[i]))
1096 self.log_msg('? invalid index, aborted')
1098 self.switch_mode('play')
1100 def enter_ascii_art(command):
1101 if len(self.input_) != 6:
1102 self.log_msg('? wrong input length, must be 6; try again')
1104 self.log_msg(' ' + self.input_)
1105 self.full_ascii_draw += self.input_
1106 self.ascii_draw_stage += 1
1107 if self.ascii_draw_stage < 3:
1108 self.restore_input_values()
1110 self.send('%s %s' % (command, quote(self.full_ascii_draw)))
1111 self.full_ascii_draw = ""
1112 self.ascii_draw_stage = 0
1114 self.switch_mode('edit')
1116 action_descriptions = {
1118 'flatten': 'flatten surroundings',
1119 'teleport': 'teleport',
1120 'take_thing': 'pick up thing',
1121 'drop_thing': 'drop thing',
1122 'toggle_map_mode': 'toggle map view',
1123 'toggle_tile_draw': 'toggle protection character drawing',
1124 'install': '(un-)install',
1125 'wear': '(un-)wear',
1126 'door': 'open/close',
1127 'consume': 'consume',
1132 'flatten': 'FLATTEN_SURROUNDINGS',
1133 'take_thing': 'PICK_UP',
1134 'drop_thing': 'DROP',
1136 'install': 'INSTALL',
1139 'command': 'COMMAND',
1140 'consume': 'INTOXICATE',
1144 curses.curs_set(False) # hide cursor
1145 curses.start_color()
1146 self.set_default_colors()
1147 curses.init_pair(1, 1, 2)
1150 self.explorer = YX(0, 0)
1153 interval = datetime.timedelta(seconds=5)
1154 last_ping = datetime.datetime.now() - interval
1156 if self.disconnected and self.force_instant_connect:
1157 self.force_instant_connect = False
1159 now = datetime.datetime.now()
1160 if now - last_ping > interval:
1161 if self.disconnected:
1171 self.do_refresh = False
1174 msg = self.queue.get(block=False)
1179 key = stdscr.getkey()
1180 self.do_refresh = True
1181 except curses.error:
1186 self.show_help = False
1187 self.draw_face = False
1188 if key == 'KEY_RESIZE':
1190 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1191 self.input_ = self.input_[:-1]
1192 elif (((not self.mode.is_intro) and keycode == 27) # Escape
1193 or (self.mode.has_input_prompt and key == '\n'
1194 and self.input_ == ''\
1195 and self.mode.name in {'chat', 'command_thing',
1196 'take_thing', 'drop_thing',
1198 if self.mode.name not in {'chat', 'play', 'study', 'edit'}:
1199 self.log_msg('@ aborted')
1200 self.switch_mode('play')
1201 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1202 self.show_help = True
1204 self.restore_input_values()
1205 elif self.mode.has_input_prompt and key != '\n': # Return key
1207 max_length = self.window_width * self.size.y - len(input_prompt) - 1
1208 if len(self.input_) > max_length:
1209 self.input_ = self.input_[:max_length]
1210 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1211 self.show_help = True
1212 elif self.mode.name == 'login' and key == '\n':
1213 self.login_name = self.input_
1214 self.send('LOGIN ' + quote(self.input_))
1216 elif self.mode.name == 'enter_face' and key == '\n':
1217 enter_ascii_art('PLAYER_FACE')
1218 elif self.mode.name == 'enter_hat' and key == '\n':
1219 enter_ascii_art('PLAYER_HAT')
1220 elif self.mode.name == 'take_thing' and key == '\n':
1221 pick_selectable('PICK_UP')
1222 elif self.mode.name == 'drop_thing' and key == '\n':
1223 pick_selectable('DROP')
1224 elif self.mode.name == 'command_thing' and key == '\n':
1225 self.send('TASK:COMMAND ' + quote(self.input_))
1227 elif self.mode.name == 'control_pw_pw' and key == '\n':
1228 if self.input_ == '':
1229 self.log_msg('@ aborted')
1231 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1232 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1233 self.switch_mode('admin')
1234 elif self.mode.name == 'password' and key == '\n':
1235 if self.input_ == '':
1237 self.password = self.input_
1238 self.switch_mode('edit')
1239 elif self.mode.name == 'admin_enter' and key == '\n':
1240 self.send('BECOME_ADMIN ' + quote(self.input_))
1241 self.switch_mode('play')
1242 elif self.mode.name == 'control_pw_type' and key == '\n':
1243 if len(self.input_) != 1:
1244 self.log_msg('@ entered non-single-char, therefore aborted')
1245 self.switch_mode('admin')
1247 self.tile_control_char = self.input_
1248 self.switch_mode('control_pw_pw')
1249 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1250 if len(self.input_) != 1:
1251 self.log_msg('@ entered non-single-char, therefore aborted')
1253 self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
1254 quote(self.input_)))
1255 self.log_msg('@ sent new protection character for thing')
1256 self.switch_mode('admin')
1257 elif self.mode.name == 'control_tile_type' and key == '\n':
1258 if len(self.input_) != 1:
1259 self.log_msg('@ entered non-single-char, therefore aborted')
1260 self.switch_mode('admin')
1262 self.tile_control_char = self.input_
1263 self.switch_mode('control_tile_draw')
1264 elif self.mode.name == 'chat' and key == '\n':
1265 if self.input_ == '':
1267 if self.input_[0] == '/':
1268 if self.input_.startswith('/nick'):
1269 tokens = self.input_.split(maxsplit=1)
1270 if len(tokens) == 2:
1271 self.send('NICK ' + quote(tokens[1]))
1273 self.log_msg('? need login name')
1275 self.log_msg('? unknown command')
1277 self.send('ALL ' + quote(self.input_))
1279 elif self.mode.name == 'name_thing' and key == '\n':
1280 if self.input_ == '':
1282 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
1284 quote(self.password)))
1285 self.switch_mode('edit')
1286 elif self.mode.name == 'annotate' and key == '\n':
1287 if self.input_ == '':
1289 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1290 quote(self.password)))
1291 self.switch_mode('edit')
1292 elif self.mode.name == 'portal' and key == '\n':
1293 if self.input_ == '':
1295 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1296 quote(self.password)))
1297 self.switch_mode('edit')
1298 elif self.mode.name == 'study':
1299 if self.mode.mode_switch_on_key(self, key):
1301 elif key == self.keys['toggle_map_mode']:
1302 self.toggle_map_mode()
1303 elif key in self.movement_keys:
1304 move_explorer(self.movement_keys[key])
1305 elif self.mode.name == 'play':
1306 if self.mode.mode_switch_on_key(self, key):
1308 elif key == self.keys['door'] and task_action_on('door'):
1309 self.send('TASK:DOOR')
1310 elif key == self.keys['consume'] and task_action_on('consume'):
1311 self.send('TASK:INTOXICATE')
1312 elif key == self.keys['wear'] and task_action_on('wear'):
1313 self.send('TASK:WEAR')
1314 elif key == self.keys['spin'] and task_action_on('spin'):
1315 self.send('TASK:SPIN')
1316 elif key == self.keys['teleport']:
1317 if self.game.player.position in self.game.portals:
1318 self.host = self.game.portals[self.game.player.position]
1322 self.log_msg('? not standing on portal')
1323 elif key in self.movement_keys and task_action_on('move'):
1324 self.send('TASK:MOVE ' + self.movement_keys[key])
1325 elif self.mode.name == 'write':
1326 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1327 self.switch_mode('edit')
1328 elif self.mode.name == 'control_tile_draw':
1329 if self.mode.mode_switch_on_key(self, key):
1331 elif key in self.movement_keys:
1332 move_explorer(self.movement_keys[key])
1333 elif key == self.keys['toggle_tile_draw']:
1334 self.tile_draw = False if self.tile_draw else True
1335 elif self.mode.name == 'admin':
1336 if self.mode.mode_switch_on_key(self, key):
1338 elif key in self.movement_keys and task_action_on('move'):
1339 self.send('TASK:MOVE ' + self.movement_keys[key])
1340 elif self.mode.name == 'edit':
1341 if self.mode.mode_switch_on_key(self, key):
1343 elif key == self.keys['flatten'] and task_action_on('flatten'):
1344 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1345 elif key == self.keys['install'] and task_action_on('install'):
1346 self.send('TASK:INSTALL %s' % quote(self.password))
1347 elif key == self.keys['toggle_map_mode']:
1348 self.toggle_map_mode()
1349 elif key in self.movement_keys and task_action_on('move'):
1350 self.send('TASK:MOVE ' + self.movement_keys[key])
1352 if len(sys.argv) != 2:
1353 raise ArgError('wrong number of arguments, need game host')