Browse Source

Adopt new move algorithms and add move with rotation

Bastien Sevajol 6 years ago
parent
commit
a3551cbbd5

+ 2 - 0
config.yaml View File

@@ -12,9 +12,11 @@ game:
12 12
         walk_ref_time: 3
13 13
         run_ref_time: 1
14 14
         crawl_ref_time: 10
15
+        rotate_ref_time: 0  # seconds per degrees
15 16
         subject:
16 17
           tank1:
17 18
             global_move_coeff: 3
19
+            rotate_ref_time: 0.1111  # seconds per degrees
18 20
     building:
19 21
       draw_interior_gap: 2
20 22
 

+ 6 - 0
opencombat/gui/actor.py View File

@@ -113,6 +113,9 @@ class BaseActor(Actor):
113 113
             )
114 114
         ]
115 115
 
116
+    def can_rotate_instant(self) -> bool:
117
+        return True
118
+
116 119
     def get_animation_appliable_images(
117 120
         self,
118 121
         animation_name: str,
@@ -295,3 +298,6 @@ class HeavyVehicle(BaseActor):
295 298
     def weapons(self) -> typing.List[str]:
296 299
         # TODO BS 2018-01-26: Will be managed by complex part of code
297 300
         return [RIFFLE]
301
+
302
+    def can_rotate_instant(self) -> bool:
303
+        return False

+ 53 - 15
opencombat/gui/base.py View File

@@ -12,11 +12,14 @@ from PIL import Image
12 12
 from pyglet.window import key
13 13
 
14 14
 from cocos.actions import MoveTo as BaseMoveTo
15
+from cocos.actions import MoveTo as RotateTo
15 16
 from synergine2_cocos2d.audio import AudioLibrary as BaseAudioLibrary
16 17
 from synergine2_cocos2d.interaction import InteractionManager
17 18
 from synergine2_cocos2d.middleware import MapMiddleware
18 19
 from synergine2_cocos2d.util import PathManager
19 20
 
21
+from opencombat.gui.animation import ANIMATION_WALK
22
+from opencombat.gui.animation import ANIMATION_CRAWL
20 23
 from opencombat.gui.fire import GuiFiringEvent
21 24
 from opencombat.simulation.interior import InteriorManager
22 25
 from opencombat.simulation.tmx import TileMap
@@ -24,8 +27,6 @@ from opencombat.user_action import UserAction
24 27
 from synergine2.config import Config
25 28
 from synergine2.terminals import Terminal
26 29
 from synergine2_cocos2d.actions import MoveTo
27
-from opencombat.gui.animation import ANIMATION_CRAWL
28
-from opencombat.gui.animation import ANIMATION_WALK
29 30
 from synergine2_cocos2d.animation import Animate
30 31
 from synergine2_cocos2d.gl import draw_line
31 32
 from synergine2_cocos2d.gui import EditLayer as BaseEditLayer
@@ -33,8 +34,7 @@ from synergine2_cocos2d.gui import SubjectMapper
33 34
 from synergine2_cocos2d.gui import Gui
34 35
 from synergine2_cocos2d.gui import TMXGui
35 36
 from synergine2_cocos2d.layer import LayerManager
36
-from synergine2_xyz.move.simulation import FinishMoveEvent
37
-from synergine2_xyz.move.simulation import StartMoveEvent
37
+from opencombat.simulation import move
38 38
 from synergine2_xyz.physics import Physics
39 39
 from synergine2_xyz.utils import get_angle
40 40
 from opencombat.simulation.event import NewVisibleOpponent
@@ -203,16 +203,30 @@ class Game(TMXGui):
203 203
         self.debug_gui = self.config.resolve('global.debug_gui', False)
204 204
 
205 205
         self.terminal.register_event_handler(
206
-            FinishMoveEvent,
206
+            move.SubjectFinishTileMoveEvent,
207
+            self.set_subject_position,
208
+        )
209
+        self.terminal.register_event_handler(
210
+            move.SubjectFinishMoveEvent,
207 211
             self.set_subject_position,
208 212
         )
209 213
 
210 214
         self.terminal.register_event_handler(
211
-            StartMoveEvent,
215
+            move.SubjectStartTileMoveEvent,
212 216
             self.start_move_subject,
213 217
         )
214 218
 
215 219
         self.terminal.register_event_handler(
220
+            move.SubjectStartRotationEvent,
221
+            self.start_rotate_subject,
222
+        )
223
+
224
+        self.terminal.register_event_handler(
225
+            move.SubjectFinishRotationEvent,
226
+            self.rotate_subject,
227
+        )
228
+
229
+        self.terminal.register_event_handler(
216 230
             NewVisibleOpponent,
217 231
             self.new_visible_opponent,
218 232
         )
@@ -257,18 +271,28 @@ class Game(TMXGui):
257 271
         self.layer_manager.interaction_manager.register(MoveCrawlActorInteraction, self.layer_manager)
258 272
         self.layer_manager.interaction_manager.register(FireActorInteraction, self.layer_manager)
259 273
 
260
-    def set_subject_position(self, event: FinishMoveEvent):
274
+    def set_subject_position(
275
+        self,
276
+        event: typing.Union[move.SubjectFinishMoveEvent, move.SubjectFinishTileMoveEvent]
277
+    ):
261 278
         actor = self.layer_manager.subject_layer.subjects_index[event.subject_id]
262
-        new_world_position = self.layer_manager.grid_manager.get_world_position_of_grid_position(event.to_position)
279
+        new_world_position = self.layer_manager\
280
+            .grid_manager\
281
+            .get_world_position_of_grid_position(event.move_to)
263 282
 
264 283
         actor.stop_actions((BaseMoveTo,))
265 284
         actor.set_position(*new_world_position)
266 285
 
267
-    def start_move_subject(self, event: StartMoveEvent):
286
+    def start_move_subject(
287
+        self,
288
+        event: typing.Union[move.SubjectFinishTileMoveEvent, move.SubjectContinueTileMoveEvent, move.SubjectFinishMoveEvent],  # nopep8
289
+    ):
268 290
         actor = self.layer_manager.subject_layer.subjects_index[event.subject_id]
269
-        new_world_position = self.layer_manager.grid_manager.get_world_position_of_grid_position(event.to_position)
270
-        actor_mode = actor.get_mode_for_gui_action(event.gui_action)
291
+        new_world_position = self.layer_manager\
292
+            .grid_manager\
293
+            .get_world_position_of_grid_position(event.move_to)
271 294
 
295
+        # FIXME BS 20180319: compute/config for cycle duration? ?
272 296
         if event.gui_action == UserAction.ORDER_MOVE:
273 297
             animation = ANIMATION_WALK
274 298
             cycle_duration = 2
@@ -283,12 +307,26 @@ class Game(TMXGui):
283 307
                 'Gui action {} unknown'.format(event.gui_action)
284 308
             )
285 309
 
286
-        move_duration = event.move_duration
310
+        move_duration = event.duration
287 311
         move_action = MoveTo(new_world_position, move_duration)
288 312
         actor.do(move_action)
289
-        actor.do(Animate(animation, duration=move_duration, cycle_duration=cycle_duration))
290
-        actor.rotation = get_angle(event.from_position, event.to_position)
291
-        actor.mode = actor_mode
313
+        actor.do(Animate(
314
+            animation,
315
+            duration=move_duration,
316
+            cycle_duration=cycle_duration,
317
+        ))
318
+        if actor.can_rotate_instant():
319
+            actor.rotation = get_angle(actor.subject.position, event.move_to)
320
+        actor.mode = actor.get_mode_for_gui_action(animation)
321
+
322
+    def start_rotate_subject(self, event: move.SubjectStartRotationEvent):
323
+        actor = self.layer_manager.subject_layer.subjects_index[event.subject_id]
324
+        rotate_action = RotateTo(event.rotate_absolute, event.duration)
325
+        actor.do(rotate_action)
326
+
327
+    def rotate_subject(self, event: move.SubjectFinishRotationEvent):
328
+        actor = self.layer_manager.subject_layer.subjects_index[event.subject_id]
329
+        actor.rotation = event.rotation_absolute
292 330
 
