Skip to content

Commit 6268035

Browse files
committed
Add squad based movement to formation
Formation as a hypothetical leader position that it follows. Uses path reconstruction to get back on track when unit has to go off the track because next position is blocked. Still some TODOs are left.
1 parent 01112c3 commit 6268035

File tree

6 files changed

+89
-66
lines changed

6 files changed

+89
-66
lines changed

formation.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,46 @@
66

77

88
class Formation:
9+
"""
10+
Formation represents a group of units that move together. The leader position
11+
can be any unit and does not need to be centered on any particular unit. All
12+
units move relative to the leader position which advances one step towards
13+
destination on each turn.
14+
"""
915

10-
def __init__(self, index, rel_pos, leader_index, units, is_valid_pos):
16+
def __init__(self, index, rel_pos, leader_pos, units, is_valid_pos):
17+
"""
18+
Formation object is unique to each unit.
19+
20+
Args:
21+
index (int): index of unit in position list
22+
rel_pos (List[Point]): relative position of other units to leader_pos
23+
leader_pos (Point): leader position
24+
units (List[Point]): unit positions
25+
is_valid_pos (func): function to determine if postion is valid
26+
"""
1127
self.index = index
12-
self.leader_index = leader_index
1328
self.units = units
14-
self.leader = units[leader_index]
29+
self.leader = leader_pos
1530
self.rel_pos = rel_pos
16-
self.turn_queue = cycle(units)
1731
self.is_valid_pos = is_valid_pos
1832
self.path_finder = PathFinder(diagonal_cost(), diagonal_cost(), self.is_valid_pos)
19-
self.leader_path = None
2033
self.moves = adjacent_octile()
2134
self.dest = None
2235

23-
def update_units(self, units):
36+
def update_units(self, units, leader_pos):
2437
"""
2538
Update formation unit positions. Decentralized formation requires
2639
each unit to observe its neighbours and perform updates.
2740
41+
Note: Should only be called when leader_path is not empty
42+
2843
Args:
2944
units (List[Unit]): List of units
45+
leader_pos (Point): Leader position
3046
"""
3147
self.units = units
32-
self.leader = units[self.leader_index]
48+
self.leader = leader_pos
3349

3450
def init_dest(self, dest):
3551
"""
@@ -40,7 +56,7 @@ def init_dest(self, dest):
4056
dest (Point): destination point
4157
"""
4258
self.dest = dest
43-
self.leader_path = self.path_finder.find_path(self.moves, self.leader, dest)
59+
return self.path_finder.find_path(self.moves, self.leader, dest)
4460

