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
         walk_ref_time: 3
12
         walk_ref_time: 3
13
         run_ref_time: 1
13
         run_ref_time: 1
14
         crawl_ref_time: 10
14
         crawl_ref_time: 10
15
+        rotate_ref_time: 0  # seconds per degrees
15
         subject:
16
         subject:
16
           tank1:
17
           tank1:
17
             global_move_coeff: 3
18
             global_move_coeff: 3
19
+            rotate_ref_time: 0.1111  # seconds per degrees
18
     building:
20
     building:
19
       draw_interior_gap: 2
21
       draw_interior_gap: 2
20
 
22
 

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

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

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

12
 from pyglet.window import key
12
 from pyglet.window import key
13
 
13
 
14
 from cocos.actions import MoveTo as BaseMoveTo
14
 from cocos.actions import MoveTo as BaseMoveTo
15
+from cocos.actions import MoveTo as RotateTo
15
 from synergine2_cocos2d.audio import AudioLibrary as BaseAudioLibrary
16
 from synergine2_cocos2d.audio import AudioLibrary as BaseAudioLibrary
16
 from synergine2_cocos2d.interaction import InteractionManager
17
 from synergine2_cocos2d.interaction import InteractionManager
17
 from synergine2_cocos2d.middleware import MapMiddleware
18
 from synergine2_cocos2d.middleware import MapMiddleware
18
 from synergine2_cocos2d.util import PathManager
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
 from opencombat.gui.fire import GuiFiringEvent
23
 from opencombat.gui.fire import GuiFiringEvent
21
 from opencombat.simulation.interior import InteriorManager
24
 from opencombat.simulation.interior import InteriorManager
22
 from opencombat.simulation.tmx import TileMap
25
 from opencombat.simulation.tmx import TileMap
24
 from synergine2.config import Config
27
 from synergine2.config import Config
25
 from synergine2.terminals import Terminal
28
 from synergine2.terminals import Terminal
26
 from synergine2_cocos2d.actions import MoveTo
29
 from synergine2_cocos2d.actions import MoveTo
27
-from opencombat.gui.animation import ANIMATION_CRAWL
28
-from opencombat.gui.animation import ANIMATION_WALK
29
 from synergine2_cocos2d.animation import Animate
30
 from synergine2_cocos2d.animation import Animate
30
 from synergine2_cocos2d.gl import draw_line
31
 from synergine2_cocos2d.gl import draw_line
31
 from synergine2_cocos2d.gui import EditLayer as BaseEditLayer
32
 from synergine2_cocos2d.gui import EditLayer as BaseEditLayer
33
 from synergine2_cocos2d.gui import Gui
34
 from synergine2_cocos2d.gui import Gui
34
 from synergine2_cocos2d.gui import TMXGui
35
 from synergine2_cocos2d.gui import TMXGui
35
 from synergine2_cocos2d.layer import LayerManager
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
 from synergine2_xyz.physics import Physics
38
 from synergine2_xyz.physics import Physics
39
 from synergine2_xyz.utils import get_angle
39
 from synergine2_xyz.utils import get_angle
40
 from opencombat.simulation.event import NewVisibleOpponent
40
 from opencombat.simulation.event import NewVisibleOpponent
203
         self.debug_gui = self.config.resolve('global.debug_gui', False)
203
         self.debug_gui = self.config.resolve('global.debug_gui', False)
204
 
204
 
205
         self.terminal.register_event_handler(
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
             self.set_subject_position,
211
             self.set_subject_position,
208
         )
212
         )
209
 
213
 
210
         self.terminal.register_event_handler(
214
         self.terminal.register_event_handler(
211
-            StartMoveEvent,
215
+            move.SubjectStartTileMoveEvent,
212
             self.start_move_subject,
216
             self.start_move_subject,
213
         )
217
         )
214
 
218
 
215
         self.terminal.register_event_handler(
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
             NewVisibleOpponent,
230
             NewVisibleOpponent,
217
             self.new_visible_opponent,
231
             self.new_visible_opponent,
218
         )