293 331
     def new_visible_opponent(self, event: NewVisibleOpponent):
294 332
         self.visible_or_no_longer_visible_opponent(event, (153, 0, 153))

+ 37 - 36
opencombat/simulation/behaviour.py View File

@@ -12,42 +12,43 @@ from opencombat.simulation.event import NewVisibleOpponent
12 12
 from opencombat.simulation.mechanism import OpponentVisibleMechanism
13 13
 from opencombat.user_action import UserAction
14 14
 from synergine2.simulation import Event
15
-from synergine2_xyz.move.simulation import MoveToBehaviour as BaseMoveToBehaviour
16
-
17
-
18
-class MoveToBehaviour(BaseMoveToBehaviour):
19
-    def is_terminated(self) -> bool:
20
-        return COLLECTION_ALIVE not in self.subject.collections
21
-
22
-    def _can_move_to_next_step(self, move_to_data: dict) -> bool:
23
-        if move_to_data['gui_action'] == UserAction.ORDER_MOVE:
24
-            return time.time() - move_to_data['last_intention_time'] >= \
25
-                   self.subject.walk_duration
26
-        if move_to_data['gui_action'] == UserAction.ORDER_MOVE_FAST:
27
-            return time.time() - move_to_data['last_intention_time'] >= \
28
-                   self.subject.run_duration
29
-        if move_to_data['gui_action'] == UserAction.ORDER_MOVE_CRAWL:
30
-            return time.time() - move_to_data['last_intention_time'] >= \
31
-                   self.subject.crawl_duration
32
-
33
-        raise NotImplementedError(
34
-            'Gui action {} unknown'.format(move_to_data['gui_action'])
35
-        )
36
-
37
-    def get_move_duration(self, move_to_data: dict) -> float:
38
-        if move_to_data['gui_action'] == UserAction.ORDER_MOVE:
39
-            return self.subject.walk_duration
40
-        if move_to_data['gui_action'] == UserAction.ORDER_MOVE_FAST:
41
-            return self.subject.run_duration
42
-        if move_to_data['gui_action'] == UserAction.ORDER_MOVE_CRAWL:
43
-            return self.subject.crawl_duration
44
-
45
-        raise NotImplementedError(
46
-            'Gui action {} unknown'.format(move_to_data['gui_action'])
47
-        )
48
-
49
-    def finalize_event(self, move_to_data: dict, event: Event) -> None:
50
-        event.move_duration = self.get_move_duration(move_to_data)
15
+# from synergine2_xyz.move.simulation import MoveToBehaviour as BaseMoveToBehaviour
16
+#
17
+#
18
+# class MoveToBehaviour(BaseMoveToBehaviour):
19
+#     def is_terminated(self) -> bool:
20
+#         return COLLECTION_ALIVE not in self.subject.collections
21
+#
22
+#     def _can_move_to_next_step(self, move_to_data: dict) -> bool:
23
+#         if move_to_data['gui_action'] == UserAction.ORDER_MOVE:
24
+#             return time.time() - move_to_data['last_intention_time'] >= \
25
+#                    self.subject.walk_duration
26
+#         if move_to_data['gui_action'] == UserAction.ORDER_MOVE_FAST:
27
+#             return time.time() - move_to_data['last_intention_time'] >= \
28
+#                    self.subject.run_duration
29
+#         if move_to_data['gui_action'] == UserAction.ORDER_MOVE_CRAWL:
30
+#             return time.time() - move_to_data['last_intention_time'] >= \
31
+#                    self.subject.crawl_duration
32
+#
33
+#         raise NotImplementedError(
34
+#             'Gui action {} unknown'.format(move_to_data['gui_action'])
35
+#         )
36
+#
37
+#     # FIXME remove this func when code with new move
38
+#     def get_move_duration(self, move_to_data: dict) -> float:
39
+#         if move_to_data['gui_action'] == UserAction.ORDER_MOVE:
40
+#             return self.subject.walk_duration
41
+#         if move_to_data['gui_action'] == UserAction.ORDER_MOVE_FAST:
42
+#             return self.subject.run_duration
43
+#         if move_to_data['gui_action'] == UserAction.ORDER_MOVE_CRAWL:
44
+#             return self.subject.crawl_duration
45
+#
46
+#         raise NotImplementedError(
47
+#             'Gui action {} unknown'.format(move_to_data['gui_action'])
48
+#         )
49
+#
50
+#     def finalize_event(self, move_to_data: dict, event: Event) -> None:
51
+#         event.move_duration = self.get_move_duration(move_to_data)
51 52
 
52 53
 
53 54
 class LookAroundBehaviour(AliveSubjectBehaviour):

+ 465 - 0
opencombat/simulation/move.py View File

