laneya

multiplayer roguelike game
git clone https://git.ce9e.org/laneya.git

commit
25d1c2c972a4f3d894acd794998984ed71285e37
parent
ca9996d5cf78467b4520f25421faabe932821cf9
Author
Tobias Bengfort <tobias.bengfort@gmx.net>
Date
2015-04-08 06:26
Merge branch 'feature-map' into tmp

Conflicts:
	laneya/client.py
	laneya/server.py

Diffstat

M docs/source/index.rst 1 +
A docs/source/map.rst 6 ++++++
M laneya/actions.py 5 +++++
M laneya/client.py 47 +++++++++++++++++++++++++++++++++++++++++------
A laneya/map.py 256 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
M laneya/server.py 47 ++++++++++++-----------------------------------

6 files changed, 321 insertions, 41 deletions


diff --git a/docs/source/index.rst b/docs/source/index.rst

@@ -10,6 +10,7 @@ Contents
   10    10     quickstart
   11    11     protocol
   12    12     actions
   -1    13     map
   13    14     deferred
   14    15 
   15    16 Indices and tables

diff --git a/docs/source/map.rst b/docs/source/map.rst

@@ -0,0 +1,6 @@
   -1     1 Map and Sprites
   -1     2 ===============
   -1     3 
   -1     4 .. automodule:: laneya.map
   -1     5     :members:
   -1     6     :show-inheritance:

diff --git a/laneya/actions.py b/laneya/actions.py