232
         )
257
         self.layer_manager.interaction_manager.register(MoveCrawlActorInteraction, self.layer_manager)
271
         self.layer_manager.interaction_manager.register(MoveCrawlActorInteraction, self.layer_manager)
258
         self.layer_manager.interaction_manager.register(FireActorInteraction, self.layer_manager)
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
         actor = self.layer_manager.subject_layer.subjects_index[event.subject_id]
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
         actor.stop_actions((BaseMoveTo,))
283
         actor.stop_actions((BaseMoveTo,))
265
         actor.set_position(*new_world_position)
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
         actor = self.layer_manager.subject_layer.subjects_index[event.subject_id]
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
         if event.gui_action == UserAction.ORDER_MOVE:
296
         if event.gui_action == UserAction.ORDER_MOVE:
273
             animation = ANIMATION_WALK
297
             animation = ANIMATION_WALK
274
             cycle_duration = 2
298
             cycle_duration = 2
283
                 'Gui action {} unknown'.format(event.gui_action)
307
                 'Gui action {} unknown'.format(event.gui_action)
284
             )
308
             )
285
 
309
 
286
-        move_duration = event.move_duration
310
+        move_duration = event.duration
287
         move_action = MoveTo(new_world_position, move_duration)
311
         move_action = MoveTo(new_world_position, move_duration)
288
         actor.do(move_action)
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
     def new_visible_opponent(self, event: NewVisibleOpponent):
331
     def new_visible_opponent(self, event: NewVisibleOpponent):
294
         self.visible_or_no_longer_visible_opponent(event, (153, 0, 153))
332
         self.visible_or_no_longer_visible_opponent(event, (153, 0, 153))

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

12
 from opencombat.simulation.mechanism import OpponentVisibleMechanism
12
 from opencombat.simulation.mechanism import OpponentVisibleMechanism
13
 from opencombat.user_action import UserAction
13
 from opencombat.user_action import UserAction
14
 from synergine2.simulation import Event
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
 class LookAroundBehaviour(AliveSubjectBehaviour):
54
 class LookAroundBehaviour(AliveSubjectBehaviour):

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

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
 # coding: utf-8
1
 # coding: utf-8
2
 import typing
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
 from opencombat.const import COLLECTION_ALIVE
9
 from opencombat.const import COLLECTION_ALIVE
7
 from opencombat.const import COMBAT_MODE_DEFENSE
10
 from opencombat.const import COMBAT_MODE_DEFENSE
8
 from opencombat.simulation.base import BaseSubject
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
 from opencombat.simulation.behaviour import EngageOpponent
14
 from opencombat.simulation.behaviour import EngageOpponent
11
 from opencombat.simulation.behaviour import LookAroundBehaviour
15
 from opencombat.simulation.behaviour import LookAroundBehaviour
12
 from synergine2.share import shared
16
 from synergine2.share import shared
24
     start_collections = [
28
     start_collections = [
25
         COLLECTION_ALIVE,
29
         COLLECTION_ALIVE,
26
     ]
30
     ]
27
-    behaviours_classes = [
28
-        MoveToBehaviour,
29
-        LookAroundBehaviour,
30
-        EngageOpponent,
31
-    ]
32
     visible_opponent_ids = shared.create_self('visible_opponent_ids', lambda: [])
31
     visible_opponent_ids = shared.create_self('visible_opponent_ids', lambda: [])
33
     combat_mode = shared.create_self('combat_mode', COMBAT_MODE_DEFENSE)
32
     combat_mode = shared.create_self('combat_mode', COMBAT_MODE_DEFENSE)
34
     behaviour_selector_class = TileBehaviourSelector
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
     def __init__(self, *args, **kwargs):
44
     def __init__(self, *args, **kwargs):
37
         super().__init__(*args, **kwargs)
45
         super().__init__(*args, **kwargs)
38
         self._walk_ref_time = float(self.config.resolve('game.move.walk_ref_time'))