@@ -0,0 +1,465 @@
1
+# coding: utf-8
2
+import time
3
+import typing
4
+
5
+from synergine2.simulation import SubjectBehaviour, SubjectMechanism
6
+from synergine2.simulation import Event
7
+from synergine2_xyz.move.intention import MoveToIntention
8
+from synergine2_xyz.simulation import XYZSimulation
9
+from synergine2_xyz.utils import get_angle
10
+
11
+from opencombat.const import COLLECTION_ALIVE
12
+from opencombat.user_action import UserAction
13
+
14
+
15
+class SubjectStartRotationEvent(Event):
16
+    def __init__(
17
+        self,
18
+        subject_id: int,
19
+        rotate_relative: float,
20
+        rotate_absolute: float,
21
+        duration: float,
22
+        gui_action: UserAction,
23
+    ) -> None:
24
+        self.subject_id = subject_id
25
+        self.rotate_relative = rotate_relative
26
+        self.rotate_absolute = rotate_absolute
27
+        self.duration = duration
28
+        self.gui_action = gui_action
29
+
30
+
31
+class SubjectContinueRotationEvent(Event):
32
+    def __init__(
33
+        self,
34
+        subject_id: int,
35
+        rotate_relative: float,
36
+        duration: float,
37
+        gui_action: UserAction,
38
+    ) -> None:
39
+        self.subject_id = subject_id
40
+        self.rotate_relative = rotate_relative
41
+        self.duration = duration
42
+        self.gui_action = gui_action
43
+
44
+
45
+class SubjectFinishRotationEvent(Event):
46
+    def __init__(
47
+        self,
48
+        subject_id: int,
49
+        rotation_absolute: float,
50
+        gui_action: UserAction,
51
+    ) -> None:
52
+        self.subject_id = subject_id
53
+        self.rotation_absolute = rotation_absolute
54
+        self.gui_action = gui_action
55
+
56
+
57
+class SubjectStartTileMoveEvent(Event):
58
+    def __init__(
59
+        self,
60
+        subject_id: int,
61
+        move_to: typing.Tuple[int, int],
62
+        duration: float,
63
+        gui_action: UserAction,
64
+    ) -> None:
65
+        self.subject_id = subject_id
66
+        self.move_to = move_to
67
+        self.duration = duration
68
+        self.gui_action = gui_action
69
+
70
+
71
+class SubjectContinueTileMoveEvent(Event):
72
+    def __init__(
73
+        self,
74
+        subject_id: int,
75
+        move_to: typing.Tuple[int, int],
76
+        duration: float,
77
+        gui_action: UserAction,
78
+    ) -> None:
79
+        self.subject_id = subject_id
80
+        self.move_to = move_to
81
+        self.duration = duration
82
+        self.gui_action = gui_action
83
+
84
+
85
+class SubjectFinishTileMoveEvent(Event):
86
+    def __init__(
87
+        self,
88
+        subject_id: int,
89
+        move_to: typing.Tuple[int, int],
90
+        gui_action: UserAction,
91
+    ) -> None:
92
+        self.subject_id = subject_id
93
+        self.move_to = move_to
94
+        self.gui_action = gui_action
95
+
96
+
97
+class SubjectFinishMoveEvent(Event):
98
+    def __init__(
99
+        self,
100
+        subject_id: int,
101
+        move_to: typing.Tuple[int, int],
102
+        gui_action: UserAction,
103
+    ) -> None:
104
+        self.subject_id = subject_id
105
+        self.move_to = move_to
106
+        self.gui_action = gui_action
107
+
108
+
109
+class MoveToMechanism(SubjectMechanism):
110
+    def run(self) -> dict:
111
+        try:
112
+            # TODO: MoveToIntention doit être configurable
113
+            move = self.subject.intentions.get(MoveToIntention)  # type: MoveToIntention
114
+        except KeyError:
115
+            return {}
116
+
117
+        if COLLECTION_ALIVE not in self.subject.collections:
118
+            return {}
119
+
120
+        return move.get_data()
121
+
122
+
123
+class MoveWithRotationBehaviour(SubjectBehaviour):
124
+    use = [MoveToMechanism]
125
+
126
+    def __init__(self, *args, **kwargs):
127
+        super().__init__(*args, **kwargs)
128
+        self.simulation = typing.cast(XYZSimulation, self.simulation)
129
+
130
+    def run(self, data) -> object:
131
+        """
132
+        Compute data relative to move
133
+        """
134
+        data = data[MoveToMechanism]
135
+        if not data:
136
+            return False
137
+
138
+        # Prepare data
139
+        to = data['to']  # type: typing.Tuple(int, int)
140
+        return_data = {}
141
+        now = time.time()
142
+
143
+        # Test if it's first time
144
+        if not data.get('path'):
145
+            return_data['path'] = self.simulation.physics.found_path(
146
+                start=self.subject.position,
147
+                end=to,
148
+                subject=self.subject,
149
+            )
150
+            # find path algorithm can skip start position, add it if not in
151
+            if return_data['path'][0] != self.subject.position:
152
+                return_data['path'] = [self.subject.position] + return_data['path']
153
+            data['path'] = return_data['path']
154
+
155
+        # Prepare data
156
+        path = data['path']  # type: typing.List[typing.Tuple(int, int)]
157
+        path_index = path.index(self.subject.position)
158
+        next_position = path[path_index + 1]
159
+        next_position_direction = get_angle(self.subject.position, next_position)
160
+        rotate_relative = next_position_direction - self.subject.direction
161
+
162
+        # Test if finish move
163
+        if path_index == len(path) - 1:
164
+            return {
165
+                'move_to_finished': to,
166
+                'gui_action': data['gui_action'],
167
+            }
168
+
169
+        # Check if moving
170
+        if self.subject.moving_to == next_position:
171
+            if self.subject.start_move + self.subject.move_duration > now:
172
+                # Let moving
173
+                return {
174
+                    'tile_move_to': next_position,
175
+                    'gui_action': data['gui_action'],
176
+                }
177
+            return_data['tile_move_to_finished'] = self.subject.moving_to
178
+            # Must consider new position of subject
179
+            path_index = path.index(return_data['tile_move_to_finished'])
180
+            if path_index == len(path) - 1:
181
+                return {
182
+                    'move_to_finished': to,
183
+                    'gui_action': data['gui_action'],
184
+                }
185
+            next_position = path[path_index + 1]
186
+            next_position_direction = get_angle(
187
+                return_data['tile_move_to_finished'],
188
+                next_position,
189
+            )
190
+            rotate_relative = next_position_direction - self.subject.direction
191
+
192
+        # Check if rotating
193
+        if self.subject.rotate_to != -1:
194
+            # If it is not finished
195
+            if self.subject.start_rotation + self.subject.rotate_duration > now:
196
+                # Let rotation do it's job
197
+                return {
198
+                    'rotate_relative': rotate_relative,
199
+                    'rotate_absolute': next_position_direction,
200
+                    'gui_action': data['gui_action'],
201
+                }
202
+            # rotation finish
203
+            return_data['rotate_to_finished'] = self.subject.rotate_to
204
+
205
+        # Check if need to rotate
206
+        if not return_data.get('rotate_to_finished') \
207
+                and self.subject.direction != next_position_direction:
208
+            return_data.update({
209
+                'rotate_relative': rotate_relative,
210
+                'rotate_absolute': next_position_direction,
211
+                'gui_action': data['gui_action'],
212
+            })
213
+            return return_data
214
+
215
+        # Need to move to next tile
216
+        return_data['tile_move_to'] = next_position
217
+        return_data['gui_action'] = data['gui_action']
218
+        return return_data
219
+
220
+    def action(self, data) -> [Event]:
221
+        events = []
222
+        now = time.time()
223
+
224
+        if data.get('path'):
225
+            move = self.subject.intentions.get(MoveToIntention)
226
+            move.path = data['path']
227
+            self.subject.intentions.set(move)
228
+
229
+        if data.get('tile_move_to_finished'):
230
+            self.subject.position = data['tile_move_to_finished']
231
+            self.subject.moving_to = (-1, -1)
232
+            self.subject.start_move = -1
233
+            self.subject.move_duration = -1
234
+            events.append(SubjectFinishTileMoveEvent(
235
+                subject_id=self.subject.id,
236
+                move_to=data['tile_move_to_finished'],
237
+                gui_action=data['gui_action'],
238
+            ))
239
+
240
+        if data.get('move_to_finished'):
241
+            self.subject.position = data['move_to_finished']
242
+            self.subject.moving_to = (-1, -1)
243
+            self.subject.start_move = -1
244
+            self.subject.move_duration = -1
245
+            self.subject.intentions.remove(MoveToIntention)
246
+            events.append(SubjectFinishMoveEvent(
247
+                subject_id=self.subject.id,
248
+                move_to=data['move_to_finished'],
249
+                gui_action=data['gui_action'],
250
+            ))
251
+
252
+        if data.get('rotate_to_finished'):
253
+            self.subject.rotate_to = -1
254
+            self.subject.rotate_duration = -1
255
+            self.subject.start_rotation = -1
256
+            self.subject.direction = data['rotate_to_finished']
257
+
258
+            events.append(SubjectFinishRotationEvent(
259
+                subject_id=self.subject.id,
260
+                rotation_absolute=data['rotate_to_finished'],
261
+                gui_action=data['gui_action'],
262
+            ))
263
+
264
+        if data.get('rotate_relative'):
265
+            # Test if rotation is already started
266
+            if self.subject.rotate_to == data['rotate_absolute']:
267
+                # look at progression
268
+                rotate_since = now - self.subject.start_rotation
269
+                rotate_progress = rotate_since / self.subject.rotate_duration
270
+                rotation_to_do = self.subject.rotate_to - self.subject.direction
271
+                rotation_done = rotation_to_do * rotate_progress
272
+                self.subject.direction = self.subject.direction + rotation_done
273
+                rotation_left = self.subject.rotate_to - self.subject.direction
274
+                duration = self.subject.get_rotate_duration(angle=rotation_left)
275
+                self.subject.rotate_duration = duration
276
+                self.subject.start_rotation = now
277
+
278
+                return [SubjectContinueRotationEvent(
279
+                    subject_id=self.subject.id,
280
+                    rotate_relative=rotation_left,
281
+                    duration=duration,
282
+                    gui_action=data['gui_action'],
283
+                )]
284
+            else:
285
+                duration = self.subject.get_rotate_duration(angle=data['rotate_relative'])
286
+                self.subject.rotate_to = data['rotate_absolute']
287
+                self.subject.rotate_duration = duration
288
+                self.subject.start_rotation = time.time()
289
+
290
+                events.append(SubjectStartRotationEvent(
291
+                    subject_id=self.subject.id,
292
+                    rotate_relative=data['rotate_relative'],
293
+                    rotate_absolute=data['rotate_absolute'],
294
+                    duration=duration,
295
+                    gui_action=data['gui_action'],
296
+                ))
297
+
298
+        if data.get('tile_move_to'):
299
+            # It is already moving ?
300
+            if self.subject.moving_to == data.get('tile_move_to'):
301
+                # look at progression
302
+                move_since = now - self.subject.start_move
303
+                move_progress = move_since / self.subject.move_duration
304
+                move_done = self.subject.move_duration * move_progress
305
+                duration = self.subject.move_duration - move_done
306
+                self.subject.move_duration = duration
307
+
308
+                return [SubjectContinueTileMoveEvent(
309
+                    subject_id=self.subject.id,
310
+                    move_to=data['tile_move_to'],
311
+                    duration=duration,
312
+                    gui_action=data['gui_action'],
313
+                )]
314
+            else:
315
+                move = self.subject.intentions.get(MoveToIntention)
316
+                move_type_duration = self.subject.get_move_duration(move)
317
+                # FIXME: duration depend next tile type, etc
318
+                # see opencombat.gui.base.Game#start_move_subject
319
+                duration = move_type_duration * 1
320
+                self.subject.moving_to = data['tile_move_to']
321
+                self.subject.move_duration = duration
322
+                self.subject.start_move = time.time()
323
+                events.append(SubjectStartTileMoveEvent(
324
+                    subject_id=self.subject.id,
325
+                    move_to=data['tile_move_to'],
326
+                    duration=duration,
327
+                    gui_action=data['gui_action'],
328
+                ))
329
+
330
+        return events
331
+
332
+
333
+class MoveBehaviour(SubjectBehaviour):
334
+    use = [MoveToMechanism]
335
+
336
+    def __init__(self, *args, **kwargs):
337
+        super().__init__(*args, **kwargs)
338
+        self.simulation = typing.cast(XYZSimulation, self.simulation)
339
+
340
+    def run(self, data) -> object:
341
+        """
342
+        Compute data relative to move
343
+        """
344
+        data = data[MoveToMechanism]
345
+        if not data:
346
+            return False
347
+
348
+        # Prepare data
349
+        to = data['to']  # type: typing.Tuple(int, int)
350
+        return_data = {}
351
+        now = time.time()
352
+
353
+        # Test if it's first time
354
+        if not data.get('path'):
355
+            return_data['path'] = self.simulation.physics.found_path(
356
+                start=self.subject.position,
357
+                end=to,
358
+                subject=self.subject,
359
+            )
360
+            # find path algorithm can skip start position, add it if not in
361
+            if return_data['path'][0] != self.subject.position:
362
+                return_data['path'] = [self.subject.position] + return_data['path']
363
+            data['path'] = return_data['path']
364
+
365
+        # Prepare data
366
+        path = data['path']  # type: typing.List[typing.Tuple(int, int)]
367
+        path_index = path.index(self.subject.position)
368
+        next_position = path[path_index + 1]
369
+
370
+        # Test if finish move
371
+        if path_index == len(path) - 1:
372
+            return {
373
+                'move_to_finished': to,
374
+                'gui_action': data['gui_action'],
375
+            }
376
+
377
+        # Check if moving
378
+        if self.subject.moving_to == next_position:
379
+            if self.subject.start_move + self.subject.move_duration > now:
380
+                # Let moving
381
+                return {
382
+                    'tile_move_to': next_position,
383
+                    'gui_action': data['gui_action'],
384
+                }
385
+            return_data['tile_move_to_finished'] = self.subject.moving_to
386
+            # Must consider new position of subject
387
+            path_index = path.index(return_data['tile_move_to_finished'])
388
+            if path_index == len(path) - 1:
389
+                return {
390
+                    'move_to_finished': to,
391
+                    'gui_action': data['gui_action'],
392
+                }
393
+            next_position = path[path_index + 1]
394
+
395
+        # Need to move to next tile
396
+        return_data['tile_move_to'] = next_position
397
+        return_data['gui_action'] = data['gui_action']
398
+        return return_data
399
+
400
+    def action(self, data) -> [Event]:
401
+        events = []
402
+        now = time.time()
403
+
404
+        if data.get('path'):
405
+            move = self.subject.intentions.get(MoveToIntention)
406
+            move.path = data['path']
407
+            self.subject.intentions.set(move)
408
+
409
+        if data.get('tile_move_to_finished'):
410
+            self.subject.position = data['tile_move_to_finished']
411
+            self.subject.moving_to = (-1, -1)
412
+            self.subject.start_move = -1
413
+            self.subject.move_duration = -1
414
+            events.append(SubjectFinishTileMoveEvent(
415
+                subject_id=self.subject.id,
416
+                move_to=data['tile_move_to_finished'],
417
+                gui_action=data['gui_action'],
418
+            ))
419
+
420
+        if data.get('move_to_finished'):
421
+            self.subject.position = data['move_to_finished']
422
+            self.subject.moving_to = (-1, -1)
423
+            self.subject.start_move = -1
424
+            self.subject.move_duration = -1
425
+            self.subject.intentions.remove(MoveToIntention)
426
+            events.append(SubjectFinishMoveEvent(
427
+                subject_id=self.subject.id,
428
+                move_to=data['move_to_finished'],
429
+                gui_action=data['gui_action'],
430
+            ))
431
+
432
+        if data.get('tile_move_to'):
433
+            # It is already moving ?
434
+            if self.subject.moving_to == data.get('tile_move_to'):
435
+                # look at progression
436
+                move_since = now - self.subject.start_move
437
+                move_progress = move_since / self.subject.move_duration
438
+                move_done = self.subject.move_duration * move_progress
439
+                duration = self.subject.move_duration - move_done
440
+                self.subject.move_duration = duration
441
+                self.subject.start_move = time.time()
442
+
443
+                return [SubjectContinueTileMoveEvent(
444
+                    subject_id=self.subject.id,
445
+                    move_to=data['tile_move_to'],
446
+                    duration=duration,
447
+                    gui_action=data['gui_action'],
448
+                )]
449
+            else:
450
+                move = self.subject.intentions.get(MoveToIntention)
451
+                move_type_duration = self.subject.get_move_duration(move)
452
+                # FIXME: duration depend next tile type, etc
453
+                # see opencombat.gui.base.Game#start_move_subject
454
+                duration = move_type_duration * 1
455
+                self.subject.moving_to = data['tile_move_to']
456
+                self.subject.move_duration = duration
457
+                self.subject.start_move = time.time()
458
+                events.append(SubjectStartTileMoveEvent(
459
+                    subject_id=self.subject.id,
460
+                    move_to=data['tile_move_to'],
461
+                    duration=duration,
462
+                    gui_action=data['gui_action'],
463
+                ))
464
+
465
+        return events

