From: Christian Heller Date: Wed, 4 Nov 2020 03:41:29 +0000 (+0100) Subject: Extend ncurses client functionality. X-Git-Url: https://plomlompom.com/repos/%7B%7Bdb.prefix%7D%7D/%7B%7B%20web_path%20%7D%7D/decks/todos?a=commitdiff_plain;h=d8d43ad18b8209eb34df79a604321e82f46b0887;p=plomrogue2-experiments Extend ncurses client functionality. --- diff --git a/new2/rogue_chat_curses.py b/new2/rogue_chat_curses.py index bdafe21..6f27256 100755 --- a/new2/rogue_chat_curses.py +++ b/new2/rogue_chat_curses.py @@ -4,19 +4,121 @@ 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 + +# TODO: fix screen refreshes on intermediary map results + +def cmd_TURN(game, n): + game.turn = n + game.things = [] + game.portals = {} +cmd_TURN.argtypes = 'int:nonneg' + +def cmd_LOGIN_OK(game): + game.tui.switch_mode('post_login_wait') + game.tui.socket.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_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') + if game.tui.mode.shows_info: + game.tui.query_info() + 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.log_msg('imagine the screen flicker (TODO)') + game.tui.do_refresh = True +cmd_PLAY_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' def recv_loop(plom_socket, q): for msg in plom_socket.recv(): q.put(msg) +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, + 'PLAY_ERROR': cmd_PLAY_ERROR} + thing_type = ThingBase + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.map_size = YX(0, 0) + 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): + def __init__(self, socket, q, game): + self.game = game + self.game.tui = self + self.parser = Parser(self.game) self.queue = q self.socket = socket self.log = [] - self.log_msg("hallo") - self.log_msg("welt") self.do_refresh = True curses.wrapper(self.loop) @@ -25,8 +127,36 @@ class TUI: if len(self.log) > 100: self.log = self.log[-100:] + def query_info(self): + self.socket.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 == '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 loop(self, stdscr): + 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 handle_input(msg): + command, args = self.parser.parse(msg) + command(*args) + def msg_into_lines_of_width(msg, width): chunk = '' lines = [] @@ -43,77 +173,225 @@ class TUI: 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(0, 1) # ugly TODO ncurses bug workaround, FIXME + 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): + # TODO movement constraints + if direction == 'up': + self.explorer += YX(-1, 0) + elif direction == 'left': + self.explorer += YX(0, -1) + elif direction == 'down': + self.explorer += YX(1, 0) + elif direction == 'right': + self.explorer += YX(0, 1) + self.query_info() 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]) + def draw_info(): + 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 + stdscr.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]) y += 1 + def draw_turn(): + stdscr.addstr(0, self.window_width, 'TURN: ' + str(self.game.turn)) + + def draw_mode(): + stdscr.addstr(1, self.window_width, 'MODE: ' + self.mode.name) + + def draw_map(): + map_lines_split = [] + for y in range(self.game.map_size.y): + start = self.game.map_size.x * y + end = start + self.game.map_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_size.y / 2), + int(self.game.map_size.x / 2)) + window_center = YX(int(self.size.y / 2), + int(self.window_width / 2)) + center = map_center + if self.mode.shows_info: + center = self.explorer + else: + player = self.game.get_thing(self.game.player_id, False) + if player: + center = player.position + 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_size.y): + to_draw = map_lines[map_y][map_x:self.window_width + offset.x] + stdscr.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() + + self.mode_play = Mode('play') + self.mode_study = Mode('study', shows_info=True) + self.mode_edit = Mode('edit') + self.mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True) + self.mode_portal = Mode('portal', has_input_prompt=True, shows_info=True) + self.mode_chat = Mode('chat', has_input_prompt=True) + self.mode_login = Mode('login', has_input_prompt=True, is_intro=True) + self.mode_post_login_wait = Mode('post_login_wait', is_intro=True) curses.curs_set(False) # hide cursor stdscr.timeout(10) reset_screen_size() - input_ = '' + self.mode = self.mode_login + self.explorer = YX(0, 0) + self.input_ = '' input_prompt = '> ' 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 + 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 # 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] + 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.socket.send('LOGIN ' + quote(self.input_)) + elif self.mode == self.mode_chat and key == '\n': + # TODO: query, nick, help, reconnect, unknown command + if self.input_ == ':p': + self.switch_mode('play') + elif self.input_ == ':?': + self.switch_mode('study') + else: + self.socket.send('ALL ' + quote(self.input_)) + self.input_ = "" + elif self.mode == self.mode_annotate and key == '\n': + if (self.input_ == ''): + self.input_ = ' ' + self.socket.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.socket.send('PORTAL %s %s' % (self.explorer, quote(self.input_))) + self.input_ = "" + self.switch_mode('study', keep_position=True) + 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.socket.send('TASK:FLATTEN_SURROUNDINGS') + elif key == 'w': + self.socket.send('TASK:MOVE UP') + elif key == 'a': + self.socket.send('TASK:MOVE LEFT') + elif key == 's': + self.socket.send('TASK:MOVE DOWN') + elif key == 'd': + self.socket.send('TASK:MOVE RIGHT') + elif self.mode == self.mode_edit: + self.socket.send('TASK:WRITE ' + key) + self.switch_mode('play') 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) +TUI(plom_socket, q, Game())