@@ -35,3 +35,8 @@ def logout():
   35    35 
   36    36     There is no explicit login.  The user is created on her initial request.
   37    37     """
   -1    38 
   -1    39 
   -1    40 def get_map(map_id):
   -1    41     """Ask the server to send a serialisation of the specified map. """
   -1    42     pass

diff --git a/laneya/client.py b/laneya/client.py

@@ -11,15 +11,50 @@ screen.border()
   11    11 class Client(protocol.ClientProtocolFactory):
   12    12     def __init__(self, loop):
   13    13         super(Client, self).__init__(loop)
   14    -1         self.position_x = 0
   15    -1         self.position_y = 0
   -1    14         self.sprites = {}
   -1    15 
   -1    16     def connection_made(self):
   -1    17         self.sendRequest('get_map', map_id='example_map')\
   -1    18             .then(lambda response: self.render_floor(response['data']))
   -1    19 
   -1    20     def render_floor(self, data):
   -1    21         floor_layer = data['floor_layer']
   -1    22         width = len(floor_layer)
   -1    23         height = len(floor_layer[0])
   -1    24 
   -1    25         is_wall = lambda x, y: (
   -1    26             x < 0 or x >= width or
   -1    27             y < 0 or y >= height or
   -1    28             floor_layer[x][y] == 'wall')
   -1    29 
   -1    30         sorrunded = lambda x, y: (
   -1    31             is_wall(x - 1, y) and
   -1    32             is_wall(x - 1, y - 1) and
   -1    33             is_wall(x - 1, y + 1) and
   -1    34             is_wall(x, y - 1) and
   -1    35             is_wall(x + 1, y) and
   -1    36             is_wall(x, y + 1) and
   -1    37             is_wall(x + 1, y - 1) and
   -1    38             is_wall(x + 1, y + 1))
   -1    39 
   -1    40         for x, column in enumerate(floor_layer):
   -1    41             for y, field in enumerate(column):
   -1    42                 if field == 'wall':
   -1    43                     if not sorrunded(x, y):
   -1    44                         screen.putstr(y, x, '#')
   16    45 
   17    46     def update_received(self, action, **kwargs):  # TODO
   18    47         if action == 'position':
   19    -1             screen.delch(self.position_y, self.position_x)
   20    -1             self.position_x = kwargs['x']
   21    -1             self.position_y = kwargs['y']
   22    -1             screen.putstr(self.position_y, self.position_x, 'X')
   -1    48             entity = kwargs['entity']
   -1    49             if entity not in self.sprites:
   -1    50                 self.sprites[entity] = {}
   -1    51             else:
   -1    52                 screen.delch(
   -1    53                     self.sprites[entity]['y'],
   -1    54                     self.sprites[entity]['x'])
   -1    55             self.sprites[entity]['x'] = kwargs['x']
   -1    56             self.sprites[entity]['y'] = kwargs['y']
   -1    57             screen.putstr(kwargs['y'], kwargs['x'], entity[0])
   23    58         screen.refresh()
   24    59 
   25    60     def move(self, direction):

diff --git a/laneya/map.py b/laneya/map.py

@@ -0,0 +1,256 @@
   -1     1 import os
   -1     2 import json
   -1     3 import random
   -1     4 
   -1     5 
   -1     6 class MapManager(object):
   -1     7     """Manager that takes care of generating and storing all maps.
   -1     8 
   -1     9     All maps have the same width/height and identified by X, Y and Z
   -1    10     coordinates. At any combination of coordinates, there can be only one map.
   -1    11 
   -1    12     """
   -1    13     def __init__(self, server, width=60, height=40, persist=True):
   -1    14         self.server = server
   -1    15         self.width = width
   -1    16         self.height = height
   -1    17         self.persist = persist
   -1    18         self.store = {}
   -1    19 
   -1    20     def generate(self, X, Y, Z):
   -1    21         """Generate a new map."""
   -1    22 
   -1    23         _map = Map(self.server, self.width, self.height)
   -1    24         rooms = []
   -1    25 
   -1    26         # make sure user and ghost are inside of a room
   -1    27         rooms.append({
   -1    28             'x_min': 5,
   -1    29             'x_max': 20,
   -1    30             'y_min': 5,
   -1    31             'y_max': 20,
   -1    32         })
   -1    33 
   -1    34         for i in range(2000):
   -1    35             x1 = random.randint(1, self.width - 2)
   -1    36             x2 = random.randint(1, self.width - 2)
   -1    37             y1 = random.randint(1, self.height - 2)
   -1    38             y2 = random.randint(1, self.height - 2)
   -1    39 
   -1    40             room = {
   -1    41                 'x_min': min(x1, x2),
   -1    42                 'x_max': max(x1, x2),
   -1    43                 'y_min': min(y1, y2),
   -1    44                 'y_max': max(y1, y2),
   -1    45             }
   -1    46 
   -1    47             collision_free = lambda other: (
   -1    48                 room['x_min'] > other['x_max'] + 1 or
   -1    49                 room['x_max'] < other['x_min'] - 1 or
   -1    50                 room['y_min'] > other['y_max'] + 1 or
   -1    51                 room['y_max'] < other['y_min'] - 1)
   -1    52 
   -1    53             if (room['x_max'] - room['x_min'] > 2 and
   -1    54                     room['y_max'] - room['y_min'] > 2):
   -1    55                 if all((collision_free(other) for other in rooms)):
   -1    56                     rooms.append(room)
   -1    57 
   -1    58         in_room = lambda x, y, room: (
   -1    59             x >= room['x_min'] and
   -1    60             x <= room['x_max'] and
   -1    61             y >= room['y_min'] and
   -1    62             y <= room['y_max'])
   -1    63 
   -1    64         # carve rooms
   -1    65         for x in range(self.width):
   -1    66             for y in range(self.height):
   -1    67                 if any((in_room(x, y, room) for room in rooms)):
   -1    68                     _map.floor_layer[x][y] = 'floor'
   -1    69                 else:
   -1    70                     _map.floor_layer[x][y] = 'wall'
   -1    71 
   -1    72         # carve paths
   -1    73         for i, room in enumerate(rooms):
   -1    74             if i != 0:
   -1    75                 last = rooms[i - 1]
   -1    76 
   -1    77                 x_center = (room['x_max'] + room['x_min']) / 2
   -1    78                 y_center = (room['y_max'] + room['y_min']) / 2
   -1    79                 last_x_center = (last['x_max'] + last['x_min']) / 2
   -1    80                 last_y_center = (last['y_max'] + last['y_min']) / 2
   -1    81 
   -1    82                 x_min = min(x_center, last_x_center)
   -1    83                 x_max = max(x_center, last_x_center) + 1
   -1    84                 y_min = min(y_center, last_y_center)
   -1    85                 y_max = max(y_center, last_y_center) + 1
   -1    86 
   -1    87                 for x in range(x_min, x_max):
   -1    88                     _map.floor_layer[x][last_y_center] = 'floor'
   -1    89 
   -1    90                 for y in range(y_min, y_max):
   -1    91                     _map.floor_layer[x_center][y] = 'floor'
   -1    92 
   -1    93         return _map
   -1    94 
   -1    95     def get(self, X, Y, Z):
   -1    96         """Get a map.  If it does not exist yet, generate one."""
   -1    97 
   -1    98         key = '%i:%i:%i' % (X, Y, Z)
   -1    99         filename = 'maps/%s.map' % key
   -1   100 
   -1   101         if key not in self.store:
   -1   102             if not os.path.exists('maps'):
   -1   103                 os.mkdir('maps')
   -1   104 
   -1   105             if os.path.exists(filename):
   -1   106                 _map = Map(self.server, self.width, self.height)
   -1   107                 _map.load(filename)
   -1   108             else:
   -1   109                 _map = self.generate(X, Y, Z)
   -1   110                 _map.dump(filename)
   -1   111 
   -1   112             self.store[key] = _map
   -1   113 
   -1   114         return self.store[key]
   -1   115 
   -1   116 
   -1   117 class Map(object):
   -1   118     """A singel map containing sprites.
   -1   119 
   -1   120     The map object takes care of managing all sprites within one map. As only
   -1   121     very few actions involve multiple maps (e.g. moving from one map to
   -1   122     another) this covers a lot of the game logic.
   -1   123 
   -1   124     The passed server is used to send updates to the clients.
   -1   125 
   -1   126     Map objects expose an API for the server (e.g. :py:meth:`step`) and another
   -1   127     one for sprites (e.g. :py:meth:`move_sprite`).
   -1   128 
   -1   129     """
   -1   130     def __init__(self, server, width, height):
   -1   131         self.server = server
   -1   132         self.width = width
   -1   133         self.height = height
   -1   134         self.sprites = {}
   -1   135         self.movable_layer = [
   -1   136             [None for i in xrange(height)] for i in xrange(width)]
   -1   137         self.floor_layer = [
   -1   138             [None for i in xrange(height)] for i in xrange(width)]
   -1   139         self.ghost = Ghost('example', self, 15, 15)
   -1   140 
   -1   141     def step(self):
   -1   142         """Update this map and all of its sprites.
   -1   143 
   -1   144         If this map is currently active (contains at least on user), the server
   -1   145         should call this method once per mainloop cycle.
   -1   146 
   -1   147         """
   -1   148         for sprite in self.sprites.itervalues():
   -1   149             sprite.step()
   -1   150 
   -1   151     def is_collision_free(self, x, y):
   -1   152         """Check whether a sprite can move to field (x, y)."""
   -1   153         return (
   -1   154             self.movable_layer[x][y] is None and
   -1   155             self.floor_layer[x][y] == 'floor')
   -1   156 
   -1   157     def move_sprite(self, sprite, dx, dy):
   -1   158         """Move a sprite."""
   -1   159         if self.is_collision_free(sprite.x + dx, sprite.y + dy):
   -1   160             self.movable_layer[sprite.x][sprite.y] = None
   -1   161             sprite.x += dx
   -1   162             sprite.y += dy
   -1   163             self.movable_layer[sprite.x][sprite.y] = sprite
   -1   164             self.server.broadcastUpdate(
   -1   165                 'position',
   -1   166                 x=sprite.x,
   -1   167                 y=sprite.y,
   -1   168                 entity=sprite.id)
   -1   169 
   -1   170     def encode(self):
   -1   171         return {
   -1   172             'floor_layer': self.floor_layer,
   -1   173         }
   -1   174 
   -1   175     def decode(self, data):
   -1   176         self.floor_layer = data['floor_layer']
   -1   177 
   -1   178     def dump(self, filename):
   -1   179         with open(filename, 'w') as fh:
   -1   180             return json.dump(self.encode(), fh)
   -1   181 
   -1   182     def load(self, filename):
   -1   183         with open(filename) as fh:
   -1   184             self.decode(json.load(fh))
   -1   185 
   -1   186 
   -1   187 class Sprite(object):
   -1   188     """Simple base class for visible game objects."""
   -1   189 
   -1   190     def __init__(self, name, _map, x, y):
   -1   191         self.id = "%s:%s" % (self.__class__.__name__, name)
   -1   192         self.map = _map
   -1   193         self.x = x
   -1   194         self.y = y
   -1   195 
   -1   196         self.map.sprites[self.id] = self
   -1   197 
   -1   198     def kill(self):
   -1   199         """Remove this sprite from the map."""
   -1   200         del self.map.sprites[self.id]
   -1   201 
   -1   202     def step(self):
   -1   203         """Update this sprite.
   -1   204 
   -1   205         This function is executed once per mainloop cycle. Subclasses should
   -1   206         overwrite this in order to define custom behavior.
   -1   207 
   -1   208         """
   -1   209         pass
   -1   210 
   -1   211     def interact(self, other):
   -1   212         """Interact with this sprite.
   -1   213 
   -1   214         Subclasses should overwrite this in order to define custom
   -1   215         interactions.
   -1   216 
   -1   217         """
   -1   218         pass
   -1   219 
   -1   220 
   -1   221 class MovingSprite(Sprite):
   -1   222     """A sprite that can move.
   -1   223 
   -1   224     You can set :py:attr:`direction` to one of 'north', 'east', 'south', 'west'
   -1   225     or 'stop'.  This sprite will then automatically move one filed in the
   -1   226     specified direction in every mainloop cycle.
   -1   227 
   -1   228     """
   -1   229 
   -1   230     def __init__(self, *args, **kwargs):
   -1   231         super(MovingSprite, self).__init__(*args, **kwargs)
   -1   232         self.direction = 'stop'
   -1   233 
   -1   234     def step(self):
   -1   235         if self.direction == 'north':
   -1   236             self.map.move_sprite(self, 0, -1)
   -1   237         elif self.direction == 'east':
   -1   238             self.map.move_sprite(self, 1, 0)
   -1   239         elif self.direction == 'south':
   -1   240             self.map.move_sprite(self, 0, 1)
   -1   241         elif self.direction == 'west':
   -1   242             self.map.move_sprite(self, -1, 0)
   -1   243 
   -1   244 
   -1   245 class User(MovingSprite):
   -1   246     """Sprite representing a user."""
   -1   247 
   -1   248 
   -1   249 class Ghost(MovingSprite):
   -1   250     def step(self):
   -1   251         self.direction = random.choice(
   -1   252             ['north', 'east', 'south', 'west', 'stop'])
   -1   253         super(Ghost, self).step()
   -1   254 
   -1   255 
   -1   256 __all__ = ['MapManager', 'Map', 'Sprite', 'MovingSprite', 'User']

diff --git a/laneya/server.py b/laneya/server.py

@@ -1,62 +1,39 @@
    1     1 import trollius as asyncio
    2     2 
    3     3 import protocol
    4    -1 
    5    -1 
    6    -1 class User(object):
    7    -1     def __init__(self, position_x=0, position_y=0, direction='stop'):
    8    -1         self.position_x = position_x
    9    -1         self.position_y = position_y
   10    -1         self.direction = direction
   -1     4 from map import MapManager, User
   11     5 
   12     6 
   13     7 class Server(protocol.ServerProtocolFactory):
   14     8     def __init__(self):
   15     9         protocol.ServerProtocolFactory.__init__(self)
   16    10         self.users = {}
   17    -1         self.movable_layer = [[None for i in xrange(100)] for i in xrange(100)]
   -1    11         self.map_manager = MapManager(self, 60, 40)
   18    12 
   19    13     def request_received(self, user, action, **kwargs):  # TODO
   20    14         if user not in self.users:
   21    -1             self.users[user] = User()
   -1    15             initial_map = self.map_manager.get(0, 0, 0)
   -1    16             self.users[user] = User(user, initial_map, 10, 10)
   22    17             print("login %s" % user)
   23    18 
   24    19         if action == 'move':
   25    20             self.users[user].direction = kwargs['direction']
   26    21         elif action == 'logout':
   -1    22             self.users[user].kill()
   27    23             del self.users[user]
   28    24             print("logout %s" % user)
   -1    25         elif action == 'get_map':
   -1    26             return self.users[user].map.encode()
   29    27         else:
   30    28             raise protocol.InvalidError
   31    29 
   32    30     def mainloop(self):
   33    -1         for key, user in self.users.iteritems():
   34    -1             if user.direction == 'north':
   35    -1                 self.move_user(user, 0, -1)
   36    -1             elif user.direction == 'east':
   37    -1                 self.move_user(user, 1, 0)
   38    -1             elif user.direction == 'south':
   39    -1                 self.move_user(user, 0, 1)
   40    -1             elif user.direction == 'west':
   41    -1                 self.move_user(user, -1, 0)
   42    -1             if user.direction != 'stop':
   43    -1                 self.broadcast_update(
   44    -1                     'position',
   45    -1                     x=user.position_x,
   46    -1                     y=user.position_y,
   47    -1                     entity=key)
   -1    31         # only the maps with users in them get updated
   -1    32         for _map in self.get_active_maps():
   -1    33             _map.step()
   48    34 
   49    -1     def collision_check(self, new_x, new_y):
   50    -1         return self.movable_layer[new_x][new_y] is None
   51    -1 
   52    -1     def move_user(self, user, dx, dy):
   53    -1         if self.collision_check(user.position_x, user.position_y - 1):
   54    -1             self.movable_layer[user.position_x][user.position_y] = None
   55    -1             user.position_x += dx
   56    -1             user.position_y += dy
   57    -1             self.movable_layer[user.position_x][user.position_y] = user
   58    -1         else:
   59    -1             raise protocol.IllegalError
   -1    35     def get_active_maps(self):
   -1    36         return set([user.map for user in self.users.itervalues()])
   60    37 
   61    38 
   62    39 def main():