home · contact · privacy
Add basic non-player things system.
authorChristian Heller <c.heller@plomlompom.de>
Fri, 13 Nov 2020 22:48:07 +0000 (23:48 +0100)
committerChristian Heller <c.heller@plomlompom.de>
Fri, 13 Nov 2020 22:48:07 +0000 (23:48 +0100)
config.json
plomrogue/commands.py
plomrogue/game.py
plomrogue/things.py
rogue_chat.py
rogue_chat_curses.py
rogue_chat_nocanvas_monochrome.html

index 27853c13cd6a5f21f22cff61cbbd28e2e881ad13..d1682c3f3b62d0519c180584b71c03eadc4bb9a6 100644 (file)
@@ -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",
index 8ec6f524d22e8283ba13fc47424dc4e18352de2d..1c59abfd0b4ca9e4bcb82351297cf1bab59c321b 100644 (file)
@@ -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'
index 0c17977f2d6e2014570332db5e7ffd202e361edc..1ffbf1acfa186f900d96a8252bd5db58dd5fa0a8 100755 (executable)
@@ -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
index ee0d501be22258e69d287da986f8c1bf4d65e978..ecddc8767483dd8599b43a373de5392127fc260e 100644 (file)
@@ -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)
index 4a9c56f923ead32b655da4811a1bd94ba2e7a051..e25eeacf5e8a9ba4f283c4edffe265d71ea80015 100755 (executable)
@@ -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)
index 3369aef86744b5ee00ea5e4b673341da9ff3d627..0a5ced7a0692c27b9211f9f57e7ac752b29fe447 100755 (executable)
@@ -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
index e699a96efee25a3f51970df37916bc3e69eba425..d9732dad32cb3d9e28e97fc8036bdcef3f39c335 100644 (file)
@@ -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";
              }