7 from plomrogue.game import GameBase
8 from plomrogue.parser import Parser
9 from plomrogue.mapping import YX, MapGeometrySquare, MapGeometryHex
10 from plomrogue.things import ThingBase
11 from plomrogue.misc import quote
12 from plomrogue.errors import BrokenSocketConnection, ArgError
18 'long': 'This mode allows you to interact with the map in various ways.'
23 'long': 'This mode allows you to study the map and its tiles in detail. Move the question mark over a tile, and the right half of the screen will show detailed information on it. Toggle the map view to show or hide different information layers.'},
25 'short': 'world edit',
27 'long': 'This mode allows you to change the game world in various ways. Individual map tiles can be protected by "protection characters", which you can see by toggling into the protections map view. You can edit a tile if you set the world edit password that matches its protection character. The character "." marks the absence of protection: Such tiles can always be edited.'
30 'short': 'name thing',
32 'long': 'Give name to/change name of thing here.'
35 'short': 'command thing',
37 'long': 'Enter a command to the thing you carry. Enter nothing to return to play mode.'
40 'short': 'take thing',
41 'intro': 'Pick up a thing in reach by entering its index number. Enter nothing to abort.',
42 'long': 'You see a list of things which you could pick up. Enter the target thing\'s index, or, to leave, nothing.'
44 'admin_thing_protect': {
45 'short': 'change thing protection',
46 'intro': '@ enter thing protection character:',
47 'long': 'Change protection character for thing here.'
50 'short': 'enter your face',
51 'intro': '@ enter face line (enter nothing to abort):',
52 'long': 'Draw your face as ASCII art. The string you enter must be 9 characters long, and will be divided on display into three lines of three characters each, from top to bottom..'
55 'short': 'change terrain',
57 '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.'
60 'short': 'change protection character password',
61 'intro': '@ enter protection character for which you want to change the password:',
62 '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.'
65 'short': 'change protection character password',
67 '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.'
69 'control_tile_type': {
70 'short': 'change tiles protection',
71 'intro': '@ enter protection character which you want to draw:',
72 '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.'
74 'control_tile_draw': {
75 'short': 'change tiles protection',
77 '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.'
80 'short': 'annotate tile',
82 '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.'
85 'short': 'edit portal',
87 '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.'
92 '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'
97 'long': 'Enter your player name.'
99 'waiting_for_server': {
100 'short': 'waiting for server response',
101 'intro': '@ waiting for server …',
102 'long': 'Waiting for a server response.'
105 'short': 'waiting for server response',
107 'long': 'Waiting for a server response.'
110 'short': 'set world edit password',
112 '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.'
115 'short': 'become admin',
116 'intro': '@ enter admin password:',
117 'long': 'This mode allows you to become admin if you know an admin password.'
122 'long': 'This mode allows you access to actions limited to administrators.'
126 from ws4py.client import WebSocketBaseClient
127 class WebSocketClient(WebSocketBaseClient):
129 def __init__(self, recv_handler, *args, **kwargs):
130 super().__init__(*args, **kwargs)
131 self.recv_handler = recv_handler
134 def received_message(self, message):
136 message = str(message)
137 self.recv_handler(message)
140 def plom_closed(self):
141 return self.client_terminated
143 from plomrogue.io_tcp import PlomSocket
144 class PlomSocketClient(PlomSocket):
146 def __init__(self, recv_handler, url):
148 self.recv_handler = recv_handler
149 host, port = url.split(':')
150 super().__init__(socket.create_connection((host, port)))
158 for msg in self.recv():
159 if msg == 'NEED_SSL':
160 self.socket = ssl.wrap_socket(self.socket)
162 self.recv_handler(msg)
163 except BrokenSocketConnection:
164 pass # we assume socket will be known as dead by now
166 def cmd_TURN(game, n):
167 game.annotations = {}
171 game.turn_complete = False
173 cmd_TURN.argtypes = 'int:nonneg'
175 def cmd_LOGIN_OK(game):
176 game.tui.switch_mode('post_login_wait')
177 game.tui.send('GET_GAMESTATE')
178 game.tui.log_msg('@ welcome')
179 cmd_LOGIN_OK.argtypes = ''
181 def cmd_ADMIN_OK(game):
182 game.tui.is_admin = True
183 game.tui.log_msg('@ you now have admin rights')
184 game.tui.switch_mode('admin')
185 game.tui.do_refresh = True
186 cmd_ADMIN_OK.argtypes = ''
188 def cmd_REPLY(game, msg):
189 game.tui.log_msg('#MUSICPLAYER: ' + msg)
190 game.tui.do_refresh = True
191 cmd_REPLY.argtypes = 'string'
193 def cmd_CHAT(game, msg):
194 game.tui.log_msg('# ' + msg)
195 game.tui.do_refresh = True
196 cmd_CHAT.argtypes = 'string'
198 def cmd_PLAYER_ID(game, player_id):
199 game.player_id = player_id
200 cmd_PLAYER_ID.argtypes = 'int:nonneg'
202 def cmd_THING(game, yx, thing_type, protection, thing_id, portable):
203 t = game.get_thing(thing_id)
205 t = ThingBase(game, thing_id)
209 t.protection = protection
210 t.portable = portable
211 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg bool'
213 def cmd_THING_NAME(game, thing_id, name):
214 t = game.get_thing(thing_id)
216 cmd_THING_NAME.argtypes = 'int:pos string'
218 def cmd_THING_FACE(game, thing_id, face):
219 t = game.get_thing(thing_id)
221 cmd_THING_FACE.argtypes = 'int:pos string'
223 def cmd_THING_CHAR(game, thing_id, c):
224 t = game.get_thing(thing_id)
226 cmd_THING_CHAR.argtypes = 'int:pos char'
228 def cmd_MAP(game, geometry, size, content):
229 map_geometry_class = globals()['MapGeometry' + geometry]
230 game.map_geometry = map_geometry_class(size)
231 game.map_content = content
232 if type(game.map_geometry) == MapGeometrySquare:
233 game.tui.movement_keys = {
234 game.tui.keys['square_move_up']: 'UP',
235 game.tui.keys['square_move_left']: 'LEFT',
236 game.tui.keys['square_move_down']: 'DOWN',
237 game.tui.keys['square_move_right']: 'RIGHT',
239 elif type(game.map_geometry) == MapGeometryHex:
240 game.tui.movement_keys = {
241 game.tui.keys['hex_move_upleft']: 'UPLEFT',
242 game.tui.keys['hex_move_upright']: 'UPRIGHT',
243 game.tui.keys['hex_move_right']: 'RIGHT',
244 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
245 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
246 game.tui.keys['hex_move_left']: 'LEFT',
248 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
250 def cmd_FOV(game, content):
252 cmd_FOV.argtypes = 'string'
254 def cmd_MAP_CONTROL(game, content):
255 game.map_control_content = content
256 cmd_MAP_CONTROL.argtypes = 'string'
258 def cmd_GAME_STATE_COMPLETE(game):
259 if game.tui.mode.name == 'post_login_wait':
260 game.tui.switch_mode('play')
261 game.turn_complete = True
262 game.tui.do_refresh = True
263 game.tui.info_cached = None
264 cmd_GAME_STATE_COMPLETE.argtypes = ''
266 def cmd_PORTAL(game, position, msg):
267 game.portals[position] = msg
268 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
270 def cmd_PLAY_ERROR(game, msg):
271 game.tui.log_msg('? ' + msg)
272 game.tui.flash = True
273 game.tui.do_refresh = True
274 cmd_PLAY_ERROR.argtypes = 'string'
276 def cmd_GAME_ERROR(game, msg):
277 game.tui.log_msg('? game error: ' + msg)
278 game.tui.do_refresh = True
279 cmd_GAME_ERROR.argtypes = 'string'
281 def cmd_ARGUMENT_ERROR(game, msg):
282 game.tui.log_msg('? syntax error: ' + msg)
283 game.tui.do_refresh = True
284 cmd_ARGUMENT_ERROR.argtypes = 'string'
286 def cmd_ANNOTATION(game, position, msg):
287 game.annotations[position] = msg
288 if game.tui.mode.shows_info:
289 game.tui.do_refresh = True
290 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
292 def cmd_TASKS(game, tasks_comma_separated):
293 game.tasks = tasks_comma_separated.split(',')
294 game.tui.mode_write.legal = 'WRITE' in game.tasks
295 game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks
296 game.tui.mode_take_thing.legal = 'PICK_UP' in game.tasks
297 cmd_TASKS.argtypes = 'string'
299 def cmd_THING_TYPE(game, thing_type, symbol_hint):
300 game.thing_types[thing_type] = symbol_hint
301 cmd_THING_TYPE.argtypes = 'string char'
303 def cmd_THING_INSTALLED(game, thing_id):
304 game.get_thing(thing_id).installed = True
305 cmd_THING_INSTALLED.argtypes = 'int:pos'
307 def cmd_THING_CARRYING(game, thing_id):
308 game.get_thing(thing_id).carrying = True
309 cmd_THING_CARRYING.argtypes = 'int:pos'
311 def cmd_TERRAIN(game, terrain_char, terrain_desc):
312 game.terrains[terrain_char] = terrain_desc
313 cmd_TERRAIN.argtypes = 'char string'
317 cmd_PONG.argtypes = ''
319 def cmd_DEFAULT_COLORS(game):
320 game.tui.set_default_colors()
321 cmd_DEFAULT_COLORS.argtypes = ''
323 def cmd_RANDOM_COLORS(game):
324 game.tui.set_random_colors()
325 cmd_RANDOM_COLORS.argtypes = ''
327 class Game(GameBase):
328 turn_complete = False
332 def __init__(self, *args, **kwargs):
333 super().__init__(*args, **kwargs)
334 self.register_command(cmd_LOGIN_OK)
335 self.register_command(cmd_ADMIN_OK)
336 self.register_command(cmd_PONG)
337 self.register_command(cmd_CHAT)
338 self.register_command(cmd_REPLY)
339 self.register_command(cmd_PLAYER_ID)
340 self.register_command(cmd_TURN)
341 self.register_command(cmd_THING)
342 self.register_command(cmd_THING_TYPE)
343 self.register_command(cmd_THING_NAME)
344 self.register_command(cmd_THING_CHAR)
345 self.register_command(cmd_THING_FACE)
346 self.register_command(cmd_THING_CARRYING)
347 self.register_command(cmd_THING_INSTALLED)
348 self.register_command(cmd_TERRAIN)
349 self.register_command(cmd_MAP)
350 self.register_command(cmd_MAP_CONTROL)
351 self.register_command(cmd_PORTAL)
352 self.register_command(cmd_ANNOTATION)
353 self.register_command(cmd_GAME_STATE_COMPLETE)
354 self.register_command(cmd_ARGUMENT_ERROR)
355 self.register_command(cmd_GAME_ERROR)
356 self.register_command(cmd_PLAY_ERROR)
357 self.register_command(cmd_TASKS)
358 self.register_command(cmd_FOV)
359 self.register_command(cmd_DEFAULT_COLORS)
360 self.register_command(cmd_RANDOM_COLORS)
361 self.map_content = ''
363 self.annotations = {}
367 def get_string_options(self, string_option_type):
368 if string_option_type == 'map_geometry':
369 return ['Hex', 'Square']
370 elif string_option_type == 'thing_type':
371 return self.thing_types.keys()
374 def get_command(self, command_name):
375 from functools import partial
376 f = partial(self.commands[command_name], self)
377 f.argtypes = self.commands[command_name].argtypes
382 def __init__(self, name, has_input_prompt=False, shows_info=False,
383 is_intro=False, is_single_char_entry=False):
385 self.short_desc = mode_helps[name]['short']
386 self.available_modes = []
387 self.available_actions = []
388 self.has_input_prompt = has_input_prompt
389 self.shows_info = shows_info
390 self.is_intro = is_intro
391 self.help_intro = mode_helps[name]['long']
392 self.intro_msg = mode_helps[name]['intro']
393 self.is_single_char_entry = is_single_char_entry
396 def iter_available_modes(self, tui):
397 for mode_name in self.available_modes:
398 mode = getattr(tui, 'mode_' + mode_name)
401 key = tui.keys['switch_to_' + mode.name]
404 def list_available_modes(self, tui):
406 if len(self.available_modes) > 0:
407 msg = 'Other modes available from here:\n'
408 for mode, key in self.iter_available_modes(tui):
409 msg += '[%s] – %s\n' % (key, mode.short_desc)
412 def mode_switch_on_key(self, tui, key_pressed):
413 for mode, key in self.iter_available_modes(tui):
414 if key_pressed == key:
415 tui.switch_mode(mode.name)
420 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
421 mode_admin = Mode('admin')
422 mode_play = Mode('play')
423 mode_study = Mode('study', shows_info=True)
424 mode_write = Mode('write', is_single_char_entry=True)
425 mode_edit = Mode('edit')
426 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
427 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
428 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
429 mode_control_tile_draw = Mode('control_tile_draw')
430 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
431 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
432 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
433 mode_chat = Mode('chat', has_input_prompt=True)
434 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
435 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
436 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
437 mode_password = Mode('password', has_input_prompt=True)
438 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
439 mode_command_thing = Mode('command_thing', has_input_prompt=True)
440 mode_take_thing = Mode('take_thing', has_input_prompt=True)
441 mode_enter_face = Mode('enter_face', has_input_prompt=True)
445 def __init__(self, host):
448 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
449 "command_thing", "take_thing"]
450 self.mode_play.available_actions = ["move", "drop_thing",
451 "teleport", "door", "consume",
453 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
454 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
455 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
456 "control_tile_type", "chat",
457 "study", "play", "edit"]
458 self.mode_admin.available_actions = ["move"]
459 self.mode_control_tile_draw.available_modes = ["admin_enter"]
460 self.mode_control_tile_draw.available_actions = ["move_explorer",
462 self.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
463 "password", "chat", "study", "play",
464 "admin_enter", "enter_face"]
465 self.mode_edit.available_actions = ["move", "flatten", "toggle_map_mode"]
470 self.parser = Parser(self.game)
472 self.do_refresh = True
473 self.queue = queue.Queue()
474 self.login_name = None
475 self.map_mode = 'terrain + things'
476 self.password = 'foo'
477 self.switch_mode('waiting_for_server')
479 'switch_to_chat': 't',
480 'switch_to_play': 'p',
481 'switch_to_password': 'P',
482 'switch_to_annotate': 'M',
483 'switch_to_portal': 'T',
484 'switch_to_study': '?',
485 'switch_to_edit': 'E',
486 'switch_to_write': 'm',
487 'switch_to_name_thing': 'N',
488 'switch_to_command_thing': 'O',
489 'switch_to_admin_enter': 'A',
490 'switch_to_control_pw_type': 'C',
491 'switch_to_control_tile_type': 'Q',
492 'switch_to_admin_thing_protect': 'T',
494 'switch_to_enter_face': 'f',
495 'switch_to_take_thing': 'z',
502 'toggle_map_mode': 'L',
503 'toggle_tile_draw': 'm',
504 'hex_move_upleft': 'w',
505 'hex_move_upright': 'e',
506 'hex_move_right': 'd',
507 'hex_move_downright': 'x',
508 'hex_move_downleft': 'y',
509 'hex_move_left': 'a',
510 'square_move_up': 'w',
511 'square_move_left': 'a',
512 'square_move_down': 's',
513 'square_move_right': 'd',
515 if os.path.isfile('config.json'):
516 with open('config.json', 'r') as f:
517 keys_conf = json.loads(f.read())
519 self.keys[k] = keys_conf[k]
520 self.show_help = False
521 self.disconnected = True
522 self.force_instant_connect = True
523 self.input_lines = []
527 self.offset = YX(0,0)
528 curses.wrapper(self.loop)
532 def handle_recv(msg):
538 self.log_msg('@ attempting connect')
539 socket_client_class = PlomSocketClient
540 if self.host.startswith('ws://') or self.host.startswith('wss://'):
541 socket_client_class = WebSocketClient
543 self.socket = socket_client_class(handle_recv, self.host)
544 self.socket_thread = threading.Thread(target=self.socket.run)
545 self.socket_thread.start()
546 self.disconnected = False
547 self.game.thing_types = {}
548 self.game.terrains = {}
549 time.sleep(0.1) # give potential SSL negotation some time …
550 self.socket.send('TASKS')
551 self.socket.send('TERRAINS')
552 self.socket.send('THING_TYPES')
553 self.switch_mode('login')
554 except ConnectionRefusedError:
555 self.log_msg('@ server connect failure')
556 self.disconnected = True
557 self.switch_mode('waiting_for_server')
558 self.do_refresh = True
561 self.log_msg('@ attempting reconnect')
563 # necessitated by some strange SSL race conditions with ws4py
564 time.sleep(0.1) # FIXME find out why exactly necessary
565 self.switch_mode('waiting_for_server')
570 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
571 raise BrokenSocketConnection
572 self.socket.send(msg)
573 except (BrokenPipeError, BrokenSocketConnection):
574 self.log_msg('@ server disconnected :(')
575 self.disconnected = True
576 self.force_instant_connect = True
577 self.do_refresh = True
579 def log_msg(self, msg):
581 if len(self.log) > 100:
582 self.log = self.log[-100:]
584 def restore_input_values(self):
585 if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
586 self.input_ = self.game.annotations[self.explorer]
587 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
588 self.input_ = self.game.portals[self.explorer]
589 elif self.mode.name == 'password':
590 self.input_ = self.password
591 elif self.mode.name == 'name_thing':
592 if hasattr(self.thing_selected, 'name'):
593 self.input_ = self.thing_selected.name
594 elif self.mode.name == 'admin_thing_protect':
595 if hasattr(self.thing_selected, 'protection'):
596 self.input_ = self.thing_selected.protection
598 def send_tile_control_command(self):
599 self.send('SET_TILE_CONTROL %s %s' %
600 (self.explorer, quote(self.tile_control_char)))
602 def toggle_map_mode(self):
603 if self.map_mode == 'terrain only':
604 self.map_mode = 'terrain + annotations'
605 elif self.map_mode == 'terrain + annotations':
606 self.map_mode = 'terrain + things'
607 elif self.map_mode == 'terrain + things':
608 self.map_mode = 'protections'
609 elif self.map_mode == 'protections':
610 self.map_mode = 'terrain only'
612 def switch_mode(self, mode_name):
613 if self.mode and self.mode.name == 'control_tile_draw':
614 self.log_msg('@ finished tile protection drawing.')
615 self.tile_draw = False
616 if mode_name == 'admin_enter' and self.is_admin:
618 elif mode_name in {'name_thing', 'admin_thing_protect'}:
619 player = self.game.get_thing(self.game.player_id)
621 for t in [t for t in self.game.things if t.position == player.position
622 and t.id_ != player.id_]:
627 self.log_msg('? not standing over thing')
630 self.thing_selected = thing
631 self.mode = getattr(self, 'mode_' + mode_name)
632 if self.mode.name in {'control_tile_draw', 'control_tile_type',
634 self.map_mode = 'protections'
635 elif self.mode.name != 'edit':
636 self.map_mode = 'terrain + things'
637 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
638 player = self.game.get_thing(self.game.player_id)
639 self.explorer = YX(player.position.y, player.position.x)
640 if self.mode.is_single_char_entry:
641 self.show_help = True
642 if len(self.mode.intro_msg) > 0:
643 self.log_msg(self.mode.intro_msg)
644 if self.mode.name == 'login':
646 self.send('LOGIN ' + quote(self.login_name))
648 self.log_msg('@ enter username')
649 elif self.mode.name == 'take_thing':
650 self.log_msg('Portable things in reach for pick-up:')
651 player = self.game.get_thing(self.game.player_id)
652 select_range = [player.position,
653 player.position + YX(0,-1),
654 player.position + YX(0, 1),
655 player.position + YX(-1, 0),
656 player.position + YX(1, 0)]
657 if type(self.game.map_geometry) == MapGeometryHex:
658 if player.position.y % 2:
659 select_range += [player.position + YX(-1, 1),
660 player.position + YX(1, 1)]
662 select_range += [player.position + YX(-1, -1),
663 player.position + YX(1, -1)]
664 self.selectables = [t for t in self.game.things
665 if t.portable and t.position in select_range]
666 if len(self.selectables) == 0:
669 for i in range(len(self.selectables)):
670 t = self.selectables[i]
671 self.log_msg(str(i) + ': ' + self.get_thing_info(t))
672 elif self.mode.name == 'command_thing':
673 self.send('TASK:COMMAND ' + quote('HELP'))
674 elif self.mode.name == 'control_pw_pw':
675 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
676 elif self.mode.name == 'control_tile_draw':
677 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']))
679 self.restore_input_values()
681 def set_default_colors(self):
682 curses.init_color(1, 1000, 1000, 1000)
683 curses.init_color(2, 0, 0, 0)
684 self.do_refresh = True
686 def set_random_colors(self):
690 return int(offset + random.random()*375)
692 curses.init_color(1, rand(625), rand(625), rand(625))
693 curses.init_color(2, rand(0), rand(0), rand(0))
694 self.do_refresh = True
698 return self.info_cached
699 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
701 if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
702 info_to_cache += 'outside field of view'
704 for t in self.game.things:
705 if t.position == self.explorer:
706 info_to_cache += 'THING: %s' % self.get_thing_info(t)
707 protection = t.protection
708 if protection == '.':
710 info_to_cache += ' / protection: %s\n' % protection
711 if hasattr(t, 'face'):
712 info_to_cache += t.face[0:3] + '\n'
713 info_to_cache += t.face[3:6] + '\n'
714 info_to_cache += t.face[6:9] + '\n'
715 terrain_char = self.game.map_content[pos_i]
717 if terrain_char in self.game.terrains:
718 terrain_desc = self.game.terrains[terrain_char]
719 info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
721 protection = self.game.map_control_content[pos_i]
722 if protection == '.':
723 protection = 'unprotected'
724 info_to_cache += 'PROTECTION: %s\n' % protection
725 if self.explorer in self.game.portals:
726 info_to_cache += 'PORTAL: ' +\
727 self.game.portals[self.explorer] + '\n'
729 info_to_cache += 'PORTAL: (none)\n'
730 if self.explorer in self.game.annotations:
731 info_to_cache += 'ANNOTATION: ' +\
732 self.game.annotations[self.explorer]
733 self.info_cached = info_to_cache
734 return self.info_cached
736 def get_thing_info(self, t):
738 (t.type_, self.game.thing_types[t.type_])
739 if hasattr(t, 'thing_char'):
741 if hasattr(t, 'name'):
742 info += ' (%s)' % t.name
743 if hasattr(t, 'installed'):
744 info += ' / installed'
747 def loop(self, stdscr):
750 def safe_addstr(y, x, line):
751 if y < self.size.y - 1 or x + len(line) < self.size.x:
752 stdscr.addstr(y, x, line, curses.color_pair(1))
753 else: # workaround to <https://stackoverflow.com/q/7063128>
754 cut_i = self.size.x - x - 1
756 last_char = line[cut_i]
757 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
758 stdscr.insstr(y, self.size.x - 2, ' ')
759 stdscr.addstr(y, x, cut, curses.color_pair(1))
761 def handle_input(msg):
762 command, args = self.parser.parse(msg)
765 def task_action_on(action):
766 return action_tasks[action] in self.game.tasks
768 def msg_into_lines_of_width(msg, width):
772 for i in range(len(msg)):
773 if x >= width or msg[i] == "\n":
785 def reset_screen_size():
786 self.size = YX(*stdscr.getmaxyx())
787 self.size = self.size - YX(self.size.y % 4, 0)
788 self.size = self.size - YX(0, self.size.x % 4)
789 self.window_width = int(self.size.x / 2)
791 def recalc_input_lines():
792 if not self.mode.has_input_prompt:
793 self.input_lines = []
795 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
798 def move_explorer(direction):
799 target = self.game.map_geometry.move_yx(self.explorer, direction)
801 self.info_cached = None
802 self.explorer = target
804 self.send_tile_control_command()
810 for line in self.log:
811 lines += msg_into_lines_of_width(line, self.window_width)
814 max_y = self.size.y - len(self.input_lines)
815 for i in range(len(lines)):
816 if (i >= max_y - height_header):
818 safe_addstr(max_y - i - 1, self.window_width, lines[i])
821 info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
822 lines = msg_into_lines_of_width(info, self.window_width)
824 for i in range(len(lines)):
825 y = height_header + i
826 if y >= self.size.y - len(self.input_lines):
828 safe_addstr(y, self.window_width, lines[i])
831 y = self.size.y - len(self.input_lines)
832 for i in range(len(self.input_lines)):
833 safe_addstr(y, self.window_width, self.input_lines[i])
837 if not self.game.turn_complete:
839 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
842 help = "hit [%s] for help" % self.keys['help']
843 if self.mode.has_input_prompt:
844 help = "enter /help for help"
845 safe_addstr(1, self.window_width,
846 'MODE: %s – %s' % (self.mode.short_desc, help))
849 if not self.game.turn_complete and len(self.map_lines) == 0:
851 if self.game.turn_complete:
853 for y in range(self.game.map_geometry.size.y):
854 start = self.game.map_geometry.size.x * y
855 end = start + self.game.map_geometry.size.x
856 if self.map_mode == 'protections':
857 map_lines_split += [[c + ' ' for c
858 in self.game.map_control_content[start:end]]]
860 map_lines_split += [[c + ' ' for c
861 in self.game.map_content[start:end]]]
862 if self.map_mode == 'terrain + annotations':
863 for p in self.game.annotations:
864 map_lines_split[p.y][p.x] = 'A '
865 elif self.map_mode == 'terrain + things':
866 for p in self.game.portals.keys():
867 original = map_lines_split[p.y][p.x]
868 map_lines_split[p.y][p.x] = original[0] + 'P'
871 def draw_thing(t, used_positions):
872 symbol = self.game.thing_types[t.type_]
874 if hasattr(t, 'thing_char'):
875 meta_char = t.thing_char
876 if t.position in used_positions:
878 if hasattr(t, 'carrying') and t.carrying:
880 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
881 used_positions += [t.position]
883 for t in [t for t in self.game.things if t.type_ != 'Player']:
884 draw_thing(t, used_positions)
885 for t in [t for t in self.game.things if t.type_ == 'Player']:
886 draw_thing(t, used_positions)
887 player = self.game.get_thing(self.game.player_id)
888 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
889 map_lines_split[self.explorer.y][self.explorer.x] = '??'
890 elif self.map_mode != 'terrain + things':
891 map_lines_split[player.position.y][player.position.x] = '??'
893 if type(self.game.map_geometry) == MapGeometryHex:
895 for line in map_lines_split:
896 self.map_lines += [indent * ' ' + ''.join(line)]
897 indent = 0 if indent else 1
899 for line in map_lines_split:
900 self.map_lines += [''.join(line)]
901 window_center = YX(int(self.size.y / 2),
902 int(self.window_width / 2))
903 center = player.position
904 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
905 center = self.explorer
906 center = YX(center.y, center.x * 2)
907 self.offset = center - window_center
908 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
909 self.offset += YX(0, 1)
910 term_y = max(0, -self.offset.y)
911 term_x = max(0, -self.offset.x)
912 map_y = max(0, self.offset.y)
913 map_x = max(0, self.offset.x)
914 while term_y < self.size.y and map_y < len(self.map_lines):
915 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
916 safe_addstr(term_y, term_x, to_draw)
921 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
922 self.mode.help_intro)
923 if len(self.mode.available_actions) > 0:
924 content += "Available actions:\n"
925 for action in self.mode.available_actions:
926 if action in action_tasks:
927 if action_tasks[action] not in self.game.tasks:
929 if action == 'move_explorer':
932 key = ','.join(self.movement_keys)
934 key = self.keys[action]
935 content += '[%s] – %s\n' % (key, action_descriptions[action])
937 content += self.mode.list_available_modes(self)
938 for i in range(self.size.y):
940 self.window_width * (not self.mode.has_input_prompt),
941 ' ' * self.window_width)
943 for line in content.split('\n'):
944 lines += msg_into_lines_of_width(line, self.window_width)
945 for i in range(len(lines)):
949 self.window_width * (not self.mode.has_input_prompt),
954 stdscr.bkgd(' ', curses.color_pair(1))
956 if self.mode.has_input_prompt:
958 if self.mode.shows_info:
963 if not self.mode.is_intro:
969 action_descriptions = {
971 'flatten': 'flatten surroundings',
972 'teleport': 'teleport',
973 'take_thing': 'pick up thing',
974 'drop_thing': 'drop thing',
975 'toggle_map_mode': 'toggle map view',
976 'toggle_tile_draw': 'toggle protection character drawing',
977 'install': '(un-)install',
978 'door': 'open/close',
979 'consume': 'consume',
983 'flatten': 'FLATTEN_SURROUNDINGS',
984 'take_thing': 'PICK_UP',
985 'drop_thing': 'DROP',
987 'install': 'INSTALL',
989 'command': 'COMMAND',
990 'consume': 'INTOXICATE',
993 curses.curs_set(False) # hide cursor
995 self.set_default_colors()
996 curses.init_pair(1, 1, 2)
999 self.explorer = YX(0, 0)
1002 interval = datetime.timedelta(seconds=5)
1003 last_ping = datetime.datetime.now() - interval
1005 if self.disconnected and self.force_instant_connect:
1006 self.force_instant_connect = False
1008 now = datetime.datetime.now()
1009 if now - last_ping > interval:
1010 if self.disconnected:
1020 self.do_refresh = False
1023 msg = self.queue.get(block=False)
1028 key = stdscr.getkey()
1029 self.do_refresh = True
1030 except curses.error:
1032 self.show_help = False
1033 if key == 'KEY_RESIZE':
1035 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1036 self.input_ = self.input_[:-1]
1037 elif self.mode.has_input_prompt and key == '\n' and self.input_ == ''\
1038 and self.mode.name in {'chat', 'command_thing', 'take_thing',
1040 if self.mode.name != 'chat':
1041 self.log_msg('@ aborted')
1042 self.switch_mode('play')
1043 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1044 self.show_help = True
1046 self.restore_input_values()
1047 elif self.mode.has_input_prompt and key != '\n': # Return key
1049 max_length = self.window_width * self.size.y - len(input_prompt) - 1
1050 if len(self.input_) > max_length:
1051 self.input_ = self.input_[:max_length]
1052 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1053 self.show_help = True
1054 elif self.mode.name == 'login' and key == '\n':
1055 self.login_name = self.input_
1056 self.send('LOGIN ' + quote(self.input_))
1058 elif self.mode.name == 'enter_face' and key == '\n':
1059 if len(self.input_) != 9:
1060 self.log_msg('? wrong input length, aborting')
1062 self.send('PLAYER_FACE %s' % quote(self.input_))
1064 self.switch_mode('edit')
1065 elif self.mode.name == 'take_thing' and key == '\n':
1067 i = int(self.input_)
1068 if i < 0 or i >= len(self.selectables):
1069 self.log_msg('? invalid index, aborted')
1071 self.send('TASK:PICK_UP %s' % self.selectables[i].id_)
1073 self.log_msg('? invalid index, aborted')
1075 self.switch_mode('play')
1076 elif self.mode.name == 'command_thing' and key == '\n':
1077 if task_action_on('command'):
1078 self.send('TASK:COMMAND ' + quote(self.input_))
1080 elif self.mode.name == 'control_pw_pw' and key == '\n':
1081 if self.input_ == '':
1082 self.log_msg('@ aborted')
1084 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1085 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1086 self.switch_mode('admin')
1087 elif self.mode.name == 'password' and key == '\n':
1088 if self.input_ == '':
1090 self.password = self.input_
1091 self.switch_mode('edit')
1092 elif self.mode.name == 'admin_enter' and key == '\n':
1093 self.send('BECOME_ADMIN ' + quote(self.input_))
1094 self.switch_mode('play')
1095 elif self.mode.name == 'control_pw_type' and key == '\n':
1096 if len(self.input_) != 1:
1097 self.log_msg('@ entered non-single-char, therefore aborted')
1098 self.switch_mode('admin')
1100 self.tile_control_char = self.input_
1101 self.switch_mode('control_pw_pw')
1102 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1103 if len(self.input_) != 1:
1104 self.log_msg('@ entered non-single-char, therefore aborted')
1106 self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
1107 quote(self.input_)))
1108 self.log_msg('@ sent new protection character for thing')
1109 self.switch_mode('admin')
1110 elif self.mode.name == 'control_tile_type' and key == '\n':
1111 if len(self.input_) != 1:
1112 self.log_msg('@ entered non-single-char, therefore aborted')
1113 self.switch_mode('admin')
1115 self.tile_control_char = self.input_
1116 self.switch_mode('control_tile_draw')
1117 elif self.mode.name == 'chat' and key == '\n':
1118 if self.input_ == '':
1120 if self.input_[0] == '/':
1121 if self.input_.startswith('/nick'):
1122 tokens = self.input_.split(maxsplit=1)
1123 if len(tokens) == 2:
1124 self.send('NICK ' + quote(tokens[1]))
1126 self.log_msg('? need login name')
1128 self.log_msg('? unknown command')
1130 self.send('ALL ' + quote(self.input_))
1132 elif self.mode.name == 'name_thing' and key == '\n':
1133 if self.input_ == '':
1135 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
1137 quote(self.password)))
1138 self.switch_mode('edit')
1139 elif self.mode.name == 'annotate' and key == '\n':
1140 if self.input_ == '':
1142 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1143 quote(self.password)))
1144 self.switch_mode('edit')
1145 elif self.mode.name == 'portal' and key == '\n':
1146 if self.input_ == '':
1148 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1149 quote(self.password)))
1150 self.switch_mode('edit')
1151 elif self.mode.name == 'study':
1152 if self.mode.mode_switch_on_key(self, key):
1154 elif key == self.keys['toggle_map_mode']:
1155 self.toggle_map_mode()
1156 elif key in self.movement_keys:
1157 move_explorer(self.movement_keys[key])
1158 elif self.mode.name == 'play':
1159 if self.mode.mode_switch_on_key(self, key):
1161 elif key == self.keys['drop_thing'] and task_action_on('drop_thing'):
1162 self.send('TASK:DROP')
1163 elif key == self.keys['door'] and task_action_on('door'):
1164 self.send('TASK:DOOR')
1165 elif key == self.keys['consume'] and task_action_on('consume'):
1166 self.send('TASK:INTOXICATE')
1167 elif key == self.keys['install'] and task_action_on('install'):
1168 self.send('TASK:INSTALL')
1169 elif key == self.keys['teleport']:
1170 player = self.game.get_thing(self.game.player_id)
1171 if player.position in self.game.portals:
1172 self.host = self.game.portals[player.position]
1176 self.log_msg('? not standing on portal')
1177 elif key in self.movement_keys and task_action_on('move'):
1178 self.send('TASK:MOVE ' + self.movement_keys[key])
1179 elif self.mode.name == 'write':
1180 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1181 self.switch_mode('edit')
1182 elif self.mode.name == 'control_tile_draw':
1183 if self.mode.mode_switch_on_key(self, key):
1185 elif key in self.movement_keys:
1186 move_explorer(self.movement_keys[key])
1187 elif key == self.keys['toggle_tile_draw']:
1188 self.tile_draw = False if self.tile_draw else True
1189 elif self.mode.name == 'admin':
1190 if self.mode.mode_switch_on_key(self, key):
1192 elif key in self.movement_keys and task_action_on('move'):
1193 self.send('TASK:MOVE ' + self.movement_keys[key])
1194 elif self.mode.name == 'edit':
1195 if self.mode.mode_switch_on_key(self, key):
1197 elif key == self.keys['flatten'] and task_action_on('flatten'):
1198 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1199 elif key == self.keys['toggle_map_mode']:
1200 self.toggle_map_mode()
1201 elif key in self.movement_keys and task_action_on('move'):
1202 self.send('TASK:MOVE ' + self.movement_keys[key])
1204 if len(sys.argv) != 2:
1205 raise ArgError('wrong number of arguments, need game host')