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 carried thing.'
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 carried thing.'
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):
177 game.turn_complete = False
178 cmd_TURN.argtypes = 'int:nonneg'
180 def cmd_OTHER_WIPE(game):
181 game.portals_new = {}
182 game.annotations_new = {}
184 cmd_OTHER_WIPE.argtypes = ''
186 def cmd_LOGIN_OK(game):
187 game.tui.switch_mode('post_login_wait')
188 game.tui.send('GET_GAMESTATE')
189 game.tui.log_msg('@ welcome')
190 cmd_LOGIN_OK.argtypes = ''
192 def cmd_ADMIN_OK(game):
193 game.tui.is_admin = True
194 game.tui.log_msg('@ you now have admin rights')
195 game.tui.switch_mode('admin')
196 game.tui.do_refresh = True
197 cmd_ADMIN_OK.argtypes = ''
199 def cmd_REPLY(game, msg):
200 game.tui.log_msg('#MUSICPLAYER: ' + msg)
201 game.tui.do_refresh = True
202 cmd_REPLY.argtypes = 'string'
204 def cmd_CHAT(game, msg):
205 game.tui.log_msg('# ' + msg)
206 game.tui.do_refresh = True
207 cmd_CHAT.argtypes = 'string'
209 def cmd_CHATFACE(game, thing_id):
210 game.tui.draw_face = thing_id
211 game.tui.do_refresh = True
212 cmd_CHATFACE.argtypes = 'int:pos'
214 def cmd_PLAYER_ID(game, player_id):
215 game.player_id = player_id
216 cmd_PLAYER_ID.argtypes = 'int:nonneg'
218 def cmd_PLAYERS_HAT_CHARS(game, hat_chars):
219 game.players_hat_chars_new = hat_chars
220 cmd_PLAYERS_HAT_CHARS.argtypes = 'string'
222 def cmd_THING(game, yx, thing_type, protection, thing_id, portable, commandable):
223 t = game.get_thing_temp(thing_id)
225 t = ThingBase(game, thing_id)
226 game.things_new += [t]
229 t.protection = protection
230 t.portable = portable
231 t.commandable = commandable
232 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg bool bool'
234 def cmd_THING_NAME(game, thing_id, name):
235 t = game.get_thing_temp(thing_id)
237 cmd_THING_NAME.argtypes = 'int:pos string'
239 def cmd_THING_FACE(game, thing_id, face):
240 t = game.get_thing_temp(thing_id)
242 cmd_THING_FACE.argtypes = 'int:pos string'
244 def cmd_THING_HAT(game, thing_id, hat):
245 t = game.get_thing_temp(thing_id)
247 cmd_THING_HAT.argtypes = 'int:pos string'
249 def cmd_THING_CHAR(game, thing_id, c):
250 t = game.get_thing_temp(thing_id)
252 cmd_THING_CHAR.argtypes = 'int:pos char'
254 def cmd_MAP(game, geometry, size, content):
255 map_geometry_class = globals()['MapGeometry' + geometry]
256 game.map_geometry_new = map_geometry_class(size)
257 game.map_content_new = content
258 if type(game.map_geometry_new) == MapGeometrySquare:
259 game.tui.movement_keys = {
260 game.tui.keys['square_move_up']: 'UP',
261 game.tui.keys['square_move_left']: 'LEFT',
262 game.tui.keys['square_move_down']: 'DOWN',
263 game.tui.keys['square_move_right']: 'RIGHT',
265 elif type(game.map_geometry_new) == MapGeometryHex:
266 game.tui.movement_keys = {
267 game.tui.keys['hex_move_upleft']: 'UPLEFT',
268 game.tui.keys['hex_move_upright']: 'UPRIGHT',
269 game.tui.keys['hex_move_right']: 'RIGHT',
270 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
271 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
272 game.tui.keys['hex_move_left']: 'LEFT',
274 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
276 def cmd_FOV(game, content):
277 game.fov_new = content
278 cmd_FOV.argtypes = 'string'
280 def cmd_MAP_CONTROL(game, content):
281 game.map_control_content_new = content
282 cmd_MAP_CONTROL.argtypes = 'string'
284 def cmd_GAME_STATE_COMPLETE(game):
285 game.tui.do_refresh = True
286 game.tui.info_cached = None
287 game.things = game.things_new
288 game.portals = game.portals_new
289 game.annotations = game.annotations_new
290 game.fov = game.fov_new
291 game.map_geometry = game.map_geometry_new
292 game.map_content = game.map_content_new
293 game.map_control_content = game.map_control_content_new
294 game.player = game.get_thing(game.player_id)
295 game.players_hat_chars = game.players_hat_chars_new
296 game.bladder_pressure = game.bladder_pressure_new
297 game.weariness = game.weariness_new
298 game.turn_complete = True
299 if game.tui.mode.name == 'post_login_wait':
300 game.tui.switch_mode('play')
301 cmd_GAME_STATE_COMPLETE.argtypes = ''
303 def cmd_PORTAL(game, position, msg):
304 game.portals_new[position] = msg
305 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
307 def cmd_PLAY_ERROR(game, msg):
308 game.tui.log_msg('? ' + msg)
309 game.tui.flash = True
310 game.tui.do_refresh = True
311 cmd_PLAY_ERROR.argtypes = 'string'
313 def cmd_GAME_ERROR(game, msg):
314 game.tui.log_msg('? game error: ' + msg)
315 game.tui.do_refresh = True
316 cmd_GAME_ERROR.argtypes = 'string'
318 def cmd_ARGUMENT_ERROR(game, msg):
319 game.tui.log_msg('? syntax error: ' + msg)
320 game.tui.do_refresh = True
321 cmd_ARGUMENT_ERROR.argtypes = 'string'
323 def cmd_ANNOTATION(game, position, msg):
324 game.annotations_new[position] = msg
325 if game.tui.mode.shows_info:
326 game.tui.do_refresh = True
327 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
329 def cmd_TASKS(game, tasks_comma_separated):
330 game.tasks = tasks_comma_separated.split(',')
331 game.tui.mode_write.legal = 'WRITE' in game.tasks
332 game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks
333 game.tui.mode_take_thing.legal = 'PICK_UP' in game.tasks
334 game.tui.mode_drop_thing.legal = 'DROP' in game.tasks
335 cmd_TASKS.argtypes = 'string'
337 def cmd_THING_TYPE(game, thing_type, symbol_hint):
338 game.thing_types[thing_type] = symbol_hint
339 cmd_THING_TYPE.argtypes = 'string char'
341 def cmd_THING_INSTALLED(game, thing_id):
342 game.get_thing_temp(thing_id).installed = True
343 cmd_THING_INSTALLED.argtypes = 'int:pos'
345 def cmd_THING_CARRYING(game, thing_id, carried_id):
346 game.get_thing_temp(thing_id).carrying = game.get_thing_temp(carried_id)
347 cmd_THING_CARRYING.argtypes = 'int:pos int:pos'
349 def cmd_TERRAIN(game, terrain_char, terrain_desc):
350 game.terrains[terrain_char] = terrain_desc
351 cmd_TERRAIN.argtypes = 'char string'
355 cmd_PONG.argtypes = ''
357 def cmd_DEFAULT_COLORS(game):
358 game.tui.set_default_colors()
359 cmd_DEFAULT_COLORS.argtypes = ''
361 def cmd_RANDOM_COLORS(game):
362 game.tui.set_random_colors()
363 cmd_RANDOM_COLORS.argtypes = ''
365 def cmd_STATS(game, bladder_pressure, weariness):
366 game.bladder_pressure_new = bladder_pressure
367 game.weariness_new = weariness
368 cmd_STATS.argtypes = 'int:nonneg int:nonneg'
370 class Game(GameBase):
371 turn_complete = False
376 def __init__(self, *args, **kwargs):
377 super().__init__(*args, **kwargs)
378 self.register_command(cmd_LOGIN_OK)
379 self.register_command(cmd_ADMIN_OK)
380 self.register_command(cmd_PONG)
381 self.register_command(cmd_CHAT)
382 self.register_command(cmd_CHATFACE)
383 self.register_command(cmd_REPLY)
384 self.register_command(cmd_PLAYER_ID)
385 self.register_command(cmd_TURN)
386 self.register_command(cmd_OTHER_WIPE)
387 self.register_command(cmd_THING)
388 self.register_command(cmd_THING_TYPE)
389 self.register_command(cmd_THING_NAME)
390 self.register_command(cmd_THING_CHAR)
391 self.register_command(cmd_THING_FACE)
392 self.register_command(cmd_THING_HAT)
393 self.register_command(cmd_THING_CARRYING)
394 self.register_command(cmd_THING_INSTALLED)
395 self.register_command(cmd_TERRAIN)
396 self.register_command(cmd_MAP)
397 self.register_command(cmd_MAP_CONTROL)
398 self.register_command(cmd_PORTAL)
399 self.register_command(cmd_ANNOTATION)
400 self.register_command(cmd_GAME_STATE_COMPLETE)
401 self.register_command(cmd_PLAYERS_HAT_CHARS)
402 self.register_command(cmd_ARGUMENT_ERROR)
403 self.register_command(cmd_GAME_ERROR)
404 self.register_command(cmd_PLAY_ERROR)
405 self.register_command(cmd_TASKS)
406 self.register_command(cmd_FOV)
407 self.register_command(cmd_DEFAULT_COLORS)
408 self.register_command(cmd_RANDOM_COLORS)
409 self.register_command(cmd_STATS)
410 self.map_content = ''
411 self.players_hat_chars = ''
413 self.annotations = {}
414 self.annotations_new = {}
416 self.portals_new = {}
420 def get_string_options(self, string_option_type):
421 if string_option_type == 'map_geometry':
422 return ['Hex', 'Square']
423 elif string_option_type == 'thing_type':
424 return self.thing_types.keys()
427 def get_command(self, command_name):
428 from functools import partial
429 f = partial(self.commands[command_name], self)
430 f.argtypes = self.commands[command_name].argtypes
433 def get_thing_temp(self, id_):
434 for thing in self.things_new:
441 def __init__(self, name, has_input_prompt=False, shows_info=False,
442 is_intro=False, is_single_char_entry=False):
444 self.short_desc = mode_helps[name]['short']
445 self.available_modes = []
446 self.available_actions = []
447 self.has_input_prompt = has_input_prompt
448 self.shows_info = shows_info
449 self.is_intro = is_intro
450 self.help_intro = mode_helps[name]['long']
451 self.intro_msg = mode_helps[name]['intro']
452 self.is_single_char_entry = is_single_char_entry
455 def iter_available_modes(self, tui):
456 for mode_name in self.available_modes:
457 mode = getattr(tui, 'mode_' + mode_name)
460 key = tui.keys['switch_to_' + mode.name]
463 def list_available_modes(self, tui):
465 if len(self.available_modes) > 0:
466 msg = 'Other modes available from here:\n'
467 for mode, key in self.iter_available_modes(tui):
468 msg += '[%s] – %s\n' % (key, mode.short_desc)
471 def mode_switch_on_key(self, tui, key_pressed):
472 for mode, key in self.iter_available_modes(tui):
473 if key_pressed == key:
474 tui.switch_mode(mode.name)
479 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
480 mode_admin = Mode('admin')
481 mode_play = Mode('play')
482 mode_study = Mode('study', shows_info=True)
483 mode_write = Mode('write', is_single_char_entry=True)
484 mode_edit = Mode('edit')
485 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
486 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
487 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
488 mode_control_tile_draw = Mode('control_tile_draw')
489 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
490 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
491 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
492 mode_chat = Mode('chat', has_input_prompt=True)
493 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
494 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
495 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
496 mode_password = Mode('password', has_input_prompt=True)
497 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
498 mode_command_thing = Mode('command_thing', has_input_prompt=True)
499 mode_take_thing = Mode('take_thing', has_input_prompt=True)
500 mode_drop_thing = Mode('drop_thing', has_input_prompt=True)
501 mode_enter_face = Mode('enter_face', has_input_prompt=True)
502 mode_enter_hat = Mode('enter_hat', has_input_prompt=True)
506 def __init__(self, host):
509 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
510 "command_thing", "take_thing",
512 self.mode_play.available_actions = ["move", "teleport", "door", "consume",
513 "install", "wear", "spin"]
514 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
515 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
516 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
517 "control_tile_type", "chat",
518 "study", "play", "edit"]
519 self.mode_admin.available_actions = ["move"]
520 self.mode_control_tile_draw.available_modes = ["admin_enter"]
521 self.mode_control_tile_draw.available_actions = ["move_explorer",
523 self.mode_edit.available_modes = ["write", "annotate", "portal",
524 "name_thing", "enter_face", "enter_hat",
526 "chat", "study", "play", "admin_enter"]
527 self.mode_edit.available_actions = ["move", "flatten", "install",
533 self.parser = Parser(self.game)
535 self.do_refresh = True
536 self.queue = queue.Queue()
537 self.login_name = None
538 self.map_mode = 'terrain + things'
539 self.password = 'foo'
540 self.switch_mode('waiting_for_server')
542 'switch_to_chat': 't',
543 'switch_to_play': 'p',
544 'switch_to_password': 'P',
545 'switch_to_annotate': 'M',
546 'switch_to_portal': 'T',
547 'switch_to_study': '?',
548 'switch_to_edit': 'E',
549 'switch_to_write': 'm',
550 'switch_to_name_thing': 'N',
551 'switch_to_command_thing': 'O',
552 'switch_to_admin_enter': 'A',
553 'switch_to_control_pw_type': 'C',
554 'switch_to_control_tile_type': 'Q',
555 'switch_to_admin_thing_protect': 'T',
557 'switch_to_enter_face': 'f',
558 'switch_to_enter_hat': 'H',
559 'switch_to_take_thing': 'z',
560 'switch_to_drop_thing': 'u',
568 'toggle_map_mode': 'L',
569 'toggle_tile_draw': 'm',
570 'hex_move_upleft': 'w',
571 'hex_move_upright': 'e',
572 'hex_move_right': 'd',
573 'hex_move_downright': 'x',
574 'hex_move_downleft': 'y',
575 'hex_move_left': 'a',
576 'square_move_up': 'w',
577 'square_move_left': 'a',
578 'square_move_down': 's',
579 'square_move_right': 'd',
581 if os.path.isfile('config.json'):
582 with open('config.json', 'r') as f:
583 keys_conf = json.loads(f.read())
585 self.keys[k] = keys_conf[k]
586 self.show_help = False
587 self.disconnected = True
588 self.force_instant_connect = True
589 self.input_lines = []
593 self.ascii_draw_stage = 0
594 self.full_ascii_draw = ''
595 self.offset = YX(0,0)
596 curses.wrapper(self.loop)
600 def handle_recv(msg):
606 self.log_msg('@ attempting connect')
607 socket_client_class = PlomSocketClient
608 if self.host.startswith('ws://') or self.host.startswith('wss://'):
609 socket_client_class = WebSocketClient
611 self.socket = socket_client_class(handle_recv, self.host)
612 self.socket_thread = threading.Thread(target=self.socket.run)
613 self.socket_thread.start()
614 self.disconnected = False
615 self.game.thing_types = {}
616 self.game.terrains = {}
617 time.sleep(0.1) # give potential SSL negotation some time …
618 self.socket.send('TASKS')
619 self.socket.send('TERRAINS')
620 self.socket.send('THING_TYPES')
621 self.switch_mode('login')
622 except ConnectionRefusedError:
623 self.log_msg('@ server connect failure')
624 self.disconnected = True
625 self.switch_mode('waiting_for_server')
626 self.do_refresh = True
629 self.log_msg('@ attempting reconnect')
631 # necessitated by some strange SSL race conditions with ws4py
632 time.sleep(0.1) # FIXME find out why exactly necessary
633 self.switch_mode('waiting_for_server')
638 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
639 raise BrokenSocketConnection
640 self.socket.send(msg)
641 except (BrokenPipeError, BrokenSocketConnection):
642 self.log_msg('@ server disconnected :(')
643 self.disconnected = True
644 self.force_instant_connect = True
645 self.do_refresh = True
647 def log_msg(self, msg):
649 if len(self.log) > 100:
650 self.log = self.log[-100:]
652 def restore_input_values(self):
653 if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
654 self.input_ = self.game.annotations[self.explorer]
655 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
656 self.input_ = self.game.portals[self.explorer]
657 elif self.mode.name == 'password':
658 self.input_ = self.password
659 elif self.mode.name == 'name_thing':
660 if hasattr(self.game.player.carrying, 'name'):
661 self.input_ = self.game.player.carrying.name
662 elif self.mode.name == 'admin_thing_protect':
663 if hasattr(self.game.player.carrying, 'protection'):
664 self.input_ = self.game.player.carrying.protection
665 elif self.mode.name in {'enter_face', 'enter_hat'}:
666 start = self.ascii_draw_stage * 6
667 end = (self.ascii_draw_stage + 1) * 6
668 if self.mode.name == 'enter_face':
669 self.input_ = self.game.player.face[start:end]
670 elif self.mode.name == 'enter_hat':
671 self.input_ = self.game.player.hat[start:end]
673 def send_tile_control_command(self):
674 self.send('SET_TILE_CONTROL %s %s' %
675 (self.explorer, quote(self.tile_control_char)))
677 def toggle_map_mode(self):
678 if self.map_mode == 'terrain only':
679 self.map_mode = 'terrain + annotations'
680 elif self.map_mode == 'terrain + annotations':
681 self.map_mode = 'terrain + things'
682 elif self.map_mode == 'terrain + things':
683 self.map_mode = 'protections'
684 elif self.map_mode == 'protections':
685 self.map_mode = 'terrain only'
687 def switch_mode(self, mode_name):
689 def fail(msg, return_mode='play'):
690 self.log_msg('? ' + msg)
692 self.switch_mode(return_mode)
694 if self.mode and self.mode.name == 'control_tile_draw':
695 self.log_msg('@ finished tile protection drawing.')
696 self.draw_face = False
697 self.tile_draw = False
698 if mode_name == 'command_thing' and\
699 (not self.game.player.carrying or
700 not self.game.player.carrying.commandable):
701 return fail('not carrying anything commandable')
702 if mode_name == 'name_thing' and not self.game.player.carrying:
703 return fail('not carrying anything to re-name')
704 if mode_name == 'admin_thing_protect' and not self.game.player.carrying:
705 return fail('not carrying anything to protect')
706 if mode_name == 'take_thing' and self.game.player.carrying:
707 return fail('already carrying something')
708 if mode_name == 'drop_thing' and not self.game.player.carrying:
709 return fail('not carrying anything droppable')
710 if mode_name == 'enter_hat' and not hasattr(self.game.player, 'hat'):
711 return fail('not wearing hat to edit', 'edit')
712 if mode_name == 'admin_enter' and self.is_admin:
714 self.mode = getattr(self, 'mode_' + mode_name)
715 if self.mode.name in {'control_tile_draw', 'control_tile_type',
717 self.map_mode = 'protections'
718 elif self.mode.name != 'edit':
719 self.map_mode = 'terrain + things'
720 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
721 self.explorer = YX(self.game.player.position.y,
722 self.game.player.position.x)
723 if self.mode.is_single_char_entry:
724 self.show_help = True
725 if len(self.mode.intro_msg) > 0:
726 self.log_msg(self.mode.intro_msg)
727 if self.mode.name == 'login':
729 self.send('LOGIN ' + quote(self.login_name))
731 self.log_msg('@ enter username')
732 elif self.mode.name == 'take_thing':
733 self.log_msg('Portable things in reach for pick-up:')
735 'HERE': YX(0, 0), 'LEFT': YX(0, -1), 'RIGHT': YX(0, 1)
737 if type(self.game.map_geometry) == MapGeometrySquare:
738 directed_moves['UP'] = YX(-1, 0)
739 directed_moves['DOWN'] = YX(1, 0)
740 elif type(self.game.map_geometry) == MapGeometryHex:
741 if self.game.player.position.y % 2:
742 directed_moves['UPLEFT'] = YX(-1, 0)
743 directed_moves['UPRIGHT'] = YX(-1, 1)
744 directed_moves['DOWNLEFT'] = YX(1, 0)
745 directed_moves['DOWNRIGHT'] = YX(1, 1)
747 directed_moves['UPLEFT'] = YX(-1, -1)
748 directed_moves['UPRIGHT'] = YX(-1, 0)
749 directed_moves['DOWNLEFT'] = YX(1, -1)
750 directed_moves['DOWNRIGHT'] = YX(1, 0)
752 for direction in directed_moves:
753 move = directed_moves[direction]
754 select_range[direction] = self.game.player.position + move
755 self.selectables = []
757 for direction in select_range:
758 for t in [t for t in self.game.things
759 if t.portable and t.position == select_range[direction]]:
760 self.selectables += [t.id_]
761 directions += [direction]
762 if len(self.selectables) == 0:
763 return fail('nothing to pick-up')
765 for i in range(len(self.selectables)):
766 t = self.game.get_thing(self.selectables[i])
767 self.log_msg('%s %s: %s' % (i, directions[i],
768 self.get_thing_info(t)))
769 elif self.mode.name == 'drop_thing':
770 self.log_msg('Direction to drop thing to:')
772 ['HERE'] + list(self.game.tui.movement_keys.values())
773 for i in range(len(self.selectables)):
774 self.log_msg(str(i) + ': ' + self.selectables[i])
775 elif self.mode.name == 'enter_hat':
776 self.log_msg('legal characters: ' + self.game.players_hat_chars)
777 elif self.mode.name == 'command_thing':
778 self.send('TASK:COMMAND ' + quote('HELP'))
779 elif self.mode.name == 'control_pw_pw':
780 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
781 elif self.mode.name == 'control_tile_draw':
782 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']))
784 self.restore_input_values()
786 def set_default_colors(self):
787 curses.init_color(1, 1000, 1000, 1000)
788 curses.init_color(2, 0, 0, 0)
789 self.do_refresh = True
791 def set_random_colors(self):
795 return int(offset + random.random()*375)
797 curses.init_color(1, rand(625), rand(625), rand(625))
798 curses.init_color(2, rand(0), rand(0), rand(0))
799 self.do_refresh = True
803 return self.info_cached
804 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
806 if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
807 info_to_cache += 'outside field of view'
809 for t in self.game.things:
810 if t.position == self.explorer:
811 info_to_cache += 'THING: %s' % self.get_thing_info(t)
812 protection = t.protection
813 if protection == '.':
815 info_to_cache += ' / protection: %s\n' % protection
816 if hasattr(t, 'hat'):
817 info_to_cache += t.hat[0:6] + '\n'
818 info_to_cache += t.hat[6:12] + '\n'
819 info_to_cache += t.hat[12:18] + '\n'
820 if hasattr(t, 'face'):
821 info_to_cache += t.face[0:6] + '\n'
822 info_to_cache += t.face[6:12] + '\n'
823 info_to_cache += t.face[12:18] + '\n'
824 terrain_char = self.game.map_content[pos_i]
826 if terrain_char in self.game.terrains:
827 terrain_desc = self.game.terrains[terrain_char]
828 info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
830 protection = self.game.map_control_content[pos_i]
831 if protection == '.':
832 protection = 'unprotected'
833 info_to_cache += 'PROTECTION: %s\n' % protection
834 if self.explorer in self.game.portals:
835 info_to_cache += 'PORTAL: ' +\
836 self.game.portals[self.explorer] + '\n'
838 info_to_cache += 'PORTAL: (none)\n'
839 if self.explorer in self.game.annotations:
840 info_to_cache += 'ANNOTATION: ' +\
841 self.game.annotations[self.explorer]
842 self.info_cached = info_to_cache
843 return self.info_cached
845 def get_thing_info(self, t):
847 (t.type_, self.game.thing_types[t.type_])
848 if hasattr(t, 'thing_char'):
850 if hasattr(t, 'name'):
851 info += ' (%s)' % t.name
852 if hasattr(t, 'installed'):
853 info += ' / installed'
856 def loop(self, stdscr):
859 def safe_addstr(y, x, line):
860 if y < self.size.y - 1 or x + len(line) < self.size.x:
861 stdscr.addstr(y, x, line, curses.color_pair(1))
862 else: # workaround to <https://stackoverflow.com/q/7063128>
863 cut_i = self.size.x - x - 1
865 last_char = line[cut_i]
866 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
867 stdscr.insstr(y, self.size.x - 2, ' ')
868 stdscr.addstr(y, x, cut, curses.color_pair(1))
870 def handle_input(msg):
871 command, args = self.parser.parse(msg)
874 def task_action_on(action):
875 return action_tasks[action] in self.game.tasks
877 def msg_into_lines_of_width(msg, width):
881 for i in range(len(msg)):
882 if x >= width or msg[i] == "\n":
894 def reset_screen_size():
895 self.size = YX(*stdscr.getmaxyx())
896 self.size = self.size - YX(self.size.y % 4, 0)
897 self.size = self.size - YX(0, self.size.x % 4)
898 self.window_width = int(self.size.x / 2)
900 def recalc_input_lines():
901 if not self.mode.has_input_prompt:
902 self.input_lines = []
904 self.input_lines = msg_into_lines_of_width(input_prompt
908 def move_explorer(direction):
909 target = self.game.map_geometry.move_yx(self.explorer, direction)
911 self.info_cached = None
912 self.explorer = target
914 self.send_tile_control_command()
920 for line in self.log:
921 lines += msg_into_lines_of_width(line, self.window_width)
924 max_y = self.size.y - len(self.input_lines)
925 for i in range(len(lines)):
926 if (i >= max_y - height_header):
928 safe_addstr(max_y - i - 1, self.window_width, lines[i])
931 info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
932 lines = msg_into_lines_of_width(info, self.window_width)
934 for i in range(len(lines)):
935 y = height_header + i
936 if y >= self.size.y - len(self.input_lines):
938 safe_addstr(y, self.window_width, lines[i])
941 y = self.size.y - len(self.input_lines)
942 for i in range(len(self.input_lines)):
943 safe_addstr(y, self.window_width, self.input_lines[i])
947 stats = 'WEARY: %s BLADDER: %s' % (self.game.weariness,
948 self.game.bladder_pressure)
949 safe_addstr(0, self.window_width, stats)
952 help = "hit [%s] for help" % self.keys['help']
953 if self.mode.has_input_prompt:
954 help = "enter /help for help"
955 safe_addstr(1, self.window_width,
956 'MODE: %s – %s' % (self.mode.short_desc, help))
959 if (not self.game.turn_complete) and len(self.map_lines) == 0:
961 if self.game.turn_complete:
963 for y in range(self.game.map_geometry.size.y):
964 start = self.game.map_geometry.size.x * y
965 end = start + self.game.map_geometry.size.x
966 if self.map_mode == 'protections':
967 map_lines_split += [[c + ' ' for c
968 in self.game.map_control_content[start:end]]]
970 map_lines_split += [[c + ' ' for c
971 in self.game.map_content[start:end]]]
972 if self.map_mode == 'terrain + annotations':
973 for p in self.game.annotations:
974 map_lines_split[p.y][p.x] = 'A '
975 elif self.map_mode == 'terrain + things':
976 for p in self.game.portals.keys():
977 original = map_lines_split[p.y][p.x]
978 map_lines_split[p.y][p.x] = original[0] + 'P'
981 def draw_thing(t, used_positions):
982 symbol = self.game.thing_types[t.type_]
984 if hasattr(t, 'thing_char'):
985 meta_char = t.thing_char
986 if t.position in used_positions:
988 if hasattr(t, 'carrying') and t.carrying:
990 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
991 used_positions += [t.position]
993 for t in [t for t in self.game.things if t.type_ != 'Player']:
994 draw_thing(t, used_positions)
995 for t in [t for t in self.game.things if t.type_ == 'Player']:
996 draw_thing(t, used_positions)
997 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
998 map_lines_split[self.explorer.y][self.explorer.x] = '??'
999 elif self.map_mode != 'terrain + things':
1000 map_lines_split[self.game.player.position.y]\
1001 [self.game.player.position.x] = '??'
1003 if type(self.game.map_geometry) == MapGeometryHex:
1005 for line in map_lines_split:
1006 self.map_lines += [indent * ' ' + ''.join(line)]
1007 indent = 0 if indent else 1
1009 for line in map_lines_split:
1010 self.map_lines += [''.join(line)]
1011 window_center = YX(int(self.size.y / 2),
1012 int(self.window_width / 2))
1013 center = self.game.player.position
1014 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
1015 center = self.explorer
1016 center = YX(center.y, center.x * 2)
1017 self.offset = center - window_center
1018 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
1019 self.offset += YX(0, 1)
1020 term_y = max(0, -self.offset.y)
1021 term_x = max(0, -self.offset.x)
1022 map_y = max(0, self.offset.y)
1023 map_x = max(0, self.offset.x)
1024 while term_y < self.size.y and map_y < len(self.map_lines):
1025 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
1026 safe_addstr(term_y, term_x, to_draw)
1030 def draw_face_popup():
1031 t = self.game.get_thing(self.draw_face)
1032 if not t or not hasattr(t, 'face'):
1033 self.draw_face = False
1036 start_x = self.window_width - 10
1038 if hasattr(t, 'thing_char'):
1039 t_char = t.thing_char
1040 def draw_body_part(body_part, end_y):
1041 safe_addstr(end_y - 4, start_x, ' _[ @' + t_char + ' ]_ ')
1042 safe_addstr(end_y - 3, start_x, '| |')
1043 safe_addstr(end_y - 2, start_x, '| ' + body_part[0:6] + ' |')
1044 safe_addstr(end_y - 1, start_x, '| ' + body_part[6:12] + ' |')
1045 safe_addstr(end_y, start_x, '| ' + body_part[12:18] + ' |')
1047 if hasattr(t, 'face'):
1048 draw_body_part(t.face, self.size.y - 2)
1049 if hasattr(t, 'hat'):
1050 draw_body_part(t.hat, self.size.y - 5)
1051 safe_addstr(self.size.y - 1, start_x, '| |')
1054 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
1055 self.mode.help_intro)
1056 if len(self.mode.available_actions) > 0:
1057 content += "Available actions:\n"
1058 for action in self.mode.available_actions:
1059 if action in action_tasks:
1060 if action_tasks[action] not in self.game.tasks:
1062 if action == 'move_explorer':
1064 if action == 'move':
1065 key = ','.join(self.movement_keys)
1067 key = self.keys[action]
1068 content += '[%s] – %s\n' % (key, action_descriptions[action])
1070 content += self.mode.list_available_modes(self)
1071 for i in range(self.size.y):
1073 self.window_width * (not self.mode.has_input_prompt),
1074 ' ' * self.window_width)
1076 for line in content.split('\n'):
1077 lines += msg_into_lines_of_width(line, self.window_width)
1078 for i in range(len(lines)):
1079 if i >= self.size.y:
1082 self.window_width * (not self.mode.has_input_prompt),
1087 stdscr.bkgd(' ', curses.color_pair(1))
1088 recalc_input_lines()
1089 if self.mode.has_input_prompt:
1091 if self.mode.shows_info:
1096 if not self.mode.is_intro:
1101 if self.draw_face and self.mode.name in {'chat', 'play'}:
1104 def pick_selectable(task_name):
1106 i = int(self.input_)
1107 if i < 0 or i >= len(self.selectables):
1108 self.log_msg('? invalid index, aborted')
1110 self.send('TASK:%s %s' % (task_name, self.selectables[i]))
1112 self.log_msg('? invalid index, aborted')
1114 self.switch_mode('play')
1116 def enter_ascii_art(command):
1117 if len(self.input_) != 6:
1118 self.log_msg('? wrong input length, must be 6; try again')
1120 self.log_msg(' ' + self.input_)
1121 self.full_ascii_draw += self.input_
1122 self.ascii_draw_stage += 1
1123 if self.ascii_draw_stage < 3:
1124 self.restore_input_values()
1126 self.send('%s %s' % (command, quote(self.full_ascii_draw)))
1127 self.full_ascii_draw = ""
1128 self.ascii_draw_stage = 0
1130 self.switch_mode('edit')
1132 action_descriptions = {
1134 'flatten': 'flatten surroundings',
1135 'teleport': 'teleport',
1136 'take_thing': 'pick up thing',
1137 'drop_thing': 'drop thing',
1138 'toggle_map_mode': 'toggle map view',
1139 'toggle_tile_draw': 'toggle protection character drawing',
1140 'install': '(un-)install',
1141 'wear': '(un-)wear',
1142 'door': 'open/close',
1143 'consume': 'consume',
1148 'flatten': 'FLATTEN_SURROUNDINGS',
1149 'take_thing': 'PICK_UP',
1150 'drop_thing': 'DROP',
1152 'install': 'INSTALL',
1155 'command': 'COMMAND',
1156 'consume': 'INTOXICATE',
1160 curses.curs_set(False) # hide cursor
1161 curses.start_color()
1162 self.set_default_colors()
1163 curses.init_pair(1, 1, 2)
1166 self.explorer = YX(0, 0)
1169 interval = datetime.timedelta(seconds=5)
1170 last_ping = datetime.datetime.now() - interval
1172 if self.disconnected and self.force_instant_connect:
1173 self.force_instant_connect = False
1175 now = datetime.datetime.now()
1176 if now - last_ping > interval:
1177 if self.disconnected:
1187 self.do_refresh = False
1190 msg = self.queue.get(block=False)
1195 key = stdscr.getkey()
1196 self.do_refresh = True
1197 except curses.error:
1202 self.show_help = False
1203 self.draw_face = False
1204 if key == 'KEY_RESIZE':
1206 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1207 self.input_ = self.input_[:-1]
1208 elif (((not self.mode.is_intro) and keycode == 27) # Escape
1209 or (self.mode.has_input_prompt and key == '\n'
1210 and self.input_ == ''\
1211 and self.mode.name in {'chat', 'command_thing',
1212 'take_thing', 'drop_thing',
1214 if self.mode.name not in {'chat', 'play', 'study', 'edit'}:
1215 self.log_msg('@ aborted')
1216 self.switch_mode('play')
1217 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1218 self.show_help = True
1220 self.restore_input_values()
1221 elif self.mode.has_input_prompt and key != '\n': # Return key
1223 max_length = self.window_width * self.size.y - len(input_prompt) - 1
1224 if len(self.input_) > max_length:
1225 self.input_ = self.input_[:max_length]
1226 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1227 self.show_help = True
1228 elif self.mode.name == 'login' and key == '\n':
1229 self.login_name = self.input_
1230 self.send('LOGIN ' + quote(self.input_))
1232 elif self.mode.name == 'enter_face' and key == '\n':
1233 enter_ascii_art('PLAYER_FACE')
1234 elif self.mode.name == 'enter_hat' and key == '\n':
1235 enter_ascii_art('PLAYER_HAT')
1236 elif self.mode.name == 'take_thing' and key == '\n':
1237 pick_selectable('PICK_UP')
1238 elif self.mode.name == 'drop_thing' and key == '\n':
1239 pick_selectable('DROP')
1240 elif self.mode.name == 'command_thing' and key == '\n':
1241 self.send('TASK:COMMAND ' + quote(self.input_))
1243 elif self.mode.name == 'control_pw_pw' and key == '\n':
1244 if self.input_ == '':
1245 self.log_msg('@ aborted')
1247 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1248 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1249 self.switch_mode('admin')
1250 elif self.mode.name == 'password' and key == '\n':
1251 if self.input_ == '':
1253 self.password = self.input_
1254 self.switch_mode('edit')
1255 elif self.mode.name == 'admin_enter' and key == '\n':
1256 self.send('BECOME_ADMIN ' + quote(self.input_))
1257 self.switch_mode('play')
1258 elif self.mode.name == 'control_pw_type' and key == '\n':
1259 if len(self.input_) != 1:
1260 self.log_msg('@ entered non-single-char, therefore aborted')
1261 self.switch_mode('admin')
1263 self.tile_control_char = self.input_
1264 self.switch_mode('control_pw_pw')
1265 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1266 if len(self.input_) != 1:
1267 self.log_msg('@ entered non-single-char, therefore aborted')
1269 self.send('THING_PROTECTION %s' % (quote(self.input_)))
1270 self.log_msg('@ sent new protection character for thing')
1271 self.switch_mode('admin')
1272 elif self.mode.name == 'control_tile_type' and key == '\n':
1273 if len(self.input_) != 1:
1274 self.log_msg('@ entered non-single-char, therefore aborted')
1275 self.switch_mode('admin')
1277 self.tile_control_char = self.input_
1278 self.switch_mode('control_tile_draw')
1279 elif self.mode.name == 'chat' and key == '\n':
1280 if self.input_ == '':
1282 if self.input_[0] == '/':
1283 if self.input_.startswith('/nick'):
1284 tokens = self.input_.split(maxsplit=1)
1285 if len(tokens) == 2:
1286 self.send('NICK ' + quote(tokens[1]))
1288 self.log_msg('? need login name')
1290 self.log_msg('? unknown command')
1292 self.send('ALL ' + quote(self.input_))
1294 elif self.mode.name == 'name_thing' and key == '\n':
1295 if self.input_ == '':
1297 self.send('THING_NAME %s %s' % (quote(self.input_),
1298 quote(self.password)))
1299 self.switch_mode('edit')
1300 elif self.mode.name == 'annotate' and key == '\n':
1301 if self.input_ == '':
1303 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1304 quote(self.password)))
1305 self.switch_mode('edit')
1306 elif self.mode.name == 'portal' and key == '\n':
1307 if self.input_ == '':
1309 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1310 quote(self.password)))
1311 self.switch_mode('edit')
1312 elif self.mode.name == 'study':
1313 if self.mode.mode_switch_on_key(self, key):
1315 elif key == self.keys['toggle_map_mode']:
1316 self.toggle_map_mode()
1317 elif key in self.movement_keys:
1318 move_explorer(self.movement_keys[key])
1319 elif self.mode.name == 'play':
1320 if self.mode.mode_switch_on_key(self, key):
1322 elif key == self.keys['door'] and task_action_on('door'):
1323 self.send('TASK:DOOR')
1324 elif key == self.keys['consume'] and task_action_on('consume'):
1325 self.send('TASK:INTOXICATE')
1326 elif key == self.keys['wear'] and task_action_on('wear'):
1327 self.send('TASK:WEAR')
1328 elif key == self.keys['spin'] and task_action_on('spin'):
1329 self.send('TASK:SPIN')
1330 elif key == self.keys['teleport']:
1331 if self.game.player.position in self.game.portals:
1332 self.host = self.game.portals[self.game.player.position]
1336 self.log_msg('? not standing on portal')
1337 elif key in self.movement_keys and task_action_on('move'):
1338 self.send('TASK:MOVE ' + self.movement_keys[key])
1339 elif self.mode.name == 'write':
1340 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1341 self.switch_mode('edit')
1342 elif self.mode.name == 'control_tile_draw':
1343 if self.mode.mode_switch_on_key(self, key):
1345 elif key in self.movement_keys:
1346 move_explorer(self.movement_keys[key])
1347 elif key == self.keys['toggle_tile_draw']:
1348 self.tile_draw = False if self.tile_draw else True
1349 elif self.mode.name == 'admin':
1350 if self.mode.mode_switch_on_key(self, key):
1352 elif key in self.movement_keys and task_action_on('move'):
1353 self.send('TASK:MOVE ' + self.movement_keys[key])
1354 elif self.mode.name == 'edit':
1355 if self.mode.mode_switch_on_key(self, key):
1357 elif key == self.keys['flatten'] and task_action_on('flatten'):
1358 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1359 elif key == self.keys['install'] and task_action_on('install'):
1360 self.send('TASK:INSTALL %s' % quote(self.password))
1361 elif key == self.keys['toggle_map_mode']:
1362 self.toggle_map_mode()
1363 elif key in self.movement_keys and task_action_on('move'):
1364 self.send('TASK:MOVE ' + self.movement_keys[key])
1366 if len(sys.argv) != 2:
1367 raise ArgError('wrong number of arguments, need game host')