From 6cc83951670f2022bd22cbf0728ebb4c25479c4d Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Fri, 13 Nov 2020 23:48:07 +0100 Subject: [PATCH] Add basic non-player things system. --- config.json | 6 ++-- plomrogue/commands.py | 44 +++++++++++++++++------ plomrogue/game.py | 56 +++++++++++++---------------- plomrogue/things.py | 20 ++++++++--- rogue_chat.py | 9 +++-- rogue_chat_curses.py | 44 ++++++++++++++++------- rogue_chat_nocanvas_monochrome.html | 22 ++++++++---- 7 files changed, 131 insertions(+), 70 deletions(-) diff --git a/config.json b/config.json index 27853c1..d1682c3 100644 --- a/config.json +++ b/config.json @@ -11,9 +11,9 @@ "hex_move_upleft": "w", "hex_move_upright": "e", "hex_move_right": "d", - "hex_move_downright": "x", - "hex_move_downleft": "y", - "hex_move_left": "a", + "hex_move_downright": "c", + "hex_move_downleft": "x", + "hex_move_left": "s", "square_move_up": "w", "square_move_left": "a", "square_move_down": "s", diff --git a/plomrogue/commands.py b/plomrogue/commands.py index 8ec6f52..1c59abf 100644 --- a/plomrogue/commands.py +++ b/plomrogue/commands.py @@ -4,11 +4,19 @@ from plomrogue.mapping import YX, MapGeometrySquare, MapGeometryHex, Map +# TODO: instead of sending tasks and thing types on request, send them on connection + def cmd_TASKS(game, connection_id): tasks = [] game.io.send('TASKS ' + ','.join(game.tasks.keys()), connection_id) cmd_TASKS.argtypes = '' +def cmd_THING_TYPES(game, connection_id): + for t_t in game.thing_types.values(): + game.io.send('THING_TYPE %s %s' % (t_t.get_type(), t_t.symbol_hint), + connection_id) +cmd_THING_TYPES.argtypes = '' + def cmd_ALL(game, msg, connection_id): def lower_msg_by_volume(msg, volume): @@ -28,7 +36,7 @@ def cmd_ALL(game, msg, connection_id): import random if not connection_id in game.sessions: raise GameError('need to be logged in for this') - speaker = game.get_thing(game.sessions[connection_id], False) + speaker = game.get_thing(game.sessions[connection_id]) n_max = 255 map_size = game.map.size_i dijkstra_map = [n_max for i in range(game.map.size_i)] @@ -55,7 +63,7 @@ def cmd_ALL(game, msg, connection_id): # x = 0 # print(' '.join(line_to_print)) for c_id in game.sessions: - listener = game.get_thing(game.sessions[c_id], create_unfound=False) + listener = game.get_thing(game.sessions[c_id]) listener_vol = dijkstra_map[game.map.get_position_index(listener.position)] volume = 1 / max(1, listener_vol) lowered_msg = lower_msg_by_volume(msg, volume) @@ -67,11 +75,11 @@ def cmd_ALL(game, msg, connection_id): cmd_ALL.argtypes = 'string' def cmd_LOGIN(game, nick, connection_id): - for t in [t for t in game.things if t.type_ == 'player' and t.nickname == nick]: + for t in [t for t in game.things if t.type_ == 'Player' and t.nickname == nick]: raise GameError('name already in use') if connection_id in game.sessions: raise GameError('cannot log in twice') - t = game.thing_types['player'](game) + t = game.thing_types['Player'](game) t.position = YX(game.map.size.y // 2, game.map.size.x // 2) game.things += [t] # TODO refactor into Thing.__init__? game.sessions[connection_id] = t.id_ @@ -83,12 +91,12 @@ def cmd_LOGIN(game, nick, connection_id): cmd_LOGIN.argtypes = 'string' def cmd_NICK(game, nick, connection_id): - for t in [t for t in game.things if t.type_ == 'player' and t.nickname == nick]: + for t in [t for t in game.things if t.type_ == 'Player' and t.nickname == nick]: raise GameError('name already in use') if not connection_id in game.sessions: raise GameError('can only rename when already logged in') t_id = game.sessions[connection_id] - t = game.get_thing(t_id, False) + t = game.get_thing(t_id) old_nick = t.nickname t.nickname = nick game.io.send('CHAT ' + quote(old_nick + ' renamed themselves to ' + nick)) @@ -104,7 +112,7 @@ cmd_GET_GAMESTATE.argtypes = '' # raise GameError('can only query when logged in') # t = game.get_thing(game.sessions[connection_id], False) # source_nick = t.nickname -# for t in [t for t in game.things if t.type_ == 'player' and t.nickname == target_nick]: +# for t in [t for t in game.things if t.type_ == 'Player' and t.nickname == target_nick]: # for c_id in game.sessions: # if game.sessions[c_id] == t.id_: # game.io.send('CHAT ' + quote(source_nick+ '->' + target_nick + ': ' + msg), c_id) @@ -123,7 +131,7 @@ def cmd_TURN(game, n): cmd_TURN.argtypes = 'int:nonneg' def cmd_ANNOTATE(game, yx, msg, pw, connection_id): - player = game.get_thing(game.sessions[connection_id], False) + player = game.get_thing(game.sessions[connection_id]) if player.fov_stencil[yx] != '.': raise GameError('cannot annotate tile outside field of view') if not game.can_do_tile_with_pw(yx, pw): @@ -137,7 +145,7 @@ def cmd_ANNOTATE(game, yx, msg, pw, connection_id): cmd_ANNOTATE.argtypes = 'yx_tuple:nonneg string string' def cmd_PORTAL(game, yx, msg, pw, connection_id): - player = game.get_thing(game.sessions[connection_id], False) + player = game.get_thing(game.sessions[connection_id]) if player.fov_stencil[yx] != '.': raise GameError('cannot edit portal on tile outside field of view') if not game.can_do_tile_with_pw(yx, pw): @@ -161,7 +169,7 @@ def cmd_GOD_PORTAL(game, yx, msg): cmd_GOD_PORTAL.argtypes = 'yx_tuple:nonneg string' def cmd_GET_ANNOTATION(game, yx, connection_id): - player = game.get_thing(game.sessions[connection_id], False) + player = game.get_thing(game.sessions[connection_id]) annotation = '(unknown)'; if player.fov_stencil[yx] == '.': annotation = '(none)'; @@ -186,3 +194,19 @@ cmd_MAP_CONTROL_LINE.argtypes = 'int:nonneg string' def cmd_MAP_CONTROL_PW(game, tile_class, password): game.map_control_passwords[tile_class] = password cmd_MAP_CONTROL_PW.argtypes = 'char string' + +def cmd_THING(game, yx, thing_type, thing_id): + if not thing_type in game.thing_types: + raise GameError('illegal thing type %s' % thing_type) + if yx.y < 0 or yx.x < 0 or yx.y >= game.map.size.y or yx.x >= game.map.size.x: + raise GameError('illegal position %s' % yx) + t_old = None + if thing_id > 0: + t_old = game.get_thing(thing_id) + t_new = game.thing_types[thing_type](game, id_=thing_id, position=yx) + if t_old: + game.things[game.things.index(t_old)] = t_new + else: + game.things += [t_new] + game.changed = True +cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type int:nonneg' diff --git a/plomrogue/game.py b/plomrogue/game.py index 0c17977..1ffbf1a 100755 --- a/plomrogue/game.py +++ b/plomrogue/game.py @@ -3,7 +3,6 @@ from plomrogue.tasks import (Task_WAIT, Task_MOVE, Task_WRITE, from plomrogue.errors import GameError, PlayError from plomrogue.io import GameIO from plomrogue.misc import quote -from plomrogue.things import Thing, ThingPlayer from plomrogue.mapping import YX, MapGeometrySquare, Map @@ -16,24 +15,21 @@ class GameBase: self.map_geometry = MapGeometrySquare(YX(24, 40)) self.commands = {} - def get_thing(self, id_, create_unfound): - # No default for create_unfound because every call to get_thing - # should be accompanied by serious consideration whether to use it. + def get_thing(self, id_): for thing in self.things: if id_ == thing.id_: return thing - if create_unfound: - t = self.thing_type(self, id_) - self.things += [t] - return t return None + def _register_object(self, obj, obj_type_desc, prefix): + if not obj.__name__.startswith(prefix): + raise GameError('illegal %s object name: %s' % (obj_type_desc, obj.__name__)) + obj_name = obj.__name__[len(prefix):] + d = getattr(self, obj_type_desc + 's') + d[obj_name] = obj + def register_command(self, command): - prefix = 'cmd_' - if not command.__name__.startswith(prefix): - raise GameError('illegal command object name: %s' % command.__name__) - command_name = command.__name__[len(prefix):] - self.commands[command_name] = command + self._register_object(command, 'command', 'cmd_') @@ -45,8 +41,7 @@ class Game(GameBase): self.changed = True self.io = GameIO(self, save_file) self.tasks = {} - self.thing_type = Thing - self.thing_types = {'player': ThingPlayer} + self.thing_types = {} self.sessions = {} self.map = Map(self.map_geometry.size) self.map_control = Map(self.map_geometry.size) @@ -57,12 +52,11 @@ class Game(GameBase): if not os.path.isfile(self.io.save_file): raise GameError('save file path refers to non-file') + def register_thing_type(self, thing_type): + self._register_object(thing_type, 'thing_type', 'Thing_') + def register_task(self, task): - prefix = 'Task_' - if not task.__name__.startswith(prefix): - raise GameError('illegal task object name: %s' % task.__name__) - task_name = task.__name__[len(prefix):] - self.tasks[task_name] = task + self._register_object(task, 'task', 'Task_') def read_savefile(self): if os.path.exists(self.io.save_file): @@ -90,6 +84,8 @@ class Game(GameBase): string.digits + string.ascii_letters + string.punctuation + ' '] elif string_option_type == 'map_geometry': return ['Hex', 'Square'] + elif string_option_type == 'thing_type': + return self.thing_types.keys() return None def get_map_geometry_shape(self): @@ -100,7 +96,7 @@ class Game(GameBase): self.io.send('TURN ' + str(self.turn)) for c_id in self.sessions: - player = self.get_thing(self.sessions[c_id], create_unfound = False) + player = self.get_thing(self.sessions[c_id]) visible_terrain = player.fov_stencil_map(self.map) self.io.send('FOV %s' % quote(player.fov_stencil.terrain), c_id) self.io.send('MAP %s %s %s' % (self.get_map_geometry_shape(), @@ -110,7 +106,7 @@ class Game(GameBase): self.io.send('MAP_CONTROL %s' % quote(visible_control), c_id) for t in [t for t in self.things if player.fov_stencil[t.position] == '.']: - self.io.send('THING_POS %s %s' % (t.id_, t.position), c_id) + self.io.send('THING %s %s %s' % (t.position, t.type_, t.id_), c_id) if hasattr(t, 'nickname'): self.io.send('THING_NAME %s %s' % (t.id_, quote(t.nickname)), c_id) @@ -128,7 +124,7 @@ class Game(GameBase): connection_id_found = True break if not connection_id_found: - t = self.get_thing(self.sessions[connection_id], create_unfound=False) + t = self.get_thing(self.sessions[connection_id]) if hasattr(t, 'nickname'): self.io.send('CHAT ' + quote(t.nickname + ' left the map.')) self.things.remove(t) @@ -165,7 +161,7 @@ class Game(GameBase): def cmd_TASK_colon(task_name, game, *args, connection_id): if connection_id not in game.sessions: raise GameError('Not registered as player.') - t = game.get_thing(game.sessions[connection_id], create_unfound=False) + t = game.get_thing(game.sessions[connection_id]) t.set_next_task(task_name, args) def task_prefixed(command_name, task_prefix, task_command): @@ -188,14 +184,8 @@ class Game(GameBase): def new_thing_id(self): if len(self.things) == 0: - return 0 - # DANGEROUS – if anywhere we append a thing to the list of lower - # ID than the highest-value ID, this might lead to re-using an - # already active ID. This condition /should/ not be fulfilled - # anywhere in the code, but if it does, trouble here is one of - # the more obvious indicators that it does – that's why there's - # no safeguard here against this. - return self.things[-1].id_ + 1 + return 1 + return max([t.id_ for t in self.things]) + 1 def save(self): @@ -218,6 +208,8 @@ class Game(GameBase): for tile_class in self.map_control_passwords: write(f, 'MAP_CONTROL_PW %s %s' % (tile_class, self.map_control_passwords[tile_class])) + for t in [t for t in self.things if not t.type_ == 'Player']: + write(f, 'THING %s %s %s' % (t.position, t.type_, t.id_)) def new_world(self, map_geometry): self.map_geometry = map_geometry diff --git a/plomrogue/things.py b/plomrogue/things.py index ee0d501..ecddc87 100644 --- a/plomrogue/things.py +++ b/plomrogue/things.py @@ -6,9 +6,9 @@ from plomrogue.mapping import YX class ThingBase: type_ = '?' - def __init__(self, game, id_=None, position=(YX(0,0))): + def __init__(self, game, id_=0, position=(YX(0,0))): self.game = game - if id_ is None: + if id_ == 0: self.id_ = self.game.new_thing_id() else: self.id_ = id_ @@ -24,6 +24,18 @@ class Thing(ThingBase): def proceed(self): pass + @property + def type_(self): + return self.__class__.get_type() + + @classmethod + def get_type(cls): + return cls.__name__[len('Thing_'):] + + + +class Thing_Stone(Thing): + symbol_hint = 'o' class ThingAnimate(Thing): @@ -88,8 +100,8 @@ class ThingAnimate(Thing): -class ThingPlayer(ThingAnimate): - type_ = 'player' +class Thing_Player(ThingAnimate): + symbol_hint = '@' def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/rogue_chat.py b/rogue_chat.py index 4a9c56f..e25eeac 100755 --- a/rogue_chat.py +++ b/rogue_chat.py @@ -2,13 +2,14 @@ from plomrogue.game import Game from plomrogue.io_websocket import PlomWebSocketServer from plomrogue.io_tcp import PlomTCPServer -from plomrogue.commands import (cmd_ALL, cmd_LOGIN, cmd_NICK, cmd_PING, +from plomrogue.commands import (cmd_ALL, cmd_LOGIN, cmd_NICK, cmd_PING, cmd_THING, cmd_MAP, cmd_TURN, cmd_MAP_LINE, cmd_GET_ANNOTATION, cmd_ANNOTATE, cmd_PORTAL, cmd_GET_GAMESTATE, cmd_TASKS, cmd_MAP_CONTROL_LINE, cmd_MAP_CONTROL_PW, - cmd_GOD_ANNOTATE, cmd_GOD_PORTAL) + cmd_GOD_ANNOTATE, cmd_GOD_PORTAL, cmd_THING_TYPES) from plomrogue.tasks import (Task_WAIT, Task_MOVE, Task_WRITE, Task_FLATTEN_SURROUNDINGS) +from plomrogue.things import Thing_Player, Thing_Stone import sys if len(sys.argv) != 2: @@ -32,10 +33,14 @@ game.register_command(cmd_GOD_ANNOTATE) game.register_command(cmd_GOD_PORTAL) game.register_command(cmd_GET_GAMESTATE) game.register_command(cmd_TASKS) +game.register_command(cmd_THING_TYPES) +game.register_command(cmd_THING) game.register_task(Task_WAIT) game.register_task(Task_MOVE) game.register_task(Task_WRITE) game.register_task(Task_FLATTEN_SURROUNDINGS) +game.register_thing_type(Thing_Player) +game.register_thing_type(Thing_Stone) game.read_savefile() game.io.start_loop() game.io.start_server(8000, PlomWebSocketServer) diff --git a/rogue_chat_curses.py b/rogue_chat_curses.py index 3369aef..0a5ced7 100755 --- a/rogue_chat_curses.py +++ b/rogue_chat_curses.py @@ -72,14 +72,19 @@ 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(game, yx, thing_type, thing_id): + t = game.get_thing(thing_id) + if not t: + t = ThingBase(game, thing_id) + game.things += [t] + t.position = yx + t.type_ = thing_type +cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type int:nonneg' def cmd_THING_NAME(game, thing_id, name): - t = game.get_thing(thing_id, True) - t.name = name + t = game.get_thing(thing_id) + if t: + t.name = name cmd_THING_NAME.argtypes = 'int:nonneg string' def cmd_MAP(game, geometry, size, content): @@ -118,7 +123,7 @@ def cmd_GAME_STATE_COMPLETE(game): game.tui.switch_mode('play') if game.tui.mode.shows_info: game.tui.query_info() - player = game.get_thing(game.player_id, False) + player = game.get_thing(game.player_id) if player.position in game.portals: game.tui.teleport_target_host = game.portals[player.position] game.tui.switch_mode('teleport') @@ -155,14 +160,18 @@ def cmd_TASKS(game, tasks_comma_separated): game.tasks = tasks_comma_separated.split(',') cmd_TASKS.argtypes = 'string' +def cmd_THING_TYPE(game, thing_type, symbol_hint): + game.thing_types[thing_type] = symbol_hint +cmd_THING_TYPE.argtypes = 'string char' + def cmd_PONG(game): pass cmd_PONG.argtypes = '' class Game(GameBase): - thing_type = ThingBase turn_complete = False tasks = {} + thing_types = {} def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -171,7 +180,8 @@ class Game(GameBase): self.register_command(cmd_CHAT) self.register_command(cmd_PLAYER_ID) self.register_command(cmd_TURN) - self.register_command(cmd_THING_POS) + self.register_command(cmd_THING) + self.register_command(cmd_THING_TYPE) self.register_command(cmd_THING_NAME) self.register_command(cmd_MAP) self.register_command(cmd_MAP_CONTROL) @@ -191,6 +201,8 @@ class Game(GameBase): def get_string_options(self, string_option_type): if string_option_type == 'map_geometry': return ['Hex', 'Square'] + elif string_option_type == 'thing_type': + return self.thing_types.keys() return None def get_command(self, command_name): @@ -289,7 +301,9 @@ class TUI: self.socket_thread = threading.Thread(target=self.socket.run) self.socket_thread.start() self.disconnected = False + self.game.thing_types = {} self.socket.send('TASKS') + self.socket.send('THING_TYPES') self.switch_mode('login') except ConnectionRefusedError: self.log_msg('@ server connect failure') @@ -338,7 +352,7 @@ class TUI: self.map_mode = 'terrain' self.mode = getattr(self, 'mode_' + mode_name) if self.mode.shows_info: - player = self.game.get_thing(self.game.player_id, False) + player = self.game.get_thing(self.game.player_id) self.explorer = YX(player.position.y, player.position.x) if self.mode.name == 'waiting_for_server': self.log_msg('@ waiting for server …') @@ -429,7 +443,10 @@ class TUI: info = 'TERRAIN: %s\n' % self.game.map_content[pos_i] for t in self.game.things: if t.position == self.explorer: - info += 'PLAYER @: %s\n' % t.name + info += 'THING: %s' % t.type_ + if hasattr(t, 'name'): + info += ' (name: %s)' % t.name + info += '\n' if self.explorer in self.game.portals: info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n' else: @@ -476,7 +493,8 @@ class TUI: map_lines_split += [list(map_content[start:end])] if self.map_mode == 'terrain': for t in self.game.things: - map_lines_split[t.position.y][t.position.x] = '@' + symbol = self.game.thing_types[t.type_] + map_lines_split[t.position.y][t.position.x] = symbol if self.mode.shows_info: map_lines_split[self.explorer.y][self.explorer.x] = '?' map_lines = [] @@ -490,7 +508,7 @@ class TUI: 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) + player = self.game.get_thing(self.game.player_id) center = player.position if self.mode.shows_info: center = self.explorer diff --git a/rogue_chat_nocanvas_monochrome.html b/rogue_chat_nocanvas_monochrome.html index e699a96..d9732da 100644 --- a/rogue_chat_nocanvas_monochrome.html +++ b/rogue_chat_nocanvas_monochrome.html @@ -177,7 +177,9 @@ let server = { this.websocket = new WebSocket(this.url); this.websocket.onopen = function(event) { server.connected = true; + game.thing_types = {}; server.send(['TASKS']); + server.send(['THING_TYPES']); tui.log_msg("@ server connected! :)"); tui.switch_mode(mode_login); }; @@ -202,12 +204,19 @@ let server = { game.things = {}; game.portals = {}; game.turn = parseInt(tokens[1]); - } else if (tokens[0] === 'THING_POS') { - game.get_thing(tokens[1], true).position = parser.parse_yx(tokens[2]); + } else if (tokens[0] === 'THING') { + let t = game.get_thing(tokens[3], true); + t.position = parser.parse_yx(tokens[1]); + t.type_ = tokens[2]; } else if (tokens[0] === 'THING_NAME') { - game.get_thing(tokens[1], true).name_ = tokens[2]; + let t = game.get_thing(tokens[1], false); + if (t) { + t.name_ = tokens[2]; + }; } else if (tokens[0] === 'TASKS') { game.tasks = tokens[1].split(',') + } else if (tokens[0] === 'THING_TYPE') { + game.thing_types[tokens[1]] = tokens[2] } else if (tokens[0] === 'MAP') { game.map_geometry = tokens[1]; tui.init_keys(); @@ -441,7 +450,8 @@ let tui = { if (this.map_mode == 'terrain') { for (const thing_id in game.things) { let t = game.things[thing_id]; - map_lines_split[t.position[0]][t.position[1]] = '@'; + let symbol = game.thing_types[t.type_]; + map_lines_split[t.position[0]][t.position[1]] = symbol; }; } if (tui.mode.shows_info) { @@ -691,9 +701,9 @@ let explorer = { for (let t_id in game.things) { let t = game.things[t_id]; if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) { - info += "PLAYER @"; + info += "THING: " + t.type_; if (t.name_) { - info += ": " + t.name_; + info += " (name: " + t.name_ + ")"; } info += "\n"; } -- 2.30.2