cmd_TURN.argtypes = 'int:nonneg'
def cmd_ANNOTATE(game, yx, msg, pw, connection_id):
+ player = game.get_thing(game.sessions[connection_id], False)
+ if player.fov_stencil[yx] == '.':
+ raise GameError('cannot annotate tile outside field of view')
if not game.can_do_tile_with_pw(yx, pw):
raise GameError('wrong password for tile')
if msg == ' ':
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)
+ 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):
raise GameError('wrong password for tile')
if msg == ' ':
cmd_GOD_PORTAL.argtypes = 'yx_tuple:nonneg string'
def cmd_GET_ANNOTATION(game, yx, connection_id):
- annotation = '(none)';
- if yx in game.annotations:
- annotation = game.annotations[yx]
+ player = game.get_thing(game.sessions[connection_id], False)
+ annotation = '(unknown)';
+ if player.fov_stencil[yx] == '.':
+ annotation = '(none)';
+ if yx in game.annotations:
+ annotation = game.annotations[yx]
game.io.send('ANNOTATION %s %s' % (yx, quote(annotation)))
cmd_GET_ANNOTATION.argtypes = 'yx_tuple:nonneg'
def send_gamestate(self, connection_id=None):
"""Send out game state data relevant to clients."""
- def send_thing(thing):
- self.io.send('THING_POS %s %s' % (thing.id_, t.position))
- if hasattr(thing, 'nickname'):
- self.io.send('THING_NAME %s %s' % (thing.id_, quote(t.nickname)))
-
self.io.send('TURN ' + str(self.turn))
- for t in self.things:
- send_thing(t)
- self.io.send('MAP %s %s %s' % (self.get_map_geometry_shape(),
- self.map_geometry.size, quote(self.map.terrain)))
- self.io.send('MAP_CONTROL %s' % quote(self.map_control.terrain))
- for yx in self.portals:
- self.io.send('PORTAL %s %s' % (yx, quote(self.portals[yx])))
+ for c_id in self.sessions:
+ player = self.get_thing(self.sessions[c_id], create_unfound = False)
+ 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(),
+ self.map_geometry.size,
+ quote(visible_terrain)), c_id)
+ visible_control = player.fov_stencil_map(self.map_control)
+ 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)
+ if hasattr(t, 'nickname'):
+ self.io.send('THING_NAME %s %s' % (t.id_,
+ quote(t.nickname)), c_id)
+ for yx in [yx for yx in self.portals
+ if player.fov_stencil[yx] == '.']:
+ self.io.send('PORTAL %s %s' % (yx, quote(self.portals[yx])), c_id)
self.io.send('GAME_STATE_COMPLETE')
def run_tick(self):
class MapGeometrySquare(MapGeometryWithLeftRightMoves):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fov_map_class = FovMapSquare
+
def move_UP(self, start_pos):
return YX(start_pos.y - 1, start_pos.x)
return YX(start_pos.y + 1, start_pos.x)
-
class MapGeometryHex(MapGeometryWithLeftRightMoves):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fov_map_class = FovMapHex
+
def move_UPLEFT(self, start_pos):
start_indented = start_pos.y % 2
if start_indented:
width = self.size.x
for y in range(self.size.y):
yield (y, self.terrain[y * width:(y + 1) * width])
+
+
+
+class FovMap(Map):
+
+ def __init__(self, source_map, center):
+ self.source_map = source_map
+ self.size = self.source_map.size
+ self.fov_radius = (self.size.y / 2) - 0.5
+ self.start_indented = True #source_map.start_indented
+ self.terrain = '?' * self.size_i
+ self.center = center
+ self[self.center] = '.'
+ self.shadow_cones = []
+ self.geometry = self.geometry_class(self.size)
+ self.circle_out(self.center, self.shadow_process)
+
+ def shadow_process(self, yx, distance_to_center, dir_i, dir_progress):
+ # Possible optimization: If no shadow_cones yet and self[yx] == '.',
+ # skip all.
+ CIRCLE = 360 # Since we'll float anyways, number is actually arbitrary.
+
+ def correct_arm(arm):
+ if arm > CIRCLE:
+ arm -= CIRCLE
+ return arm
+
+ def in_shadow_cone(new_cone):
+ for old_cone in self.shadow_cones:
+ if old_cone[0] <= new_cone[0] and \
+ new_cone[1] <= old_cone[1]:
+ return True
+ # We might want to also shade tiles whose middle arm is inside a
+ # shadow cone for a darker FOV. Note that we then could not for
+ # optimization purposes rely anymore on the assumption that a
+ # shaded tile cannot add growth to existing shadow cones.
+ return False
+
+ def merge_cone(new_cone):
+ import math
+ for old_cone in self.shadow_cones:
+ if new_cone[0] < old_cone[0] and \
+ (new_cone[1] > old_cone[0] or
+ math.isclose(new_cone[1], old_cone[0])):
+ old_cone[0] = new_cone[0]
+ return True
+ if new_cone[1] > old_cone[1] and \
+ (new_cone[0] < old_cone[1] or
+ math.isclose(new_cone[0], old_cone[1])):
+ old_cone[1] = new_cone[1]
+ return True
+ return False
+
+ def eval_cone(cone):
+ if in_shadow_cone(cone):
+ return
+ self[yx] = '.'
+ if self.source_map[yx] == 'X':
+ unmerged = True
+ while merge_cone(cone):
+ unmerged = False
+ if unmerged:
+ self.shadow_cones += [cone]
+
+ step_size = (CIRCLE/len(self.circle_out_directions)) / distance_to_center
+ number_steps = dir_i * distance_to_center + dir_progress
+ left_arm = correct_arm(step_size/2 + step_size*number_steps)
+ right_arm = correct_arm(left_arm + step_size)
+
+ # Optimization potential: left cone could be derived from previous
+ # right cone. Better even: Precalculate all cones.
+ if right_arm < left_arm:
+ eval_cone([left_arm, CIRCLE])
+ eval_cone([0, right_arm])
+ else:
+ eval_cone([left_arm, right_arm])
+
+ def basic_circle_out_move(self, pos, direction):
+ """Move position pos into direction. Return whether still in map."""
+ mover = getattr(self.geometry, 'move_' + direction)
+ pos = mover(pos) #, self.start_indented)
+ if pos.y < 0 or pos.x < 0 or \
+ pos.y >= self.size.y or pos.x >= self.size.x:
+ return pos, False
+ return pos, True
+
+ def circle_out(self, yx, f):
+ # Optimization potential: Precalculate movement positions. (How to check
+ # circle_in_map then?)
+ # Optimization potential: Precalculate what tiles are shaded by what tile
+ # and skip evaluation of already shaded tile. (This only works if tiles
+ # shading implies they completely lie in existing shades; otherwise we
+ # would lose shade growth through tiles at shade borders.)
+ circle_in_map = True
+ distance = 1
+ yx = YX(yx.y, yx.x)
+ while circle_in_map:
+ if distance > self.fov_radius:
+ break
+ circle_in_map = False
+ yx, _ = self.basic_circle_out_move(yx, 'RIGHT')
+ for dir_i in range(len(self.circle_out_directions)):
+ for dir_progress in range(distance):
+ direction = self.circle_out_directions[dir_i]
+ yx, test = self.circle_out_move(yx, direction)
+ if test:
+ f(yx, distance, dir_i, dir_progress)
+ circle_in_map = True
+ distance += 1
+
+
+
+class FovMapHex(FovMap):
+ circle_out_directions = ('DOWNLEFT', 'LEFT', 'UPLEFT',
+ 'UPRIGHT', 'RIGHT', 'DOWNRIGHT')
+ geometry_class = MapGeometryHex
+
+ def circle_out_move(self, yx, direction):
+ return self.basic_circle_out_move(yx, direction)
+
+
+
+class FovMapSquare(FovMap):
+ circle_out_directions = (('DOWN', 'LEFT'), ('LEFT', 'UP'),
+ ('UP', 'RIGHT'), ('RIGHT', 'DOWN'))
+ geometry_class = MapGeometrySquare
+
+ def circle_out_move(self, yx, direction):
+ yx, _ = self.basic_circle_out_move(yx, direction[0])
+ return self.basic_circle_out_move(yx, direction[1])
super().__init__(*args, **kwargs)
self.next_tasks = []
self.set_task('WAIT')
+ self._fov = None
def set_task(self, task_name, args=()):
task_class = self.game.tasks[task_name]
return None
def proceed(self):
+ self._fov = None
if self.task is None:
self.task = self.get_next_task()
return
self.game.changed = True
self.task = self.get_next_task()
+ @property
+ def fov_stencil(self):
+ if self._fov:
+ return self._fov
+ fov_map_class = self.game.map_geometry.fov_map_class
+ self._fov = fov_map_class(self.game.map, self.position)
+ return self._fov
+
+ def fov_stencil_map(self, map):
+ visible_terrain = ''
+ for i in range(self.fov_stencil.size_i):
+ if self.fov_stencil.terrain[i] == '.':
+ visible_terrain += map.terrain[i]
+ else:
+ visible_terrain += ' '
+ return visible_terrain
+
class ThingPlayer(ThingAnimate):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.nickname = 'undefined'
-
}
cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
+def cmd_FOV(game, content):
+ game.fov = content
+cmd_FOV.argtypes = 'string'
+
def cmd_MAP_CONTROL(game, content):
game.map_control_content = content
cmd_MAP_CONTROL.argtypes = 'string'
self.register_command(cmd_GAME_ERROR)
self.register_command(cmd_PLAY_ERROR)
self.register_command(cmd_TASKS)
+ self.register_command(cmd_FOV)
self.map_content = ''
self.player_id = -1
self.info_db = {}
self.disconnected = True
self.force_instant_connect = True
self.input_lines = []
+ self.fov = ''
curses.wrapper(self.loop)
def flash(self):
if not self.game.turn_complete:
return
pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
- 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
- if self.explorer in self.game.portals:
- info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
- else:
- info += 'PORTAL: (none)\n'
- if self.explorer in self.game.info_db:
- info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
- else:
- info += 'ANNOTATION: waiting …'
+ info = 'outside field of view'
+ if self.game.fov[pos_i] == '.':
+ 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
+ if self.explorer in self.game.portals:
+ info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
+ else:
+ info += 'PORTAL: (none)\n'
+ if self.explorer in self.game.info_db:
+ info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
+ else:
+ info += 'ANNOTATION: waiting …'
lines = msg_into_lines_of_width(info, self.window_width)
height_header = 2
for i in range(len(lines)):
tui.init_keys();
game.map_size = parser.parse_yx(tokens[2]);
game.map = tokens[3]
+ } else if (tokens[0] === 'FOV') {
+ game.fov = tokens[1]
} else if (tokens[0] === 'MAP_CONTROL') {
game.map_control = tokens[1]
} else if (tokens[0] === 'GAME_STATE_COMPLETE') {
server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
},
get_info: function() {
- let info = "";
let position_i = this.position[0] * game.map_size[1] + this.position[1];
+ if (game.fov[position_i] != '.') {
+ return 'outside field of view';
+ };
+ let info = "";
info += "TERRAIN: " + game.map[position_i] + "\n";
for (let t_id in game.things) {
let t = game.things[t_id];