4561
def find_path(self, src, dest):
4662
"""
@@ -65,4 +81,7 @@ def predict_pos_from(self, pos):
6581
possible_pos = pos + self.rel_pos[self.index]
6682
store = self.path_finder.generic_a_star(self.moves, pos, possible_pos, 30)
6783
score_points = [(possible_pos.dist(pos), pos) for pos in store.keys()]
68-
return min(score_points)
84+
if score_points:
85+
return min(score_points)[1]
86+
else:
87+
return None

game_map.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ def __init__(self, map_data):
99
def is_valid_point(self, pos):
1010
"""
1111
Checks static points and active points. Returns
12-
true if pos is unoccupied in either
12+
true if pos is unoccupied in both
1313
1414
Args:
1515
pos (Point): position to check
1616
"""
17-
return self.static[pos.y][pos.x] and any(lambda x: x.cur_pos == pos for x in self.active)
17+
return 0 <= pos.x < 100 and 0 <= pos.y < 100 and self.static[pos.y][pos.x] and all(lambda x: x.cur_pos != pos for x in self.active)
1818

1919
def valid_next_pos(self, cur_unit):
2020
return [pos for pos in cur_unit.next_pos() if self.is_valid_point(pos)]

main.py

Lines changed: 22 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -23,37 +23,27 @@ def is_valid_pos(cur_pos):
2323
if __name__ == "__main__":
2424
map_data = map_generator.generate_map(100, 100, seed=25, obstacle_density=0.35)
2525
game_map = Map(map_data)
26-
start = Point(18, 65)
26+
start = Point(2, 2)
2727
end = Point(25, 65)
28-
cur_unit = SCOUT.copy(start)
29-
cur_unit.set_game_state(game_map)
30-
game_map.active.append(cur_unit)
31-
cur_unit.find_path(end)
32-
anim = animate_game_state(game_map)
28+
rel_pos = [
29+
Point(-1, 1),
30+
Point(1, 1),
31+
Point(0, 0),
32+
Point(-1, -1),
33+
Point(1, -1),
34+
]
35+
36+
units_pos = [start + pos for pos in rel_pos]
37+
units = []
38+
for i, unit_pos in enumerate(units_pos):
39+
unit = FORMATION.copy(start + rel_pos[i])
40+
unit.add_to_formation(Formation(i, rel_pos, start, units_pos, game_map.is_valid_point), i == 2) # 2nd index unit is leader
41+
unit.set_dest(end)
42+
unit.game_map = game_map
43+
units.append(unit)
44+
45+
game_map = Map(map_data)
46+
game_map.active = units
47+
48+
anim = animate_game_state(game_map, frames=200)
3349
anim.save('run_game.gif', writer='imagemagick', fps=10)
34-
# rel_pos = [
35-
# Point(-1, 1),
36-
# Point(1, 1),
37-
# Point(0, 0),
38-
# Point(-1, -1),
39-
# Point(1, -1),
40-
# ]
41-
# units_pos = [start + pos for pos in rel_pos]
42-
# units = []
43-
# for i, unit_pos in enumerate(units_pos):
44-
# unit = FORMATION.copy(start + rel_pos[i])
45-
# unit.add_to_formation(Formation(i, rel_pos, 2, units_pos, is_valid_pos), i == 2) # 2nd index unit is leader
46-
# unit.set_dest(end)
47-
# units.append(unit)
48-
49-
# game_map = Map(map_data)
50-
# game_map.active = units
51-
52-
# for unit in cycle(units):
53-
# if unit.is_leader and unit.cur_pos == end:
54-
# break
55-
56-
# units_pos = [unit.cur_pos for unit in units]
57-
# unit.formation.update_units(units_pos)
58-
# unit.cur_pos = unit.next_pos()
59-
# print(units)

mapworks/visualization.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
def animate_game_state(game_map, frames=10, grid=True):
1212

1313
fig = plt.figure()
14+
fig.set_size_inches(12, 12, True)
1415
ax = plt.axes()
1516
active_scatter = None
1617
mock_scatter = None
@@ -43,7 +44,7 @@ def update_plot(i):
4344
active_scatter.set_offsets(units_pos)
4445
return active_scatter,
4546

46-
return animation.FuncAnimation(fig, update_plot, frames=frames, blit=False)
47+
return animation.FuncAnimation(fig, update_plot, frames=frames, blit=True)
4748

4849

4950
def view_game_state(game_map, view_mock=False, grid=True):
@@ -57,6 +58,7 @@ def view_game_state(game_map, view_mock=False, grid=True):
5758
Args:
5859
game_map (Map object)
5960
view_mock (Boolean): mock objects are shown if true
61+
grid (Boolean): True displays grid
6062
"""
6163
# show active units
6264
unit_points = [game_unit.cur_pos for game_unit in game_map.active]
@@ -88,7 +90,6 @@ def view_game_state(game_map, view_mock=False, grid=True):
8890
plt.connect('button_press_event', mouse_move)
8991
plt.grid(grid)
9092
plt.show()
91-
return
9293

9394

9495
def view_map(map_data, stores=None, paths=None, points=None, grid=True):

path_finding.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,10 @@ def best_potential_step(self, game_map, cur_unit):
203203
If path and range is given, finds the best potential score point
204204
within the range of the next step prescribed by path.
205205
206+
Note: function does not compare does not calculate score of current
207+
position. This may cause thrashing as unit might try to move to points
208+
that are not an improvement on the current position. TODO
209+
206210
Args:
207211
cur_pos Point: current unit
208212
path List(Point): prescribed path from source to destination
@@ -216,24 +220,24 @@ def best_potential_step(self, game_map, cur_unit):
216220
best_state = min(potential_values)
217221
return best_state[1]
218222