+ 59 - 8
opencombat/simulation/subject.py View File

@@ -1,12 +1,16 @@
1 1
 # coding: utf-8
2 2
 import typing
3 3
 
4
-from synergine2.simulation import SubjectBehaviourSelector, SubjectBehaviour
4
+from synergine2.simulation import SubjectBehaviourSelector
5
+from synergine2.simulation import SubjectBehaviour
6
+from opencombat.user_action import UserAction
7
+from synergine2_xyz.move.intention import MoveToIntention
5 8
 
6 9
 from opencombat.const import COLLECTION_ALIVE
7 10
 from opencombat.const import COMBAT_MODE_DEFENSE
8 11
 from opencombat.simulation.base import BaseSubject
9
-from opencombat.simulation.behaviour import MoveToBehaviour
12
+from opencombat.simulation.move import MoveBehaviour
13
+from opencombat.simulation.move import MoveWithRotationBehaviour
10 14
 from opencombat.simulation.behaviour import EngageOpponent
11 15
 from opencombat.simulation.behaviour import LookAroundBehaviour
12 16
 from synergine2.share import shared
@@ -24,20 +28,26 @@ class TileSubject(BaseSubject):
24 28
     start_collections = [
25 29
         COLLECTION_ALIVE,
26 30
     ]
27
-    behaviours_classes = [
28
-        MoveToBehaviour,
29
-        LookAroundBehaviour,
30
-        EngageOpponent,
31
-    ]
32 31
     visible_opponent_ids = shared.create_self('visible_opponent_ids', lambda: [])
