From e4b04a5ba87c262b7f01a52ce6e25001b8a53506 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Thu, 5 Nov 2020 22:16:08 +0100 Subject: [PATCH] Ensure rough feature parity between clients. --- new2/plomrogue/game.py | 2 +- new2/rogue_chat_curses.py | 263 ++++++++++++++++++++++++++------------ 2 files changed, 180 insertions(+), 85 deletions(-) diff --git a/new2/plomrogue/game.py b/new2/plomrogue/game.py index 1cb6e22..da14846 100755 --- a/new2/plomrogue/game.py +++ b/new2/plomrogue/game.py @@ -16,6 +16,7 @@ class GameBase: def __init__(self): self.turn = 0 self.things = [] + self.map_geometry = MapGeometrySquare(YX(24, 40)) def get_thing(self, id_, create_unfound): # No default for create_unfound because every call to get_thing @@ -42,7 +43,6 @@ class Game(GameBase): 'MOVE': Task_MOVE, 'WRITE': Task_WRITE, 'FLATTEN_SURROUNDINGS': Task_FLATTEN_SURROUNDINGS} - self.map_geometry = MapGeometrySquare(YX(24, 40)) self.commands = {'QUERY': cmd_QUERY, 'ALL': cmd_ALL, 'LOGIN': cmd_LOGIN, diff --git a/new2/rogue_chat_curses.py b/new2/rogue_chat_curses.py index ad6f4f9..6d0f8b7 100755 --- a/new2/rogue_chat_curses.py +++ b/new2/rogue_chat_curses.py @@ -14,11 +14,12 @@ 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.socket.send('GET_GAMESTATE') + game.tui.send('GET_GAMESTATE') game.tui.log_msg('@ welcome') cmd_LOGIN_OK.argtypes = '' @@ -42,7 +43,7 @@ def cmd_THING_NAME(game, thing_id, name): cmd_THING_NAME.argtypes = 'int:nonneg string' def cmd_MAP(game, size, content): - game.map_size = size + game.map_geometry.size = size game.map_content = content cmd_MAP.argtypes = 'yx_tuple:pos string' @@ -50,8 +51,16 @@ 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 = '' @@ -60,10 +69,15 @@ def cmd_PORTAL(game, 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.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 @@ -75,10 +89,6 @@ def cmd_ANNOTATION(game, position, msg): 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, @@ -91,12 +101,13 @@ class Game(GameBase): '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_size = YX(0, 0) self.map_content = '' self.player_id = -1 self.info_db = {} @@ -110,46 +121,134 @@ class Game(GameBase): class TUI: - def __init__(self, socket, q, game): - self.game = game + 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.queue = q - self.socket = socket self.log = [] 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.socket.send('GET_ANNOTATION ' + str(self.explorer)) + 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 == 'annotate' and self.explorer in self.game.info_db: + 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): - class Mode: + 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) - 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 + 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) @@ -160,19 +259,18 @@ class TUI: 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.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) @@ -185,16 +283,12 @@ class TUI: 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() + target = self.game.map_geometry.move(self.explorer, direction) + if target: + self.explorer = target + self.query_info() + else: + self.flash() def draw_history(): lines = [] @@ -206,9 +300,11 @@ class TUI: for i in range(len(lines)): 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: @@ -223,29 +319,29 @@ class TUI: y = height_header + i if y >= self.size.y - len(self.input_lines): break - stdscr.addstr(y, self.window_width, lines[i]) + safe_addstr(y, self.window_width, lines[i]) def draw_input(): 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(): - stdscr.addstr(0, self.window_width, 'TURN: ' + str(self.game.turn)) + if not self.game.turn_complete: + return + safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn)) def draw_mode(): - stdscr.addstr(1, self.window_width, 'MODE: ' + self.mode.name) + safe_addstr(1, self.window_width, 'MODE: ' + self.mode.name) def draw_map(): - player = self.game.get_thing(self.game.player_id, False) - if not player: - # catches race conditions where game.things still empty + if not self.game.turn_complete: return 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 + 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] = '@' @@ -254,11 +350,12 @@ class TUI: 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)) + 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)) - center = player + 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 @@ -266,9 +363,9 @@ class TUI: 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): + 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] - stdscr.addstr(term_y, term_x, to_draw) + safe_addstr(term_y, term_x, to_draw) term_y += 1 map_y += 1 @@ -286,21 +383,13 @@ class TUI: 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() - self.mode = self.mode_login self.explorer = YX(0, 0) self.input_ = '' input_prompt = '> ' + connect() while True: if self.do_refresh: draw_screen() @@ -322,50 +411,61 @@ class TUI: 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.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_)) + self.send('LOGIN ' + quote(self.input_)) self.input_ = "" elif self.mode == self.mode_chat and key == '\n': - # TODO: query, nick, help, reconnect, unknown command 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.socket.send('LOGIN ' + quote(tokens[1])) + 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.socket.send('QUERY %s %s' % (quote(tokens[1]), + 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.socket.send('ALL ' + quote(self.input_)) + self.send('ALL ' + quote(self.input_)) self.input_ = "" elif self.mode == self.mode_annotate and key == '\n': - if (self.input_ == ''): + if self.input_ == '': self.input_ = ' ' - self.socket.send('ANNOTATE %s %s' % (self.explorer, quote(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_ == ''): + if self.input_ == '': self.input_ = ' ' - self.socket.send('PORTAL %s %s' % (self.explorer, quote(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') @@ -376,13 +476,13 @@ class TUI: elif key == 'P': self.switch_mode('portal', keep_position=True) elif key == 'w': - move_explorer('up') + move_explorer('UP') elif key == 'a': - move_explorer('left') + move_explorer('LEFT') elif key == 's': - move_explorer('down') + move_explorer('DOWN') elif key == 'd': - move_explorer('right') + move_explorer('RIGHT') elif self.mode == self.mode_play: if key == 'c': self.switch_mode('chat') @@ -391,22 +491,17 @@ class TUI: if key == 'e': self.switch_mode('edit') elif key == 'f': - self.socket.send('TASK:FLATTEN_SURROUNDINGS') + self.send('TASK:FLATTEN_SURROUNDINGS') elif key == 'w': - self.socket.send('TASK:MOVE UP') + self.send('TASK:MOVE UP') elif key == 'a': - self.socket.send('TASK:MOVE LEFT') + self.send('TASK:MOVE LEFT') elif key == 's': - self.socket.send('TASK:MOVE DOWN') + self.send('TASK:MOVE DOWN') elif key == 'd': - self.socket.send('TASK:MOVE RIGHT') + self.send('TASK:MOVE RIGHT') elif self.mode == self.mode_edit: - self.socket.send('TASK:WRITE ' + key) + self.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, Game()) +TUI('127.0.0.1', 5000) -- 2.30.2