- 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 = 015 -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 protocol4 -15 -16 -1 class User(object):7 -1 def __init__(self, position_x=0, position_y=0, direction='stop'):8 -1 self.position_x = position_x9 -1 self.position_y = position_y10 -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 3449 -1 def collision_check(self, new_x, new_y):50 -1 return self.movable_layer[new_x][new_y] is None51 -152 -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] = None55 -1 user.position_x += dx56 -1 user.position_y += dy57 -1 self.movable_layer[user.position_x][user.position_y] = user58 -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():