33 32
     combat_mode = shared.create_self('combat_mode', COMBAT_MODE_DEFENSE)
34 33
     behaviour_selector_class = TileBehaviourSelector
35 34
 
35
+    direction = shared.create_self('direction', 0)
36
+    moving_to = shared.create_self('moving_to', (-1, -1))
37
+    move_duration = shared.create_self('move_duration', -1)
38
+    start_move = shared.create_self('start_move', -1)
39
+
40
+    rotate_to = shared.create_self('rotate_to', -1)
41
+    rotate_duration = shared.create_self('rotate_duration', -1)
42
+    start_rotation = shared.create_self('start_rotation', -1)
43
+
36 44
     def __init__(self, *args, **kwargs):
37 45
         super().__init__(*args, **kwargs)
38 46
         self._walk_ref_time = float(self.config.resolve('game.move.walk_ref_time'))
39 47
         self._run_ref_time = float(self.config.resolve('game.move.run_ref_time'))
40 48
         self._crawl_ref_time = float(self.config.resolve('game.move.crawl_ref_time'))
49
+        self._rotate_ref_time = float(self.config.resolve('game.move.rotate_ref_time'))
50
+        self.direction = kwargs.get('direction', 0)
41 51
 
42 52
     @property
43 53
     def global_move_coeff(self) -> float:
@@ -45,21 +55,58 @@ class TileSubject(BaseSubject):
45 55
 
46 56
     @property
47 57
     def run_duration(self) -> float:
58
+        """
59
+        :return: move to tile time (s) when running
60
+        """
48 61
         return self._run_ref_time * self.global_move_coeff
49 62
 
50 63
     @property
51 64
     def walk_duration(self) -> float:
65
+        """
66
+        :return: move to tile time (s) when walking
67
+        """
52 68
         return self._walk_ref_time * self.global_move_coeff
53 69
 
54 70
     @property
55 71
     def crawl_duration(self) -> float:
72
+        """
73
+        :return: move to tile time (s) when crawling
74
+        """
56 75
         return self._crawl_ref_time * self.global_move_coeff
57 76
 
77
+    def get_rotate_duration(self, angle: float) -> float:
78
+        return angle * self._rotate_ref_time
79
+
80
+    def get_move_duration(self, move: MoveToIntention) -> float:
81
+        gui_action = move.gui_action
82
+
83
+        if gui_action == UserAction.ORDER_MOVE:
84
+            return self.walk_duration
85
+        if gui_action == UserAction.ORDER_MOVE_FAST:
86
+            return self.run_duration
87
+        if gui_action == UserAction.ORDER_MOVE_CRAWL:
88
+            return self.crawl_duration
89
+
90
+        raise NotImplementedError(
91
+            'Gui action {} unknown'.format(move.gui_action)
92
+        )
93
+
58 94
 
59 95
 class ManSubject(TileSubject):
60
-    pass
96
+    behaviours_classes = [
97
+        MoveBehaviour,
98
+        LookAroundBehaviour,
99
+        EngageOpponent,
100
+    ]  # type: typing.List[SubjectBehaviour]
101
+
61 102
 
62 103
 class TankSubject(TileSubject):
104
+    behaviours_classes = [
105
+        MoveWithRotationBehaviour,
106
+        LookAroundBehaviour,
107
+        EngageOpponent,
108
+    ]  # type: typing.List[SubjectBehaviour]
109
+
63 110
     def __init__(self, *args, **kwargs) -> None:
64 111
         super().__init__(*args, **kwargs)
65 112
         # TODO BS 2018-01-26: This coeff will be dependent of real
@@ -68,6 +115,10 @@ class TankSubject(TileSubject):
68 115
             'game.move.subject.tank1.global_move_coeff',
69 116
             3,
70 117
         )
118
+        self._rotate_ref_time = float(self.config.resolve(
119
+            'game.move.subject.tank1.rotate_ref_time',
120
+            0.1111,
121
+        ))
71 122
 
72 123
     @property
73 124
     def global_move_coeff(self) -> float:

+ 6 - 4
opencombat/terminal/base.py View File

@@ -10,16 +10,18 @@ from opencombat.simulation.event import NoLongerVisibleOpponent
10 10
 from opencombat.simulation.physics import TilePhysics
11 11
 from synergine2_cocos2d.terminal import GameTerminal
12 12
 from synergine2_cocos2d.util import get_map_file_path_from_dir
13
-from synergine2_xyz.move.simulation import FinishMoveEvent
14
-from synergine2_xyz.move.simulation import StartMoveEvent
13
+from opencombat.simulation import move
15 14
 
16 15
 
17 16
 class CocosTerminal(GameTerminal):
18 17
     main_process = True
19 18
 
