From 1fcb132eabcaa1a95bf2b527dc18c92c15016d2a Mon Sep 17 00:00:00 2001 From: Christian Heller <c.heller@plomlompom.de> Date: Fri, 6 Nov 2020 01:34:33 +0100 Subject: [PATCH] Add Hex map capabilities. --- new2/plomrogue/commands.py | 9 +- new2/plomrogue/game.py | 21 +- new2/plomrogue/mapping.py | 32 +++ new2/rogue_chat_curses.py | 76 ++++--- new2/rogue_chat_nocanvas_monochrome.html | 258 +++++++++++++++-------- 5 files changed, 266 insertions(+), 130 deletions(-) diff --git a/new2/plomrogue/commands.py b/new2/plomrogue/commands.py index 3741033..bdf4824 100644 --- a/new2/plomrogue/commands.py +++ b/new2/plomrogue/commands.py @@ -1,6 +1,6 @@ from plomrogue.misc import quote from plomrogue.errors import GameError -from plomrogue.mapping import YX +from plomrogue.mapping import YX, MapGeometrySquare, MapGeometryHex @@ -89,6 +89,7 @@ def cmd_MAP_LINE(game, y, line): game.map.set_line(y, line) cmd_MAP_LINE.argtypes = 'int:nonneg string' -def cmd_MAP(game, size): - game.new_world(size) -cmd_MAP.argtypes = 'yx_tuple:pos' +def cmd_MAP(game, geometry, size): + map_geometry_class = globals()['MapGeometry' + geometry] + game.new_world(map_geometry_class(size)) +cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos' diff --git a/new2/plomrogue/game.py b/new2/plomrogue/game.py index da14846..0c09b01 100755 --- a/new2/plomrogue/game.py +++ b/new2/plomrogue/game.py @@ -1,8 +1,8 @@ from plomrogue.tasks import (Task_WAIT, Task_MOVE, Task_WRITE, Task_FLATTEN_SURROUNDINGS) from plomrogue.errors import GameError, PlayError -from plomrogue.commands import (cmd_ALL, cmd_LOGIN, cmd_QUERY, cmd_PING, - cmd_TURN, cmd_MAP_LINE, cmd_MAP, cmd_GET_ANNOTATION, +from plomrogue.commands import (cmd_ALL, cmd_LOGIN, cmd_QUERY, cmd_PING, cmd_MAP, + cmd_TURN, cmd_MAP_LINE, cmd_GET_ANNOTATION, cmd_ANNOTATE, cmd_PORTAL, cmd_GET_GAMESTATE) from plomrogue.io import GameIO from plomrogue.misc import quote @@ -75,11 +75,16 @@ class Game(GameBase): import string if string_option_type == 'direction': return self.map_geometry.get_directions() - if string_option_type == 'char': + elif string_option_type == 'char': return [c for c in string.digits + string.ascii_letters + string.punctuation + ' '] + elif string_option_type == 'map_geometry': + return ['Hex', 'Square'] return None + def get_map_geometry_shape(self): + return self.map_geometry.__class__.__name__[len('MapGeometry'):] + def send_gamestate(self, connection_id=None): """Send out game state data relevant to clients.""" @@ -91,7 +96,8 @@ class Game(GameBase): self.io.send('TURN ' + str(self.turn)) for t in self.things: send_thing(t) - self.io.send('MAP %s %s' % (self.map_geometry.size, quote(self.map.terrain))) + self.io.send('MAP %s %s %s' % (self.get_map_geometry_shape(), + self.map_geometry.size, quote(self.map.terrain))) for yx in self.portals: self.io.send('PORTAL %s %s' % (yx, quote(self.portals[yx]))) self.io.send('GAME_STATE_COMPLETE') @@ -179,7 +185,8 @@ class Game(GameBase): with open(self.io.save_file, 'w') as f: write(f, 'TURN %s' % self.turn) - write(f, 'MAP %s' % (self.map_geometry.size,)) + map_geometry_shape = self.get_map_geometry_shape() + write(f, 'MAP %s %s' % (map_geometry_shape, self.map_geometry.size,)) for y, line in self.map.lines(): write(f, 'MAP_LINE %5s %s' % (y, quote(line))) for yx in self.annotations: @@ -187,7 +194,7 @@ class Game(GameBase): for yx in self.portals: write(f, 'PORTAL %s %s' % (yx, quote(self.portals[yx]))) - def new_world(self, size): - self.map_geometry = MapGeometrySquare(YX(size.y, size.x)) + def new_world(self, map_geometry): + self.map_geometry = map_geometry self.map = Map(self.map_geometry.size) self.annotations = {} diff --git a/new2/plomrogue/mapping.py b/new2/plomrogue/mapping.py index f056e10..e0a59d8 100644 --- a/new2/plomrogue/mapping.py +++ b/new2/plomrogue/mapping.py @@ -64,6 +64,38 @@ class MapGeometrySquare(MapGeometryWithLeftRightMoves): +class MapGeometryHex(MapGeometryWithLeftRightMoves): + + def move_UPLEFT(self, start_pos): + start_indented = start_pos.y % 2 + if start_indented: + return YX(start_pos.y - 1, start_pos.x) + else: + return YX(start_pos.y - 1, start_pos.x - 1) + + def move_UPRIGHT(self, start_pos): + start_indented = start_pos.y % 2 + if start_indented: + return YX(start_pos.y - 1, start_pos.x + 1) + else: + return YX(start_pos.y - 1, start_pos.x) + + def move_DOWNLEFT(self, start_pos): + start_indented = start_pos.y % 2 + if start_indented: + return YX(start_pos.y + 1, start_pos.x) + else: + return YX(start_pos.y + 1, start_pos.x - 1) + + def move_DOWNRIGHT(self, start_pos): + start_indented = start_pos.y % 2 + if start_indented: + return YX(start_pos.y + 1, start_pos.x + 1) + else: + return YX(start_pos.y + 1, start_pos.x) + + + class Map(): def __init__(self, map_size): diff --git a/new2/rogue_chat_curses.py b/new2/rogue_chat_curses.py index 6d0f8b7..643023b 100755 --- a/new2/rogue_chat_curses.py +++ b/new2/rogue_chat_curses.py @@ -6,7 +6,7 @@ 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.mapping import YX, MapGeometrySquare, MapGeometryHex from plomrogue.things import ThingBase from plomrogue.misc import quote @@ -42,10 +42,27 @@ def cmd_THING_NAME(game, thing_id, name): t.name = name cmd_THING_NAME.argtypes = 'int:nonneg string' -def cmd_MAP(game, size, content): - game.map_geometry.size = size +def cmd_MAP(game, geometry, size, content): + map_geometry_class = globals()['MapGeometry' + geometry] + game.map_geometry = map_geometry_class(size) game.map_content = content -cmd_MAP.argtypes = 'yx_tuple:pos string' + if type(game.map_geometry) == MapGeometrySquare: + game.tui.movement_keys = { + 'w': 'UP', + 'a': 'LEFT', + 's': 'DOWN', + 'd': 'RIGHT', + } + elif type(game.map_geometry) == MapGeometryHex: + game.tui.movement_keys = { + 'w': 'UPLEFT', + 'e': 'UPRIGHT', + 'd': 'RIGHT', + 'c': 'DOWNRIGHT', + 'x': 'DOWNLEFT', + 's': 'LEFT', + } +cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string' def cmd_GAME_STATE_COMPLETE(game): game.info_db = {} @@ -113,6 +130,11 @@ class Game(GameBase): self.info_db = {} self.portals = {} + def get_string_options(self, string_option_type): + if string_option_type == 'map_geometry': + return ['Hex', 'Square'] + return None + def get_command(self, command_name): from functools import partial f = partial(self.commands[command_name], self) @@ -348,17 +370,25 @@ class TUI: 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)) + if type(self.game.map_geometry) == MapGeometryHex: + indent = 0 + for line in map_lines_split: + map_lines += [indent*' ' + ' '.join(line)] + indent = 0 if indent else 1 + else: + for line in map_lines_split: + map_lines += [''.join(line)] 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 + if type(self.game.map_geometry) == MapGeometryHex: + center = YX(center.y, center.x * 2) offset = center - window_center + if type(self.game.map_geometry) == MapGeometryHex and offset.y % 2: + offset += YX(0, 1) term_y = max(0, -offset.y) term_x = max(0, -offset.x) map_y = max(0, offset.y) @@ -467,39 +497,27 @@ class TUI: self.switch_mode('play') self.input_ = '' elif self.mode == self.mode_study: - if key == 'c': + if key == 'C': self.switch_mode('chat') - elif key == 'p': + elif key == 'P': self.switch_mode('play') elif key == 'A': self.switch_mode('annotate', keep_position=True) - elif key == 'P': + 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 key in self.movement_keys: + move_explorer(self.movement_keys[key]) elif self.mode == self.mode_play: - if key == 'c': + if key == 'C': self.switch_mode('chat') elif key == '?': self.switch_mode('study') - if key == 'e': + 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 key in self.movement_keys: + self.send('TASK:MOVE ' + self.movement_keys[key]) elif self.mode == self.mode_edit: self.send('TASK:WRITE ' + key) self.switch_mode('play') diff --git a/new2/rogue_chat_nocanvas_monochrome.html b/new2/rogue_chat_nocanvas_monochrome.html index f513ad9..bb4c771 100644 --- a/new2/rogue_chat_nocanvas_monochrome.html +++ b/new2/rogue_chat_nocanvas_monochrome.html @@ -4,9 +4,9 @@ </style> </head><body> <div> -movement: <select id="WASD_selector" name="WASD_selector" > -<option value="w, a, s, d" selected>w, a, s, d</option> -<option value="arrow keys">arrow keys</option> +movement: <select id="movement_keys" name="movement_keys" > +<option value="alphabetic" selected>w/a/s/d (square grid) or e,d,c,x,s,r (hex grid)</option> +<option value="arrow_or_numpad">arrow keys (square grid) or numpad (hex grid)</option> </select> rows: <input id="n_rows" type="number" step=2 min=10 value=24 /> cols: <input id="n_cols" type="number" step=4 min=20 value=80 /> @@ -21,7 +21,7 @@ command character: <select id="command_char"" > "use strict"; let websocket_location = "ws://localhost:8000"; -let wasd_selector = document.getElementById("WASD_selector"); +let movement_keys_selector = document.getElementById("movement_keys"); let rows_selector = document.getElementById("n_rows"); let cols_selector = document.getElementById("n_cols"); let command_char_selector = document.getElementById("command_char"); @@ -172,6 +172,7 @@ let server = { handle_event: function(event) { let tokens = parser.tokenize(event.data)[0]; if (tokens[0] === 'TURN') { + game.turn_complete = false; game.things = {}; game.portals = {}; game.turn = parseInt(tokens[1]); @@ -180,11 +181,17 @@ let server = { } else if (tokens[0] === 'THING_NAME') { game.get_thing(tokens[1], true).name_ = tokens[2]; } else if (tokens[0] === 'MAP') { - game.map_size = parser.parse_yx(tokens[1]); - game.map = tokens[2] + game.map_geometry = tokens[1]; + tui.init_wasd(); + game.map_size = parser.parse_yx(tokens[2]); + game.map = tokens[3] } else if (tokens[0] === 'GAME_STATE_COMPLETE') { + game.turn_complete = true; explorer.empty_info_db(); - if (tui.mode == mode_study) { + if (tui.mode == mode_post_login_wait) { + tui.switch_mode(mode_play); + tui.log_help(); + } else if (tui.mode == mode_study) { explorer.query_info(); } let t = game.get_thing(game.player_id); @@ -200,9 +207,7 @@ let server = { game.player_id = parseInt(tokens[1]); } else if (tokens[0] === 'LOGIN_OK') { this.send(['GET_GAMESTATE']); - tui.log_help(); - // TODO wait for game state for this switch, use intermediary mode? - tui.switch_mode(mode_play); + tui.switch_mode(mode_post_login_wait); } else if (tokens[0] === 'PORTAL') { let position = parser.parse_yx(tokens[1]); game.portals[position] = tokens[2]; @@ -260,6 +265,7 @@ class Mode { } let mode_waiting_for_server = new Mode('waiting_for_server', false, false, true); let mode_login = new Mode('login', true, false, true); +let mode_post_login_wait = new Mode('waiting for game world', false, false, true); let mode_chat = new Mode('chat / write messages to players', true, false); let mode_annotate = new Mode('add message to map tile', true, true); let mode_play = new Mode('play / move around', false, false); @@ -286,25 +292,55 @@ let tui = { this.init_wasd(); }, init_wasd: function() { - if (wasd_selector.value == 'w, a, s, d') { - tui.key_up = 'w'; - tui.key_down = 's'; - tui.key_left = 'a'; - tui.key_right = 'd'; - } else if (wasd_selector.value == 'arrow keys') { - tui.key_up = 'ArrowUp'; - tui.key_down = 'ArrowDown'; - tui.key_left = 'ArrowLeft'; - tui.key_right = 'ArrowRight'; + + if (movement_keys_selector.value == 'alphabetic') { + if (game.map_geometry == 'Square') { + this.movement_keys = { + 'w': 'UP', + 'a': 'LEFT', + 's': 'DOWN', + 'd': 'RIGHT' + }; + tui.movement_keys_desc = 'w, a, s, d'; + } else if (game.map_geometry == 'Hex') { + this.movement_keys = { + 'w': 'UPLEFT', + 'e': 'UPRIGHT', + 'd': 'RIGHT', + 'c': 'DOWNRIGHT', + 'x': 'DOWNLEFT', + 's': 'LEFT' + }; + tui.movement_keys_desc = 'e, d, c, x, s, w'; + }; + } else if (movement_keys_selector.value == 'arrow_or_numpad') { + if (game.map_geometry == 'Square') { + this.movement_keys = { + 'ArrowUp': 'UP', + 'ArrowLeft': 'LEFT', + 'ArrowDown': 'DOWN', + 'ArrowRight': 'RIGHT' + }; + tui.movement_keys_desc = 'arrow keys'; + } else if (game.map_geometry == 'Hex') { + this.movement_keys = { + '7': 'UPLEFT', + '9': 'UPRIGHT', + '6': 'RIGHT', + '3': 'DOWNRIGHT', + '1': 'DOWNLEFT', + '4': 'LEFT' + }; + tui.movement_keys_desc = 'numpad keys'; + }; }; - tui.movement_keys_desc = wasd_selector.value; }, init_login: function() { this.log_msg("@ please enter your username:"); this.switch_mode(mode_login); }, switch_mode: function(mode, keep_pos=false) { - if (mode == mode_study && !keep_pos) { + if (mode == mode_study && !keep_pos && game.player_id in game.things) { explorer.position = game.things[game.player_id].position; } this.mode = mode; @@ -371,49 +407,71 @@ let tui = { this.log_msg(" " + command_char_selector.value + "? or " + command_char_selector.value + "study - switch to study mode"); this.log_msg("commands common to study and play mode:"); this.log_msg(" " + this.movement_keys_desc + " - move"); - this.log_msg(" c - switch to chat mode"); + this.log_msg(" C - switch to chat mode"); this.log_msg("commands specific to play mode:"); - this.log_msg(" e - write following ASCII character"); + this.log_msg(" E - write following ASCII character"); this.log_msg(" f - flatten surroundings"); this.log_msg(" ? - switch to study mode"); this.log_msg("commands specific to study mode:"); - this.log_msg(" e - annotate terrain"); - this.log_msg(" p - switch to play mode"); + this.log_msg(" E - annotate terrain"); + this.log_msg(" P - switch to play mode"); }, draw_map: function() { - let map_lines = []; + let map_lines_split = []; let line = []; for (let i = 0, j = 0; i < game.map.length; i++, j++) { if (j == game.map_size[1]) { - map_lines.push(line); + map_lines_split.push(line); line = []; j = 0; }; line.push(game.map[i]); }; - map_lines.push(line); - let center_pos = [Math.floor(game.map_size[0] / 2), - Math.floor(game.map_size[1] / 2)]; + map_lines_split.push(line); for (const thing_id in game.things) { let t = game.things[thing_id]; - map_lines[t.position[0]][t.position[1]] = '@'; - if (game.player_id == thing_id) { - center_pos = t.position; - } + map_lines_split[t.position[0]][t.position[1]] = '@'; }; if (tui.mode.shows_info) { - map_lines[explorer.position[0]][explorer.position[1]] = '?'; - center_pos = explorer.position; + map_lines_split[explorer.position[0]][explorer.position[1]] = '?'; } - let offset = [(terminal.rows / 2) - center_pos[0], - this.window_width / 2 - center_pos[1]]; - for (let term_y = offset[0], map_y = 0; - term_y < terminal.rows && map_y < game.map_size[0]; - term_y++, map_y++) { - if (term_y >= 0) { - let to_draw = map_lines[map_y].join('').slice(0, this.window_width - offset[1]); - terminal.write(term_y, offset[1], to_draw); - } + let map_lines = [] + if (game.map_geometry == 'Square') { + for (let line_split of map_lines_split) { + map_lines.push(line_split.join('')); + }; + } else if (game.map_geometry == 'Hex') { + let indent = 0 + for (let line_split of map_lines_split) { + map_lines.push(' '.repeat(indent) + line_split.join(' ')); + if (indent == 0) { + indent = 1; + } else { + indent = 0; + }; + }; + } + let window_center = [terminal.rows / 2, this.window_width / 2]; + let player = game.things[game.player_id]; + let center_position = [player.position[0], player.position[1]]; + if (tui.mode.shows_info) { + center_position = [explorer.position[0], explorer.position[1]]; + } + if (game.map_geometry == 'Hex') { + center_position[1] = center_position[1] * 2; + }; + let offset = [center_position[0] - window_center[0], + center_position[1] - window_center[1]] + if (game.map_geometry == 'Hex' && offset[0] % 2) { + offset[1] += 1; + }; + let term_y = Math.max(0, -offset[0]); + let term_x = Math.max(0, -offset[1]); + let map_y = Math.max(0, offset[0]); + let map_x = Math.max(0, offset[1]); + for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) { + let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]); + terminal.write(term_y, term_x, to_draw); } }, draw_mode_line: function() { @@ -453,8 +511,10 @@ let tui = { this.draw_history(); this.draw_input(); } else { - this.draw_map(); - this.draw_turn_line(); + if (game.turn_complete) { + this.draw_map(); + this.draw_turn_line(); + } this.draw_mode_line(); if (this.mode.shows_info) { this.draw_info(); @@ -484,6 +544,48 @@ let game = { game.things[id_] = t; return t; }; + }, + move: function(start_position, direction) { + let target = [start_position[0], start_position[1]]; + if (direction == 'LEFT') { + target[1] -= 1; + } else if (direction == 'RIGHT') { + target[1] += 1; + } else if (game.map_geometry == 'Square') { + if (direction == 'UP') { + target[0] -= 1; + } else if (direction == 'DOWN') { + target[0] += 1; + }; + } else if (game.map_geometry == 'Hex') { + let start_indented = start_position[0] % 2; + if (direction == 'UPLEFT') { + target[0] -= 1; + if (!start_indented) { + target[1] -= 1; + } + } else if (direction == 'UPRIGHT') { + target[0] -= 1; + if (start_indented) { + target[1] += 1; + } + } else if (direction == 'DOWNLEFT') { + target[0] += 1; + if (!start_indented) { + target[1] -= 1; + } + } else if (direction == 'DOWNRIGHT') { + target[0] += 1; + if (start_indented) { + target[1] += 1; + } + }; + }; + if (target[0] < 0 || target[1] < 0 || + target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) { + return null; + }; + return target; } } @@ -496,26 +598,14 @@ let explorer = { position: [0,0], info_db: {}, move: function(direction) { - let try_pos = [0,0]; - try_pos[0] = this.position[0]; - try_pos[1] = this.position[1]; - if (direction == 'left') { - try_pos[1] -= 1; - } else if (direction == 'right') { - try_pos[1] += 1; - } else if (direction == 'up') { - try_pos[0] -= 1; - } else if (direction == 'down') { - try_pos[0] += 1; - }; - if (!(try_pos[0] < 0) && - !(try_pos[1] < 0) && - !(try_pos[0] >= game.map_size[0]) - && !(try_pos[1] >= game.map_size[1])) { - this.position = try_pos; + let target = game.move(this.position, direction); + if (target) { + this.position = target this.query_info(); tui.full_refresh(); - } + } else { + terminal.blink_screen(); + }; }, update_info_db: function(yx, str) { this.info_db[yx] = str; @@ -644,10 +734,10 @@ tui.inputEl.addEventListener('keydown', (event) => { tui.empty_input(); tui.full_refresh(); } else if (tui.mode == mode_play) { - if (event.key === 'c') { + if (event.key === 'C') { event.preventDefault(); tui.switch_mode(mode_chat); - } else if (event.key === 'e') { + } else if (event.key === 'E') { event.preventDefault(); tui.switch_mode(mode_edit); } else if (event.key === '?') { @@ -656,40 +746,28 @@ tui.inputEl.addEventListener('keydown', (event) => { tui.log_help(); } else if (event.key === 'f') { server.send(["TASK:FLATTEN_SURROUNDINGS"]); - } else if (event.key === tui.key_left) { - server.send(['TASK:MOVE', 'LEFT']); - } else if (event.key === tui.key_right) { - server.send(['TASK:MOVE', 'RIGHT']); - } else if (event.key === tui.key_up) { - server.send(['TASK:MOVE', 'UP']); - } else if (event.key === tui.key_down) { - server.send(['TASK:MOVE', 'DOWN']); + } else if (event.key in tui.movement_keys) { + server.send(['TASK:MOVE', tui.movement_keys[event.key]]); }; } else if (tui.mode == mode_study) { - if (event.key === 'c') { + if (event.key === 'C') { event.preventDefault(); tui.switch_mode(mode_chat); - } else if (event.key == 'p') { + } else if (event.key == 'P') { tui.switch_mode(mode_play); - } else if (event.key === 'P') { + } else if (event.key === 'p') { event.preventDefault(); tui.switch_mode(mode_portal); - } else if (event.key === tui.key_left) { - explorer.move('left'); - } else if (event.key === tui.key_right) { - explorer.move('right'); - } else if (event.key === tui.key_up) { - explorer.move('up'); - } else if (event.key === tui.key_down) { - explorer.move('down'); - } else if (event.key === 'e') { + } else if (event.key in tui.movement_keys) { + explorer.move(tui.movement_keys[event.key]); + } else if (event.key === 'E') { event.preventDefault(); tui.switch_mode(mode_annotate); }; } }, false); -wasd_selector.addEventListener('input', function() { +movement_keys_selector.addEventListener('input', function() { tui.init_wasd(); }, false); rows_selector.addEventListener('input', function() { @@ -708,7 +786,7 @@ cols_selector.addEventListener('input', function() { tui.full_refresh(); }, false); window.setInterval(function() { - if (!(['input', 'n_cols', 'n_rows', 'WASD_selector', 'command_char'].includes(document.activeElement.id))) { + if (!(['input', 'n_cols', 'n_rows', 'movement_keys', 'command_char'].includes(document.activeElement.id))) { tui.inputEl.focus(); } }, 100); -- 2.30.2