X-Git-Url: https://plomlompom.com/repos/?a=blobdiff_plain;f=new2%2Frogue_chat_curses.py;h=6d0f8b77f8518f16b224c0afb761438793a63180;hb=e4b04a5ba87c262b7f01a52ce6e25001b8a53506;hp=bdafe21dcbd7c06972be7c43e98141f9b9f8e3fc;hpb=e6a78872ee29c0e893a01d06b7b467e339bc80ec;p=plomrogue2-experiments diff --git a/new2/rogue_chat_curses.py b/new2/rogue_chat_curses.py index bdafe21..6d0f8b7 100755 --- a/new2/rogue_chat_curses.py +++ b/new2/rogue_chat_curses.py @@ -4,116 +4,504 @@ import socket import queue import threading from plomrogue.io_tcp import PlomSocket +from plomrogue.game import GameBase +from plomrogue.parser import Parser +from plomrogue.mapping import YX +from plomrogue.things import ThingBase +from plomrogue.misc import quote -def recv_loop(plom_socket, q): - for msg in plom_socket.recv(): - q.put(msg) +def cmd_TURN(game, n): + game.turn = n + game.things = [] + game.portals = {} + game.turn_complete = False +cmd_TURN.argtypes = 'int:nonneg' + +def cmd_LOGIN_OK(game): + game.tui.switch_mode('post_login_wait') + game.tui.send('GET_GAMESTATE') + game.tui.log_msg('@ welcome') +cmd_LOGIN_OK.argtypes = '' + +def cmd_CHAT(game, msg): + game.tui.log_msg('# ' + msg) + game.tui.do_refresh = True +cmd_CHAT.argtypes = 'string' + +def cmd_PLAYER_ID(game, player_id): + game.player_id = player_id +cmd_PLAYER_ID.argtypes = 'int:nonneg' + +def cmd_THING_POS(game, thing_id, position): + t = game.get_thing(thing_id, True) + t.position = position +cmd_THING_POS.argtypes = 'int:nonneg yx_tuple:nonneg' + +def cmd_THING_NAME(game, thing_id, name): + t = game.get_thing(thing_id, True) + t.name = name +cmd_THING_NAME.argtypes = 'int:nonneg string' + +def cmd_MAP(game, size, content): + game.map_geometry.size = size + game.map_content = content +cmd_MAP.argtypes = 'yx_tuple:pos string' + +def cmd_GAME_STATE_COMPLETE(game): + game.info_db = {} + if game.tui.mode.name == 'post_login_wait': + game.tui.switch_mode('play') + game.tui.help() + if game.tui.mode.shows_info: + game.tui.query_info() + player = game.get_thing(game.player_id, False) + if player.position in game.portals: + host, port = game.portals[player.position].split(':') + game.tui.teleport_target_host = host + game.tui.teleport_target_port = port + game.tui.switch_mode('teleport') + game.turn_complete = True + game.tui.do_refresh = True +cmd_GAME_STATE_COMPLETE.argtypes = '' + +def cmd_PORTAL(game, position, msg): + game.portals[position] = msg +cmd_PORTAL.argtypes = 'yx_tuple:nonneg string' + +def cmd_PLAY_ERROR(game, msg): + game.tui.flash() + game.tui.do_refresh = True +cmd_PLAY_ERROR.argtypes = 'string' + +def cmd_GAME_ERROR(game, msg): + game.tui.log_msg('? game error: ' + msg) + game.tui.do_refresh = True +cmd_GAME_ERROR.argtypes = 'string' + +def cmd_ARGUMENT_ERROR(game, msg): + game.tui.log_msg('? syntax error: ' + msg) + game.tui.do_refresh = True +cmd_ARGUMENT_ERROR.argtypes = 'string' + +def cmd_ANNOTATION(game, position, msg): + game.info_db[position] = msg + if game.tui.mode.shows_info: + game.tui.do_refresh = True +cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string' + +class Game(GameBase): + commands = {'LOGIN_OK': cmd_LOGIN_OK, + 'CHAT': cmd_CHAT, + 'PLAYER_ID': cmd_PLAYER_ID, + 'TURN': cmd_TURN, + 'THING_POS': cmd_THING_POS, + 'THING_NAME': cmd_THING_NAME, + 'MAP': cmd_MAP, + 'PORTAL': cmd_PORTAL, + 'ANNOTATION': cmd_ANNOTATION, + 'GAME_STATE_COMPLETE': cmd_GAME_STATE_COMPLETE, + 'ARGUMENT_ERROR': cmd_ARGUMENT_ERROR, + 'GAME_ERROR': cmd_GAME_ERROR, + 'PLAY_ERROR': cmd_PLAY_ERROR} + thing_type = ThingBase + turn_complete = False + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.map_content = '' + self.player_id = -1 + self.info_db = {} + self.portals = {} + + def get_command(self, command_name): + from functools import partial + f = partial(self.commands[command_name], self) + f.argtypes = self.commands[command_name].argtypes + return f class TUI: - def __init__(self, socket, q): - self.queue = q - self.socket = socket + class Mode: + + def __init__(self, name, has_input_prompt=False, shows_info=False, + is_intro = False): + self.name = name + self.has_input_prompt = has_input_prompt + self.shows_info = shows_info + self.is_intro = is_intro + + def __init__(self, host, port): + self.host = host + self.port = port + self.mode_play = self.Mode('play') + self.mode_study = self.Mode('study', shows_info=True) + self.mode_edit = self.Mode('edit') + self.mode_annotate = self.Mode('annotate', has_input_prompt=True, shows_info=True) + self.mode_portal = self.Mode('portal', has_input_prompt=True, shows_info=True) + self.mode_chat = self.Mode('chat', has_input_prompt=True) + self.mode_waiting_for_server = self.Mode('waiting_for_server', is_intro=True) + self.mode_login = self.Mode('login', has_input_prompt=True, is_intro=True) + self.mode_post_login_wait = self.Mode('post_login_wait', is_intro=True) + self.mode_teleport = self.Mode('teleport', has_input_prompt=True) + self.game = Game() + self.game.tui = self + self.parser = Parser(self.game) self.log = [] - self.log_msg("hallo") - self.log_msg("welt") self.do_refresh = True + self.queue = queue.Queue() + self.switch_mode('waiting_for_server') curses.wrapper(self.loop) + def flash(self): + curses.flash() + + def send(self, msg): + try: + self.socket.send(msg) + except BrokenPipeError: + self.log_msg('@ server disconnected :(') + self.do_refresh = True + def log_msg(self, msg): self.log += [msg] if len(self.log) > 100: self.log = self.log[-100:] + def query_info(self): + self.send('GET_ANNOTATION ' + str(self.explorer)) + + def switch_mode(self, mode_name, keep_position = False): + self.mode = getattr(self, 'mode_' + mode_name) + if self.mode.shows_info and not keep_position: + player = self.game.get_thing(self.game.player_id, False) + self.explorer = YX(player.position.y, player.position.x) + if self.mode.name == 'waiting_for_server': + self.log_msg('@ waiting for server …') + elif self.mode.name == 'login': + self.log_msg('@ enter username') + elif self.mode.name == 'teleport': + self.log_msg("@ May teleport to %s:%s" % (self.teleport_target_host, + self.teleport_target_port)); + self.log_msg("@ Enter 'YES!' to affirm."); + elif self.mode.name == 'annotate' and self.explorer in self.game.info_db: + info = self.game.info_db[self.explorer] + if info != '(none)': + self.input_ = info + elif self.mode.name == 'portal' and self.explorer in self.game.portals: + self.input_ = self.game.portals[self.explorer] + + def help(self): + self.log_msg("HELP:"); + self.log_msg("chat mode commands:"); + self.log_msg(" :nick NAME - re-name yourself to NAME"); + self.log_msg(" :msg USER TEXT - send TEXT to USER"); + self.log_msg(" :help - show this help"); + self.log_msg(" :p or :play - switch to play mode"); + self.log_msg(" :? or :study - switch to study mode"); + self.log_msg("commands common to study and play mode:"); + self.log_msg(" w,a,s,d - move"); + self.log_msg(" c - switch to chat mode"); + self.log_msg("commands specific to play mode:"); + self.log_msg(" e - write following ASCII character"); + self.log_msg(" f - flatten surroundings"); + self.log_msg(" ? - switch to study mode"); + self.log_msg("commands specific to study mode:"); + self.log_msg(" e - annotate terrain"); + self.log_msg(" p - switch to play mode"); + def loop(self, stdscr): + def safe_addstr(y, x, line): + if y < self.size.y - 1 or x + len(line) < self.size.x: + stdscr.addstr(y, x, line) + else: # workaround to + cut_i = self.size.x - x - 1 + cut = line[:cut_i] + last_char = line[cut_i] + stdscr.addstr(y, self.size.x - 2, last_char) + stdscr.insstr(y, self.size.x - 2, ' ') + stdscr.addstr(y, x, cut) + + def connect(): + import time + + def recv_loop(): + for msg in self.socket.recv(): + if msg == 'BYE': + break + self.queue.put(msg) + + while True: + try: + s = socket.create_connection((self.host, self.port)) + self.socket = PlomSocket(s) + self.socket_thread = threading.Thread(target=recv_loop) + self.socket_thread.start() + self.switch_mode('login') + return + except ConnectionRefusedError: + self.log_msg('@ server connect failure, trying again …') + draw_screen() + stdscr.refresh() + time.sleep(1) + + def reconnect(): + self.send('QUIT') + self.switch_mode('waiting_for_server') + connect() + + def handle_input(msg): + command, args = self.parser.parse(msg) + command(*args) + def msg_into_lines_of_width(msg, width): chunk = '' lines = [] x = 0 for i in range(len(msg)): - x += 1 if x >= width or msg[i] == "\n": lines += [chunk] chunk = '' x = 0 if msg[i] != "\n": chunk += msg[i] + x += 1 lines += [chunk] return lines def reset_screen_size(): - self.rows, self.cols = stdscr.getmaxyx() - self.cols -= 1 # ugly TODO ncurses bug workaround, FIXME - self.window_width = self.cols // 2 + self.size = YX(*stdscr.getmaxyx()) + self.size = self.size - YX(self.size.y % 2, 0) + self.size = self.size - YX(0, self.size.x % 4) + self.window_width = int(self.size.x / 2) def recalc_input_lines(): - self.input_lines = msg_into_lines_of_width(input_prompt +input_, - self.window_width) + if not self.mode.has_input_prompt: + self.input_lines = [] + else: + self.input_lines = msg_into_lines_of_width(input_prompt + self.input_, + self.window_width) + + def move_explorer(direction): + target = self.game.map_geometry.move(self.explorer, direction) + if target: + self.explorer = target + self.query_info() + else: + self.flash() def draw_history(): lines = [] for line in self.log: lines += msg_into_lines_of_width(line, self.window_width) lines.reverse() - max_y = self.rows - len(self.input_lines) + height_header = 2 + max_y = self.size.y - len(self.input_lines) for i in range(len(lines)): - if (i >= max_y): + if (i >= max_y - height_header): break - stdscr.addstr(max_y - i - 1, self.window_width, lines[i]) + safe_addstr(max_y - i - 1, self.window_width, lines[i]) + + def draw_info(): + if not self.game.turn_complete: + return + if self.explorer in self.game.portals: + info = 'PORTAL: ' + self.game.portals[self.explorer] + '\n' + else: + info = 'PORTAL: (none)\n' + if self.explorer in self.game.info_db: + info += 'ANNOTATION: ' + self.game.info_db[self.explorer] + else: + info += 'ANNOTATION: waiting …' + lines = msg_into_lines_of_width(info, self.window_width) + height_header = 2 + for i in range(len(lines)): + y = height_header + i + if y >= self.size.y - len(self.input_lines): + break + safe_addstr(y, self.window_width, lines[i]) def draw_input(): - y = self.rows - len(self.input_lines) + y = self.size.y - len(self.input_lines) for i in range(len(self.input_lines)): - stdscr.addstr(y, self.window_width, self.input_lines[i]) + safe_addstr(y, self.window_width, self.input_lines[i]) y += 1 + def draw_turn(): + if not self.game.turn_complete: + return + safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn)) + + def draw_mode(): + safe_addstr(1, self.window_width, 'MODE: ' + self.mode.name) + + def draw_map(): + if not self.game.turn_complete: + return + map_lines_split = [] + for y in range(self.game.map_geometry.size.y): + start = self.game.map_geometry.size.x * y + end = start + self.game.map_geometry.size.x + map_lines_split += [list(self.game.map_content[start:end])] + for t in self.game.things: + map_lines_split[t.position.y][t.position.x] = '@' + if self.mode.shows_info: + map_lines_split[self.explorer.y][self.explorer.x] = '?' + map_lines = [] + for line in map_lines_split: + map_lines += [''.join(line)] + map_center = YX(int(self.game.map_geometry.size.y / 2), + int(self.game.map_geometry.size.x / 2)) + window_center = YX(int(self.size.y / 2), + int(self.window_width / 2)) + player = self.game.get_thing(self.game.player_id, False) + center = player.position + if self.mode.shows_info: + center = self.explorer + offset = center - window_center + term_y = max(0, -offset.y) + term_x = max(0, -offset.x) + map_y = max(0, offset.y) + map_x = max(0, offset.x) + while (term_y < self.size.y and map_y < self.game.map_geometry.size.y): + to_draw = map_lines[map_y][map_x:self.window_width + offset.x] + safe_addstr(term_y, term_x, to_draw) + term_y += 1 + map_y += 1 + + def draw_screen(): + stdscr.clear() + recalc_input_lines() + if self.mode.has_input_prompt: + draw_input() + if self.mode.shows_info: + draw_info() + else: + draw_history() + draw_mode() + if not self.mode.is_intro: + draw_turn() + draw_map() + curses.curs_set(False) # hide cursor stdscr.timeout(10) reset_screen_size() - input_ = '' + self.explorer = YX(0, 0) + self.input_ = '' input_prompt = '> ' + connect() while True: - if self.do_refresh: + draw_screen() self.do_refresh = False - stdscr.clear() - recalc_input_lines() - - draw_input() - draw_history() - while True: try: msg = self.queue.get(block=False) - self.log_msg(msg) - self.do_refresh = True + handle_input(msg) except queue.Empty: break - try: key = stdscr.getkey() + self.do_refresh = True except curses.error: continue - self.do_refresh = True - if key == 'KEY_RESIZE': reset_screen_size() - elif key == 'KEY_BACKSPACE': - input_ = input_[:-1] - elif key == '\n': # Return key - self.socket.send(input_) - input_ = "" - else: - input_ += key - # TODO find out why - 1 is necessary here - max_length = self.window_width * self.rows - len(input_prompt) - 1 - if len(input_) > max_length: - input_ = input_[:max_length] - -s = socket.create_connection(('127.0.0.1', 5000)) -plom_socket = PlomSocket(s) -q = queue.Queue() -t = threading.Thread(target=recv_loop, args=(plom_socket, q)) -t.start() -TUI(plom_socket, q) + elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE': + self.input_ = self.input_[:-1] + elif self.mode.has_input_prompt and key != '\n': # Return key + self.input_ += key + max_length = self.window_width * self.size.y - len(input_prompt) - 1 + if len(self.input_) > max_length: + self.input_ = self.input_[:max_length] + elif self.mode == self.mode_login and key == '\n': + self.send('LOGIN ' + quote(self.input_)) + self.input_ = "" + elif self.mode == self.mode_chat and key == '\n': + if self.input_[0] == ':': + if self.input_ in {':p', ':play'}: + self.switch_mode('play') + elif self.input_ in {':?', ':study'}: + self.switch_mode('study') + if self.input_ == ':help': + self.help() + if self.input_ == ':reconnect': + reconnect() + elif self.input_.startswith(':nick'): + tokens = self.input_.split(maxsplit=1) + if len(tokens) == 2: + self.send('LOGIN ' + quote(tokens[1])) + else: + self.log_msg('? need login name') + elif self.input_.startswith(':msg'): + tokens = self.input_.split(maxsplit=2) + if len(tokens) == 3: + self.send('QUERY %s %s' % (quote(tokens[1]), + quote(tokens[2]))) + else: + self.log_msg('? need message target and message') + else: + self.log_msg('? unknown command') + else: + self.send('ALL ' + quote(self.input_)) + self.input_ = "" + elif self.mode == self.mode_annotate and key == '\n': + if self.input_ == '': + self.input_ = ' ' + self.send('ANNOTATE %s %s' % (self.explorer, quote(self.input_))) + self.input_ = "" + self.switch_mode('study', keep_position=True) + elif self.mode == self.mode_portal and key == '\n': + if self.input_ == '': + self.input_ = ' ' + self.send('PORTAL %s %s' % (self.explorer, quote(self.input_))) + self.input_ = "" + self.switch_mode('study', keep_position=True) + elif self.mode == self.mode_teleport and key == '\n': + if self.input_ == 'YES!': + self.host = self.teleport_target_host + self.port = self.teleport_target_port + reconnect() + else: + self.log_msg('@ teleport aborted') + self.switch_mode('play') + self.input_ = '' + elif self.mode == self.mode_study: + if key == 'c': + self.switch_mode('chat') + elif key == 'p': + self.switch_mode('play') + elif key == 'A': + self.switch_mode('annotate', keep_position=True) + elif key == 'P': + self.switch_mode('portal', keep_position=True) + elif key == 'w': + move_explorer('UP') + elif key == 'a': + move_explorer('LEFT') + elif key == 's': + move_explorer('DOWN') + elif key == 'd': + move_explorer('RIGHT') + elif self.mode == self.mode_play: + if key == 'c': + self.switch_mode('chat') + elif key == '?': + self.switch_mode('study') + if key == 'e': + self.switch_mode('edit') + elif key == 'f': + self.send('TASK:FLATTEN_SURROUNDINGS') + elif key == 'w': + self.send('TASK:MOVE UP') + elif key == 'a': + self.send('TASK:MOVE LEFT') + elif key == 's': + self.send('TASK:MOVE DOWN') + elif key == 'd': + self.send('TASK:MOVE RIGHT') + elif self.mode == self.mode_edit: + self.send('TASK:WRITE ' + key) + self.switch_mode('play') + +TUI('127.0.0.1', 5000)