20 19
     subscribed_events = [
21
-        FinishMoveEvent,
22
-        StartMoveEvent,
20
+        move.SubjectFinishTileMoveEvent,
21
+        move.SubjectFinishMoveEvent,
22
+        move.SubjectStartTileMoveEvent,
23
+        move.SubjectStartRotationEvent,
24
+        move.SubjectFinishRotationEvent,
23 25
         NewVisibleOpponent,
24 26
         NoLongerVisibleOpponent,
25 27
         FireEvent,

+ 34 - 0
test_config.yaml View File

@@ -0,0 +1,34 @@
1
+core:
2
+    cycle_duration: 0.25
3
+    use_x_cores: 2
4
+terminals:
5
+    sync: True
6
+game:
7
+    look_around:
8
+        frequency: 1
9
+    engage:
10
+        frequency: 1
11
+    move:
12
+        walk_ref_time: 3
13
+        run_ref_time: 1
14
+        crawl_ref_time: 10
15
+        rotate_ref_time: 0  # seconds per degrees
16
+        subject:
17
+          tank1:
18
+            global_move_coeff: 3
19
+            rotate_ref_time: 0.1111  # seconds per degrees
20
+    building:
21
+      draw_interior_gap: 2
22
+
23
+global:
24
+    cache_dir_path: 'cache'
25
+    include_path:
26
+      maps:
27
+        - "maps"
28
+      graphics:
29
+        - "medias/images"
30
+      sounds:
31
+        - "medias/sounds"
32
+    logging_level: ERROR
33
+    debug: false
34
+    debug_gui: false

+ 2 - 0
tests/conftest.py View File

@@ -0,0 +1,2 @@
1
+# coding: utf-8
2
+from tests.fixtures.main import *

+ 10 - 0
tests/fixtures/main.py View File

@@ -0,0 +1,10 @@
1
+# coding: utf-8
2
+import pytest
3
+from synergine2.config import Config
4
+
5
+
6
+@pytest.fixture()
7
+def config() -> Config:
8
+    config_ = Config()
9
+    config_.load_yaml('test_config.yaml')
10
+    return config_

+ 1 - 0
tests/move/__init__.py View File

@@ -0,0 +1 @@
1
+# coding: utf-8

+ 509 - 0
tests/move/test_move.py View File

@@ -0,0 +1,509 @@
1
+# coding: utf-8
2
+import pytest
3
+from freezegun import freeze_time
4
+from synergine2_xyz.move.intention import MoveToIntention
5
+from synergine2_xyz.simulation import XYZSimulation
6
+
7
+from opencombat.simulation.move import MoveWithRotationBehaviour
8
+from opencombat.simulation.move import MoveToMechanism
9
+from opencombat.simulation.move import MoveBehaviour
10
+from opencombat.simulation.move import SubjectFinishMoveEvent
11
+from opencombat.simulation.move import SubjectStartRotationEvent
12
+from opencombat.simulation.move import SubjectFinishRotationEvent
13
+from opencombat.simulation.move import SubjectContinueRotationEvent
14
+from opencombat.simulation.move import SubjectStartTileMoveEvent
15
+from opencombat.simulation.move import SubjectContinueTileMoveEvent
16
+from opencombat.simulation.move import SubjectFinishTileMoveEvent
17
+from opencombat.simulation.subject import TankSubject
18
+from opencombat.simulation.subject import ManSubject
19
+from opencombat.user_action import UserAction
20
+
21
+
22
+def test_move_and_rotate_behaviour__begin_rotate(config):
23
+    simulation = XYZSimulation(config)
24
+    simulation.physics.graph.add_edge('0.0', '1.1', {})
25
+    simulation.physics.graph.add_edge('1.1', '2.1', {})
26
+
27
+    subject = TankSubject(
28
+        config,
29
+        simulation,
30
+        position=(0, 0),
31
+    )
32
+    move = MoveToIntention(
33
+        to=(2, 1),
34
+        gui_action=UserAction.ORDER_MOVE,
35
+    )
36
+    subject.intentions.set(move)
37
+
38
+    move_behaviour = MoveWithRotationBehaviour(
39
+        config=config,
40
+        simulation=simulation,
41
+        subject=subject,
42
+    )
43
+
44
+    # Rotation required to begin move
45
+    with freeze_time("2000-01-01 00:00:00", tz_offset=0):
46
+        data = move_behaviour.run({MoveToMechanism: move.get_data()})
47
+        assert {
48
+            'gui_action': UserAction.ORDER_MOVE,'gui_action': UserAction.ORDER_MOVE,
49
+            'path': [
50
+                (0, 0),
51
+                (1, 1),
52
+                (2, 1),
53
+            ],
54
+            'rotate_relative': 45,
55
+            'rotate_absolute': 45,
56
+        } == data
57
+
58
+        events = move_behaviour.action(data)
59
+        assert events
60
+        assert 1 == len(events)
61
+        assert isinstance(events[0], SubjectStartRotationEvent)
62
+        assert 45.0 == events[0].rotate_relative
63
+        assert 4.9995 == events[0].duration
64
+        assert subject.position == (0, 0)
65
+        assert subject.direction == 0
66
+        assert subject.rotate_to == 45
67
+        assert subject.start_rotation == 946684800.0
68
+        assert subject.rotate_duration == 4.9995
69
+        assert subject.intentions.get(MoveToIntention)
70
+
71
+    # This is 1 second before end of rotation
72
+    with freeze_time("2000-01-01 00:00:04", tz_offset=0):
73
+        data = move_behaviour.run({MoveToMechanism: move.get_data()})
74
+        assert {
75
+                   'gui_action': UserAction.ORDER_MOVE,
76
+                   'rotate_relative': 45,
77
+                   'rotate_absolute': 45,
78
+        } == data
79
+
80
+        events = move_behaviour.action(data)
81
+        assert 1 == len(events)
82
+        assert isinstance(events[0], SubjectContinueRotationEvent)
83
+        assert 9 == round(events[0].rotate_relative)
84
+        assert 0.9995 == events[0].duration
85
+        assert subject.position == (0, 0)
86
+        assert int(subject.direction) == 36
87
+        assert subject.rotate_to == 45
88
+        assert subject.start_rotation == 946684804.0
89
+        assert subject.rotate_duration == 0.9995
90
+        assert subject.intentions.get(MoveToIntention)
91
+
92
+    # We are now just after rotation duration, a move will start
93
+    with freeze_time("2000-01-01 00:00:05", tz_offset=0):
94
+        data = move_behaviour.run({MoveToMechanism: move.get_data()})
95
+        assert {
96
+                   'gui_action': UserAction.ORDER_MOVE,
97
+                   'tile_move_to': (1, 1),
98
+                   'rotate_to_finished': 45,
99
+        } == data
100
+
101
+        events = move_behaviour.action(data)
102
+        assert 2 == len(events)
103
+        assert isinstance(events[1], SubjectStartTileMoveEvent)
104
+        assert isinstance(events[0], SubjectFinishRotationEvent)
105
+        assert (1, 1) == events[1].move_to
106
+        assert 9.0 == events[1].duration
107
+        assert subject.position == (0, 0)
108
+        assert subject.moving_to == (1, 1)
109
+        assert subject.move_duration == 9.0
110
+        assert subject.start_move == 946684805.0
111
+        assert subject.intentions.get(MoveToIntention)
112
+
113
+    # We are during the move
114
+    with freeze_time("2000-01-01 00:00:13", tz_offset=0):
115
+        data = move_behaviour.run({MoveToMechanism: move.get_data()})
116
+        assert {
117
+                   'gui_action': UserAction.ORDER_MOVE,
118
+                   'tile_move_to': (1, 1),
119
+        } == data
120
+
121
+        events = move_behaviour.action(data)
122
+        assert 1 == len(events)
123
+        assert isinstance(events[0], SubjectContinueTileMoveEvent)
124
+        assert (1, 1) == events[0].move_to
125
+        assert 1.0 == events[0].duration
126
+        assert subject.intentions.get(MoveToIntention)
127
+
128
+    # We are after the move
129
+    with freeze_time("2000-01-01 00:00:14", tz_offset=0):
130
+        data = move_behaviour.run({MoveToMechanism: move.get_data()})
131
+        assert {
132
+                   'gui_action': UserAction.ORDER_MOVE,
133
+                   'tile_move_to_finished': (1, 1),
134
+                   'rotate_relative': 45,
135
+                   'rotate_absolute': 90,
136
+        } == data
137
+
138
+        events = move_behaviour.action(data)
139
+        assert 2 == len(events)
140
+        assert isinstance(events[0], SubjectFinishTileMoveEvent)
141
+        assert isinstance(events[1], SubjectStartRotationEvent)
142
+        assert (1, 1) == events[0].move_to
143
+        assert 4.9995 == events[1].duration
144
+        assert 45 == events[1].rotate_relative
145
+        assert (1, 1) == subject.position
146
+        assert (-1, -1) == subject.moving_to
147
+        assert -1 == subject.start_move
148
+        assert -1 == subject.move_duration
149
+        assert subject.rotate_to == 90
150
+        assert subject.start_rotation == 946684814.0
151
+        assert subject.rotate_duration == 4.9995
152
+        assert subject.intentions.get(MoveToIntention)
153
+
154
+    # We are rotating
155
+    with freeze_time("2000-01-01 00:00:18", tz_offset=0):
156
+        data = move_behaviour.run({MoveToMechanism: move.get_data()})
157
+        assert {
158
+                   'gui_action': UserAction.ORDER_MOVE,
159
+                   'rotate_relative': 45,
160
+                   'rotate_absolute': 90,
161
+        } == data
162
+
163
+        events = move_behaviour.action(data)
164
+        assert 1 == len(events)
165
+        assert isinstance(events[0], SubjectContinueRotationEvent)
166
+        assert 9 == round(events[0].rotate_relative)
167
+        assert 0.9995 == events[0].duration
168
+        assert subject.position == (1, 1)
169
+        assert int(subject.direction) == 81
170
+        assert subject.rotate_to == 90
171
+        assert subject.start_rotation == 946684818.0
172
+        assert subject.rotate_duration == 0.9995
173
+        assert subject.intentions.get(MoveToIntention)
174
+
175
+    # We finish rotating and start to move to final tile
176
+    with freeze_time("2000-01-01 00:00:19", tz_offset=0):
177
+        data = move_behaviour.run({MoveToMechanism: move.get_data()})
178
+        assert {
179
+                   'gui_action': UserAction.ORDER_MOVE,
180
+                   'tile_move_to': (2, 1),
181
+                   'rotate_to_finished': 90,
182
+        } == data
183
+
184
+        events = move_behaviour.action(data)
185
+        assert 2 == len(events)
186
+        assert isinstance(events[1], SubjectStartTileMoveEvent)
187
+        assert isinstance(events[0], SubjectFinishRotationEvent)
188
+        assert (2, 1) == events[1].move_to
189
+        assert 9.0 == events[1].duration
190
+        assert subject.position == (1, 1)
191
+        assert subject.moving_to == (2, 1)
192
+        assert subject.move_duration == 9.0
193
+        assert subject.start_move == 946684819.0
194
+        assert subject.intentions.get(MoveToIntention)
195
+
196
+    # We are moving to final tile
197
+    with freeze_time("2000-01-01 00:00:27", tz_offset=0):
198
+        data = move_behaviour.run({MoveToMechanism: move.get_data()})
199
+        assert {
200
+                   'gui_action': UserAction.ORDER_MOVE,
201
+                   'tile_move_to': (2, 1),
202
+        } == data
203
+
204
+        events = move_behaviour.action(data)
205
+        assert 1 == len(events)
206
+        assert isinstance(events[0], SubjectContinueTileMoveEvent)
207
+        assert (2, 1) == events[0].move_to
208
+        assert 1.0 == events[0].duration
209
+        assert subject.intentions.get(MoveToIntention)
210
+
211
+    # We arrived on final tile
212
+    with freeze_time("2000-01-01 00:00:28", tz_offset=0):
213
+        data = move_behaviour.run({MoveToMechanism: move.get_data()})
214
+        assert {
215
+                   'gui_action': UserAction.ORDER_MOVE,
216
+                   'move_to_finished': (2, 1),
217
+        } == data
218
+
219
+        events = move_behaviour.action(data)
220
+        assert 1 == len(events)
221
+        assert isinstance(events[0], SubjectFinishMoveEvent)
222
+        assert (2, 1) == events[0].move_to
223
+        assert (2, 1) == subject.position
224
+        assert (-1, -1) == subject.moving_to
225
+        assert -1 == subject.start_move
226
+        assert -1 == subject.move_duration
227
+        with pytest.raises(KeyError):
228
+            assert subject.intentions.get(MoveToIntention)
229
+
230
+
231
+def test_move_and_rotate_behaviour__begin_move(config):
232
+    simulation = XYZSimulation(config)
233
+    simulation.physics.graph.add_edge('0.0', '0.1', {})
234
+    simulation.physics.graph.add_edge('0.1', '1.1', {})
235
+
236
+    subject = TankSubject(
237
+        config,
238
+        simulation,
239
+        position=(0, 0),
240
+    )
241
+    move = MoveToIntention(
242
+        to=(1, 1),
243
+        gui_action=UserAction.ORDER_MOVE,
244
+    )
245
+    subject.intentions.set(move)
246
+
247
+    move_behaviour = MoveWithRotationBehaviour(
248
+        config=config,
249
+        simulation=simulation,
250
+        subject=subject,
251
+    )
252
+
253
+    # First is a move
254
+    with freeze_time("2000-01-01 00:00:00", tz_offset=0):
255
+        data = move_behaviour.run({MoveToMechanism: move.get_data()})
256
+        assert {
257
+                   'gui_action': UserAction.ORDER_MOVE,
258
+                   'path': [
259
+                       (0, 0),
260
+                       (0, 1),
261
+                       (1, 1),
262
+                   ],
263
+                   'tile_move_to': (0, 1),
264
+               } == data
265
+
266
+        events = move_behaviour.action(data)
267
+        assert 1 == len(events)
268
+        assert isinstance(events[0], SubjectStartTileMoveEvent)
269
+        assert (0, 1) == events[0].move_to
270
+        assert 9.0 == events[0].duration
271
+        assert subject.intentions.get(MoveToIntention)
272
+
273
+    # Continue the move, rest 1s
274
+    with freeze_time("2000-01-01 00:00:08", tz_offset=0):
275
+        data = move_behaviour.run({MoveToMechanism: move.get_data()})
276
+        assert {
277
+                   'gui_action': UserAction.ORDER_MOVE,
278
+                   'tile_move_to': (0, 1),
279
+               } == data
280
+
281
+        events = move_behaviour.action(data)
282
+        assert 1 == len(events)
283
+        assert isinstance(events[0], SubjectContinueTileMoveEvent)
284
+        assert (0, 1) == events[0].move_to
285
+        assert 1.0 == events[0].duration
286
+        assert subject.intentions.get(MoveToIntention)
287
+
288
+    # Tile move finished, begin a rotate
289
+    with freeze_time("2000-01-01 00:00:09", tz_offset=0):
290
+        data = move_behaviour.run({MoveToMechanism: move.get_data()})
291
+        assert {
292
+                   'gui_action': UserAction.ORDER_MOVE,
293
+                   'tile_move_to_finished': (0, 1),
294
+                   'rotate_relative': 90,
295
+                   'rotate_absolute': 90,
296
+               } == data
297
+
298
+        events = move_behaviour.action(data)
299
+        assert 2 == len(events)
300
+        assert isinstance(events[0], SubjectFinishTileMoveEvent)
301
+        assert isinstance(events[1], SubjectStartRotationEvent)
302
+        assert (0, 1) == events[0].move_to
303
+        assert 10 == round(events[1].duration)
304
+        assert 90 == events[1].rotate_relative
305
+        assert (0, 1) == subject.position
306
+        assert (-1, -1) == subject.moving_to
307
+        assert -1 == subject.start_move
308
+        assert -1 == subject.move_duration
309
+        assert subject.rotate_to == 90
310
+        assert subject.start_rotation == 946684809.0
311
+        assert round(subject.rotate_duration) == 10
312
+        assert subject.intentions.get(MoveToIntention)
313
+
314
+    # We are rotating, rest 1s
315
+    with freeze_time("2000-01-01 00:00:18", tz_offset=0):
316
+        data = move_behaviour.run({MoveToMechanism: move.get_data()})
317
+        assert {
318
+                   'gui_action': UserAction.ORDER_MOVE,
319
+                   'rotate_relative': 90,
320
+                   'rotate_absolute': 90,
321
+               } == data
322
+
323
+        events = move_behaviour.action(data)
324
+        assert 1 == len(events)
325
+        assert isinstance(events[0], SubjectContinueRotationEvent)
326
+        assert 9 == round(events[0].rotate_relative)
327
+        assert 1 == round(events[0].duration)
328
+        assert subject.position == (0, 1)
329
+        assert int(subject.direction) == 81
330
+        assert subject.rotate_to == 90
331
+        assert subject.start_rotation == 946684818.0
332
+        assert round(subject.rotate_duration) == 1
333
+        assert subject.intentions.get(MoveToIntention)
334
+
335
+    # We finish rotating and start to move to final tile
336
+    with freeze_time("2000-01-01 00:00:19", tz_offset=0):
337
+        data = move_behaviour.run({MoveToMechanism: move.get_data()})
338
+        assert {
339
+                   'gui_action': UserAction.ORDER_MOVE,
340
+                   'tile_move_to': (1, 1),
341
+                   'rotate_to_finished': 90,
342
+               } == data
343
+
344
+        events = move_behaviour.action(data)
345
+        assert 2 == len(events)
346
+        assert isinstance(events[1], SubjectStartTileMoveEvent)
347
+        assert isinstance(events[0], SubjectFinishRotationEvent)
348
+        assert (1, 1) == events[1].move_to
349
+        assert 9.0 == events[1].duration
350
+        assert subject.position == (0, 1)
351
+        assert subject.moving_to == (1, 1)
352
+        assert subject.move_duration == 9.0
353
+        assert subject.start_move == 946684819.0
354
+        assert subject.intentions.get(MoveToIntention)
355
+
356
+    # Continue the move, rest 1s
357
+    with freeze_time("2000-01-01 00:00:27", tz_offset=0):
358
+        data = move_behaviour.run({MoveToMechanism: move.get_data()})
359
+        assert {
360
+                   'gui_action': UserAction.ORDER_MOVE,
361
+                   'tile_move_to': (1, 1),
362
+               } == data
363
+
364
+        events = move_behaviour.action(data)
365
+        assert 1 == len(events)
366
+        assert isinstance(events[0], SubjectContinueTileMoveEvent)
367
+        assert (1, 1) == events[0].move_to
368
+        assert 1.0 == events[0].duration
369
+        assert subject.moving_to == (1, 1)
370
+        assert subject.move_duration == 1.0
371
+        assert subject.start_move == 946684819.0
372
+        assert subject.intentions.get(MoveToIntention)
373
+
374
+    # We arrived on final tile
375
+    with freeze_time("2000-01-01 00:00:28", tz_offset=0):
376
+        data = move_behaviour.run({MoveToMechanism: move.get_data()})
377
+        assert {
378
+                   'gui_action': UserAction.ORDER_MOVE,
379
+                   'move_to_finished': (1, 1),
380
+        } == data
381
+
382
+        events = move_behaviour.action(data)
383
+        assert 1 == len(events)
384
+        assert isinstance(events[0], SubjectFinishMoveEvent)
385
+        assert (1, 1) == events[0].move_to
386
+        assert (1, 1) == subject.position
387
+        assert (-1, -1) == subject.moving_to
388
+        assert -1 == subject.start_move
389
+        assert -1 == subject.move_duration
390
+        with pytest.raises(KeyError):
391
+            assert subject.intentions.get(MoveToIntention)
392
+
393
+
394
+def test_move_behaviour(config):
395
+    simulation = XYZSimulation(config)
396
+    simulation.physics.graph.add_edge('0.0', '0.1', {})
397
+    simulation.physics.graph.add_edge('0.1', '1.1', {})
398
+
399
+    subject = ManSubject(
400
+        config,
401
+        simulation,
402
+        position=(0, 0),
403
+    )
404
+    move = MoveToIntention(
405
+        to=(1, 1),
406
+        gui_action=UserAction.ORDER_MOVE,
407
+    )
408
+    subject.intentions.set(move)
409
+
410
+    move_behaviour = MoveBehaviour(
411
+        config=config,
412
+        simulation=simulation,
413
+        subject=subject,
414
+    )
415
+
416
+    # First begin move
417
+    with freeze_time("2000-01-01 00:00:00", tz_offset=0):
418
+        data = move_behaviour.run({MoveToMechanism: move.get_data()})
419
+        assert {
420
+                   'gui_action': UserAction.ORDER_MOVE,
421
+                   'path': [
422
+                       (0, 0),
423
+                       (0, 1),
424
+                       (1, 1),
425
+                   ],
426
+                   'tile_move_to': (0, 1),
427
+               } == data
428
+
429
+        events = move_behaviour.action(data)
430
+        assert 1 == len(events)
431
+        assert isinstance(events[0], SubjectStartTileMoveEvent)
432
+        assert (0, 1) == events[0].move_to
433
+        assert 3.0 == events[0].duration
434
+        assert subject.intentions.get(MoveToIntention)
435
+
436
+    # Continue the move, rest 1s
437
+    with freeze_time("2000-01-01 00:00:02", tz_offset=0):
438
+        data = move_behaviour.run({MoveToMechanism: move.get_data()})
439
+        assert {
440
+                   'gui_action': UserAction.ORDER_MOVE,
441
+                   'tile_move_to': (0, 1),
442
+               } == data
443
+
444
+        events = move_behaviour.action(data)
445
+        assert 1 == len(events)
446
+        assert isinstance(events[0], SubjectContinueTileMoveEvent)
447
+        assert (0, 1) == events[0].move_to
448
+        assert 1.0 == events[0].duration
449
+        assert subject.intentions.get(MoveToIntention)
450
+
451
+    # Tile move finished, begin a new move
452
+    with freeze_time("2000-01-01 00:00:03", tz_offset=0):
453
+        data = move_behaviour.run({MoveToMechanism: move.get_data()})
454
+        assert {
455
+                   'gui_action': UserAction.ORDER_MOVE,
456
+                   'tile_move_to_finished': (0, 1),
457
+                   'tile_move_to': (1, 1),
458
+               } == data
459
+
460
+        events = move_behaviour.action(data)
461
+        assert 2 == len(events)
462
+        assert isinstance(events[0], SubjectFinishTileMoveEvent)
463
+        assert isinstance(events[1], SubjectStartTileMoveEvent)
464
+        assert (0, 1) == events[0].move_to
465
+        assert (1, 1) == events[1].move_to
466
+        assert 3 == events[1].duration
467
+        assert (0, 1) == subject.position
468
+        assert (1, 1) == subject.moving_to
469
+        assert 946684803.0 == subject.start_move
470
+        assert 3 == subject.move_duration
471
+        assert subject.intentions.get(MoveToIntention)
472
+
473
+    # We are moving, rest 1s
474
+    with freeze_time("2000-01-01 00:00:05", tz_offset=0):
475
+        data = move_behaviour.run({MoveToMechanism: move.get_data()})
476
+        assert {
477
+                   'gui_action': UserAction.ORDER_MOVE,
478
+                   'tile_move_to': (1, 1),
479
+               } == data
480
+
481
+        events = move_behaviour.action(data)
482
+        assert 1 == len(events)
483
+        assert isinstance(events[0], SubjectContinueTileMoveEvent)
484
+        assert (1, 1) == events[0].move_to
485
+        assert 1.0 == events[0].duration
486
+        assert (0, 1) == subject.position
487
+        assert (1, 1) == subject.moving_to
488
+        assert 946684805.0 == subject.start_move
489
+        assert 1 == subject.move_duration
490
+        assert subject.intentions.get(MoveToIntention)
491
+
492
+    # We arrived on final tile
493
+    with freeze_time("2000-01-01 00:00:06", tz_offset=0):
494
+        data = move_behaviour.run({MoveToMechanism: move.get_data()})
495
+        assert {
496
+                   'gui_action': UserAction.ORDER_MOVE,
497
+                   'move_to_finished': (1, 1),
498
+        } == data
499
+
500
+        events = move_behaviour.action(data)
501
+        assert 1 == len(events)
502
+        assert isinstance(events[0], SubjectFinishMoveEvent)
503
+        assert (1, 1) == events[0].move_to
504
+        assert (1, 1) == subject.position
505
+        assert (-1, -1) == subject.moving_to
506
+        assert -1 == subject.start_move
507
+        assert -1 == subject.move_duration
508
+        with pytest.raises(KeyError):
509
+            assert subject.intentions.get(MoveToIntention)