46
         self._walk_ref_time = float(self.config.resolve('game.move.walk_ref_time'))
39
         self._run_ref_time = float(self.config.resolve('game.move.run_ref_time'))
47
         self._run_ref_time = float(self.config.resolve('game.move.run_ref_time'))
40
         self._crawl_ref_time = float(self.config.resolve('game.move.crawl_ref_time'))
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
     @property
52
     @property
43
     def global_move_coeff(self) -> float:
53
     def global_move_coeff(self) -> float:
45
 
55
 
46
     @property
56
     @property
47
     def run_duration(self) -> float:
57
     def run_duration(self) -> float:
58
+        """
59
+        :return: move to tile time (s) when running
60
+        """
48
         return self._run_ref_time * self.global_move_coeff
61
         return self._run_ref_time * self.global_move_coeff
49
 
62
 
50
     @property
63
     @property
51
     def walk_duration(self) -> float:
64
     def walk_duration(self) -> float:
65
+        """
66
+        :return: move to tile time (s) when walking
67
+        """
52
         return self._walk_ref_time * self.global_move_coeff
68
         return self._walk_ref_time * self.global_move_coeff
53
 
69
 
54
     @property
70
     @property
55
     def crawl_duration(self) -> float:
71
     def crawl_duration(self) -> float:
72
+        """
73
+        :return: move to tile time (s) when crawling
74
+        """
56
         return self._crawl_ref_time * self.global_move_coeff
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
 class ManSubject(TileSubject):
95
 class ManSubject(TileSubject):
60
-    pass
96
+    behaviours_classes = [
97
+        MoveBehaviour,
98
+        LookAroundBehaviour,
99
+        EngageOpponent,
100
+    ]  # type: typing.List[SubjectBehaviour]
101
+
61
 
102
 
62
 class TankSubject(TileSubject):
103
 class TankSubject(TileSubject):
104
+    behaviours_classes = [
105
+        MoveWithRotationBehaviour,
106
+        LookAroundBehaviour,
107
+        EngageOpponent,
108
+    ]  # type: typing.List[SubjectBehaviour]
109
+
63
     def __init__(self, *args, **kwargs) -> None:
110
     def __init__(self, *args, **kwargs) -> None:
64
         super().__init__(*args, **kwargs)
111
         super().__init__(*args, **kwargs)
65
         # TODO BS 2018-01-26: This coeff will be dependent of real
112
         # TODO BS 2018-01-26: This coeff will be dependent of real
68
             'game.move.subject.tank1.global_move_coeff',
115
             'game.move.subject.tank1.global_move_coeff',
69
             3,
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
     @property
123
     @property
73
     def global_move_coeff(self) -> float:
124
     def global_move_coeff(self) -> float:

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

10
 from opencombat.simulation.physics import TilePhysics
10
 from opencombat.simulation.physics import TilePhysics
11
 from synergine2_cocos2d.terminal import GameTerminal
11
 from synergine2_cocos2d.terminal import GameTerminal
12
 from synergine2_cocos2d.util import get_map_file_path_from_dir
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
 class CocosTerminal(GameTerminal):
16
 class CocosTerminal(GameTerminal):
18
     main_process = True
17
     main_process = True
19
 
18
 
20
     subscribed_events = [
19
     subscribed_events = [
21
-        FinishMoveEvent,
22
-        StartMoveEvent,
20
+        move.SubjectFinishTileMoveEvent,
21
+        move.SubjectFinishMoveEvent,
22
+        move.SubjectStartTileMoveEvent,
23
+        move.SubjectStartRotationEvent,
24
+        move.SubjectFinishRotationEvent,
23
         NewVisibleOpponent,
25
         NewVisibleOpponent,
24
         NoLongerVisibleOpponent,
26
         NoLongerVisibleOpponent,
25
         FireEvent,
27
         FireEvent,

+ 34 - 0
test_config.yaml View File

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

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

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

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

1
+# coding: utf-8

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

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)