219-
def reconstruct_path(self, cur_pos, path):
223+
def reconstruct_path(self, moves, cur_pos, path):
220224
"""
221225
TODO
222226
Reconstructs a new path from current position,
223-
that joins the given path at some point
227+
that joins the given path at some point.
228+
229+
Note: consider intersecting intersecting further along the
230+
path based on point sensitivity TODO
224231
225232
Args:
233+
moves: set of moves possible on each turn
226234
cur_pos: current position of unit
227235
path: previous path from which unit has deviated
228236
229237
Return:
230238
New path
231239
"""
232-
# check if path is None and return None
233-
# Find position of intersection with path,
234-
# simple case is head of path
235-
# complex case can be to analyze sensitivity of path points
236-
# and set intersection at most sensitive point
237-
# calculate path from cur_pos to intersection
238-
# return concatenated path
239-
return None
240+
dest = path.popleft()
241+
correction = self.find_path(moves, cur_pos, dest)
242+
path.extendleft(correction.reverse())
243+
return path

unit.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from collections import deque
22

3+
from mock_object import PathObject
34
from path_finding import PathFinder
45
from movement_cost import diagonal_cost, linear_cost
56
from moves import adjacent_linear, bc19_9_radius, adjacent_octile
@@ -84,46 +85,54 @@ def __init__(self, *args):
8485
self.leader_path = None
8586
self.path = None
8687
self.game_map = None
88+
self.at_objective = False # Temporary measure should be replace by concept of unit state
8789

8890
def copy(self, cur_pos):
8991
if not cur_pos:
9092
cur_pos = self.cur_pos
91-
return FormationUnit(cur_pos, self.poten_func, self.next_moves, self.move_cost_func, self.sight_range)
93+
return FormationUnit(cur_pos, self.name, self.poten_func, self.next_moves, self.move_cost_func, self.sight_range)
9294

9395
def add_to_formation(self, formation, is_leader):
9496
self.is_leader = is_leader
9597
self.formation = formation
9698

9799
def set_dest(self, dest):
98-
self.formation.init_dest(dest)
99-
if self.is_leader:
100-
self.path = deque(self.formation.leader_path)
101-
else:
102-
self.leader_path = deque(self.formation.leader_path)
100+
self.leader_path = deque(self.formation.init_dest(dest))
103101

104102
def find_path(self):
105103
if len(self.leader_path) > FormationUnit.MAX_PREDICT:
106104
future_leader_pos = self.leader_path[FormationUnit.MAX_PREDICT]
107105
else:
108106
future_leader_pos = self.leader_path[-1]
109107

110-
_, short_dest = self.formation.predict_pos_from(future_leader_pos)
111-
self.path = deque(self.formation.find_path(self.cur_pos, short_dest))
108+
short_dest = self.formation.predict_pos_from(future_leader_pos)
109+
if not short_dest:
110+
self.path = deque([self.cur_pos])
111+
else:
112+
self.path = deque(self.formation.find_path(self.cur_pos, short_dest))
112113

113114
def update_pos(self):
115+
if self.at_objective:
116+
return self.cur_pos
117+
114118
if not self.path:
115119
self.find_path()
116120

121+
self.update_formation()
117122
self.leader_path.popleft()
123+
if not self.leader_path: # reached objective when leader path is empty
124+
self.at_objective = True
118125
next_pos = self.path.popleft()
119-
if self.game_map.is_valid_pos(next_pos):
126+
if self.game_map.is_valid_point(next_pos):
120127
self.cur_pos = next_pos
121128
else:
122-
self.cur_pos = next_pos
129+
vis_path = [pos for pos in self.path if self.can_see_point(pos)]
130+
self.game_map.mock = [PathObject(vis_path)]
131+
self.cur_pos = self.path_finder.best_potential_step(self.game_map, self)
123132

124133
def update_formation(self):
125134
leader_pos = self.leader_path[0]
126-
units = self.game_map.active
135+
units = self.game_map.active # temporary measure as all active units are part of formation
127136
self.formation.update_units(units, leader_pos)
128137

129138
def __str__(self):

0 commit comments

Comments
 (0)