File: test_motion.py

package info (click to toggle)
terminaltexteffects 0.14.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,324 kB
  • sloc: python: 16,857; makefile: 3
file content (482 lines) | stat: -rw-r--r-- 18,452 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
"""Tests for the Path, Segment, Waypoint and Motion classes."""

import pytest

from terminaltexteffects.engine.base_character import EffectCharacter, EventHandler
from terminaltexteffects.engine.motion import Path, Segment, Waypoint
from terminaltexteffects.utils import easing
from terminaltexteffects.utils.exceptions import (
    ActivateEmptyPathError,
    DuplicatePathIDError,
    DuplicateWaypointIDError,
    PathInvalidSpeedError,
    PathNotFoundError,
    WaypointNotFoundError,
)
from terminaltexteffects.utils.geometry import Coord, find_length_of_bezier_curve, find_length_of_line

pytestmark = [pytest.mark.engine, pytest.mark.motion, pytest.mark.smoke]


@pytest.fixture
def character() -> EffectCharacter:
    """Fixture for creating an EffectCharacter instance."""
    return EffectCharacter(0, "a", 0, 0)


@pytest.fixture
def waypoint() -> Waypoint:
    """Fixture for creating a Waypoint instance."""
    return Waypoint(waypoint_id="waypoint_0", coord=Coord(0, 0), bezier_control=(Coord(0, 10),))


def test_waypoint_init(waypoint: Waypoint) -> None:
    """Test the initialization of a Waypoint."""
    assert waypoint.waypoint_id == "waypoint_0"
    assert waypoint.coord == Coord(0, 0)
    assert waypoint.bezier_control == (Coord(0, 10),)


def test_waypoint_equal_waypoint(waypoint: Waypoint) -> None:
    """Test equality of waypoints with the same ID and coordinates."""
    assert waypoint == Waypoint(waypoint_id="waypoint_0", coord=Coord(0, 0), bezier_control=(Coord(0, 10),))


def test_waypoint_equal_unqual_waypoint(waypoint: Waypoint) -> None:
    """Test inequality of waypoints with different IDs and coordinates."""
    assert waypoint != Waypoint(waypoint_id="waypoint_1", coord=Coord(1, 0), bezier_control=(Coord(0, 10),))


def test_waypoint_equal_different_type(waypoint: Waypoint) -> None:
    """Test inequality of waypoint with a different type."""
    assert waypoint != "waypoint_0"


def test_segment_length_no_bezier() -> None:
    """Test segment length calculation without bezier control points."""
    waypoint_0 = Waypoint(waypoint_id="waypoint_0", coord=Coord(0, 0))
    waypoint_1 = Waypoint(waypoint_id="waypoint_1", coord=Coord(10, 0))
    segment = Segment(waypoint_0, waypoint_1, find_length_of_line(waypoint_0.coord, waypoint_1.coord))
    line_length = 10
    assert segment.distance == line_length


def test_segment_length_bezier() -> None:
    """Test segment length calculation with bezier control points."""
    waypoint_0 = Waypoint(waypoint_id="waypoint_0", coord=Coord(0, 0), bezier_control=(Coord(5, 5),))
    waypoint_1 = Waypoint(waypoint_id="waypoint_1", coord=Coord(10, 0), bezier_control=(Coord(10, 10),))
    segment = Segment(
        waypoint_0,
        waypoint_1,
        find_length_of_bezier_curve(waypoint_0.coord, waypoint_0.bezier_control, waypoint_1.coord),  # type: ignore[arg-type]
    )
    bezier_length = 12.70820393249937
    assert segment.distance == bezier_length


def test_segment_is_hashable() -> None:
    """Test that a Segment instance is hashable."""
    waypoint_0 = Waypoint(waypoint_id="waypoint_0", coord=Coord(0, 0))
    waypoint_1 = Waypoint(waypoint_id="waypoint_1", coord=Coord(10, 0))
    segment = Segment(waypoint_0, waypoint_1, find_length_of_line(waypoint_0.coord, waypoint_1.coord))
    assert hash(segment) == hash((waypoint_0, waypoint_1))


def test_segment_equal_segment() -> None:
    """Test equality of segments with the same waypoints and distance."""
    waypoint_0 = Waypoint(waypoint_id="waypoint_0", coord=Coord(0, 0))
    waypoint_1 = Waypoint(waypoint_id="waypoint_1", coord=Coord(10, 0))
    segment = Segment(waypoint_0, waypoint_1, find_length_of_line(waypoint_0.coord, waypoint_1.coord))
    assert segment == Segment(waypoint_0, waypoint_1, find_length_of_line(waypoint_0.coord, waypoint_1.coord))


def test_segment_equal_incorrect_type() -> None:
    """Test inequality of segment with a different type."""
    waypoint_0 = Waypoint(waypoint_id="waypoint_0", coord=Coord(0, 0))
    waypoint_1 = Waypoint(waypoint_id="waypoint_1", coord=Coord(10, 0))
    segment = Segment(waypoint_0, waypoint_1, find_length_of_line(waypoint_0.coord, waypoint_1.coord))
    assert segment != "segment"


def test_path_init() -> None:
    """Test the initialization of a Path."""
    p = Path("path_0")
    assert p.path_id == "path_0"
    assert p.speed == 1
    assert p.ease is None
    assert p.layer is None
    assert p.hold_time == 0
    assert p.loop is False
    assert p.segments == []
    assert p.waypoints == []
    assert p.waypoint_lookup == {}
    assert p.total_distance == 0
    assert p.current_step == 0
    assert p.max_steps == 0
    assert p.hold_time_remaining == 0
    assert p.last_distance_reached == 0
    assert p.origin_segment is None


def test_path_init_invalid_speed() -> None:
    """Test initialization of a Path with invalid speed."""
    with pytest.raises(PathInvalidSpeedError):
        Path("path_0", speed=-1)


def test_path_new_waypoint_auto_id_generation() -> None:
    """Test auto ID generation for new waypoints.

    ID's should start at 0 and increment by 1 for each new waypoint.
    """
    p = Path("p")
    p.new_waypoint(Coord(0, 0))
    p.new_waypoint(Coord(10, 0))
    assert p.waypoints[0].waypoint_id == "0"
    assert p.waypoints[1].waypoint_id == "1"


def test_path_new_waypoint_auto_id_deleted_waypoints() -> None:
    """Test auto ID generation for new waypoints after deletion.

    ID's should start at 0 and increment by 1 for each new waypoint, even if waypoints have been deleted.
    """
    # waypoint auto ID's start at 0
    p = Path("p")
    p.new_waypoint(Coord(0, 0))
    p.new_waypoint(Coord(10, 0))
    p.new_waypoint(Coord(20, 0))
    p.waypoints.pop(-1)
    new_waypoint = p.new_waypoint(Coord(30, 0))
    assert new_waypoint.waypoint_id == "3"


def test_path_new_waypoint_duplicate_waypoint_id() -> None:
    """Test that creating a waypoint with a duplicate ID raises an error."""
    p = Path("p")
    p.new_waypoint(Coord(0, 0), waypoint_id="0")
    with pytest.raises(DuplicateWaypointIDError):
        p.new_waypoint(Coord(10, 0), waypoint_id="0")


def test_path_new_waypoint_bezier_as_single_coord() -> None:
    """Test that a single coordinate can be used as a bezier control point."""
    p = Path("p")
    p.new_waypoint(Coord(0, 0), bezier_control=Coord(0, 10))
    assert p.waypoints[0].bezier_control == (Coord(0, 10),)


def test_path_new_waypoint_bezier_as_tuple() -> None:
    """Test that a tuple can be used as bezier control points."""
    p = Path("p")
    p.new_waypoint(Coord(0, 0), bezier_control=(Coord(0, 10),))
    assert p.waypoints[0].bezier_control == (Coord(0, 10),)


def test_path_new_waypoint_multiple_waypoints_with_bezier_segment() -> None:
    """Test multiple waypoints with bezier segments."""
    p = Path("p")
    p.new_waypoint(Coord(0, 0))
    p.new_waypoint(Coord(10, 0), bezier_control=Coord(10, 10))
    assert p.segments[0].distance == find_length_of_bezier_curve(Coord(0, 0), Coord(10, 10), Coord(10, 0))


def test_path_query_waypoint_valid_waypoint() -> None:
    """Test querying an existing waypoint ID in a path."""
    p = Path("p")
    p.new_waypoint(Coord(0, 0))
    p.new_waypoint(Coord(10, 0))
    assert p.query_waypoint("0") == p.waypoints[0]


def test_path_query_waypoint_invalid_waypoint() -> None:
    """Test querying a non-existing waypoint ID in a path."""
    p = Path("p")
    p.new_waypoint(Coord(0, 0))
    p.new_waypoint(Coord(10, 0))
    with pytest.raises(WaypointNotFoundError):
        p.query_waypoint("2")


def test_path_step_zero_distance(character: EffectCharacter) -> None:
    """Test stepping through a path with zero distance."""
    p = Path("p")
    p.new_waypoint(Coord(0, 0))
    p.new_waypoint(Coord(0, 0))
    assert p.step(character.event_handler) == Coord(0, 0)


def test_path_step_single_segment(character: EffectCharacter) -> None:
    """Test stepping through a single segment path."""
    p = Path("p")
    p.new_waypoint(Coord(0, 0))
    p.new_waypoint(Coord(10, 0))
    current_point = p.step(character.event_handler)
    while current_point != Coord(10, 0):
        current_point = p.step(character.event_handler)


def test_path_step_single_segment_eased(character: EffectCharacter) -> None:
    """Test stepping through a single segment path with easing."""
    p = Path("p", ease=easing.in_out_sine)
    p.new_waypoint(Coord(0, 0))
    p.new_waypoint(Coord(10, 0))
    current_point = p.step(character.event_handler)
    while current_point != Coord(10, 0):
        current_point = p.step(character.event_handler)


def test_path_step_multiple_segments(character: EffectCharacter) -> None:
    """Test stepping through a path with multiple segments."""
    p = Path("p")
    p.new_waypoint(Coord(0, 0))
    p.new_waypoint(Coord(10, 0))
    p.new_waypoint(Coord(10, 10))
    p.new_waypoint(Coord(0, 10))
    current_point = p.step(character.event_handler)
    while current_point != Coord(0, 10):
        current_point = p.step(character.event_handler)


def test_path_step_multiple_segments_eased(character: EffectCharacter) -> None:
    """Test stepping through a path with multiple segments and easing."""
    p = Path("p", ease=easing.in_out_elastic)
    p.new_waypoint(Coord(0, 0))
    p.new_waypoint(Coord(10, 0))
    p.new_waypoint(Coord(10, 10))
    p.new_waypoint(Coord(0, 10))
    current_point = p.step(character.event_handler)
    while current_point != Coord(0, 10):
        current_point = p.step(character.event_handler)


def test_path_step_multiple_segments_zero_distance(character: EffectCharacter) -> None:
    """Test stepping through a path with multiple segments and zero distance."""
    p = Path("p")
    p.new_waypoint(Coord(0, 0))
    p.new_waypoint(Coord(0, 0))
    p.new_waypoint(Coord(0, 0))
    p.new_waypoint(Coord(0, 0))
    current_point = p.step(character.event_handler)
    while current_point != Coord(0, 0):
        current_point = p.step(character.event_handler)


def test_path_step_multiple_segments_mutiple_bezier(character: EffectCharacter) -> None:
    """Test stepping through a path with multiple segments and multiple bezier control points."""
    p = Path("p")
    p.new_waypoint(Coord(0, 0))
    p.new_waypoint(Coord(10, 0), bezier_control=Coord(10, 10))
    p.new_waypoint(Coord(10, 10), bezier_control=Coord(0, 10))
    p.new_waypoint(Coord(0, 10), bezier_control=Coord(0, 0))
    current_point = p.step(character.event_handler)
    while current_point != Coord(0, 10):
        current_point = p.step(character.event_handler)


def test_path_equality() -> None:
    """Test equality of paths with the same waypoints."""
    p1 = Path("p")
    p1.new_waypoint(Coord(0, 0))
    p1.new_waypoint(Coord(10, 0))
    p1.new_waypoint(Coord(10, 10))
    p1.new_waypoint(Coord(0, 10))

    p2 = Path("p")
    p2.new_waypoint(Coord(0, 0))
    p2.new_waypoint(Coord(10, 0))
    p2.new_waypoint(Coord(10, 10))
    p2.new_waypoint(Coord(0, 10))

    assert p1 == p2


def test_path_equality_invalid_type() -> None:
    """Test inequality of path with a different type."""
    p = Path("p")
    assert p != "p"


def test_motion_set_coordinate(character: EffectCharacter) -> None:
    """Test setting the coordinate of a character."""
    character.motion.set_coordinate(Coord(10, 10))
    assert character.motion.current_coord == Coord(10, 10)


def test_motion_new_path_duplicate_path_id(character: EffectCharacter) -> None:
    """Test creating a new path with a duplicate ID raises an error."""
    character.motion.new_path(path_id="0")
    with pytest.raises(DuplicatePathIDError):
        character.motion.new_path(path_id="0")


def test_motion_new_path_auto_id_avoid_duplicate(character: EffectCharacter) -> None:
    """Test auto ID generation for new paths avoiding duplicates."""
    character.motion.new_path(path_id="1")
    character.motion.new_path(path_id="2")
    character.motion.new_path(path_id="3")
    new_path = character.motion.new_path()
    assert new_path.path_id == "4"


def test_motion_query_path_valid_path(character: EffectCharacter) -> None:
    """Test querying an existing path ID."""
    character.motion.new_path(path_id="0")
    character.motion.new_path(path_id="1")
    assert character.motion.query_path("0").path_id == "0"


def test_motion_query_path_invalid_path(character: EffectCharacter) -> None:
    """Test querying a non-existing path ID raises an error."""
    character.motion.new_path(path_id="0")
    character.motion.new_path(path_id="1")
    with pytest.raises(PathNotFoundError):
        character.motion.query_path("2")


def test_motion_movement_is_complete_no_active_paths(character: EffectCharacter) -> None:
    """Test checking if movement is complete with no active paths."""
    assert character.motion.movement_is_complete() is True


def test_motion_movement_is_complete_active_path_complete(character: EffectCharacter) -> None:
    """Test checking if movement is complete with an active path that is complete."""
    p = character.motion.new_path(path_id="0")
    p.new_waypoint(Coord(0, 0))
    p.new_waypoint(Coord(10, 0))
    character.motion.activate_path(p)
    while character.motion.active_path:
        character.motion.move()

    assert character.motion.movement_is_complete() is True


def test_motion_movement_is_complete_active_path_incomplete(character: EffectCharacter) -> None:
    """Test checking if movement is complete with an active path that is incomplete."""
    p = character.motion.new_path(path_id="0")
    p.new_waypoint(Coord(0, 0))
    p.new_waypoint(Coord(10, 0))
    character.motion.activate_path(p)

    assert character.motion.movement_is_complete() is False


def test_motion_chain_paths_single_path(character: EffectCharacter) -> None:
    """Test chaining a single path."""
    p = character.motion.new_path(path_id="0")
    p.new_waypoint(Coord(0, 0))
    p.new_waypoint(Coord(10, 0))
    character.motion.chain_paths(
        [
            p,
        ],
    )


def test_motion_chain_paths_multiple_paths(character: EffectCharacter) -> None:
    """Test chaining multiple paths."""
    p1 = character.motion.new_path(path_id="0")
    p1.new_waypoint(Coord(0, 0))
    p1.new_waypoint(Coord(10, 0))

    p2 = character.motion.new_path(path_id="1")
    p2.new_waypoint(Coord(10, 0))
    p2.new_waypoint(Coord(10, 10))

    character.motion.chain_paths([p1, p2])
    assert (EventHandler.Event.PATH_COMPLETE, p1) in character.event_handler.registered_events
    assert character.event_handler.registered_events[(EventHandler.Event.PATH_COMPLETE, p1)] == [
        (EventHandler.Action.ACTIVATE_PATH, p2),
    ]


def test_motion_chain_paths_multiple_paths_looping(character: EffectCharacter) -> None:
    """Test chaining multiple paths with looping."""
    p1 = character.motion.new_path(path_id="0")
    p1.new_waypoint(Coord(0, 0))
    p1.new_waypoint(Coord(10, 0))

    p2 = character.motion.new_path(path_id="1")
    p2.new_waypoint(Coord(10, 0))
    p2.new_waypoint(Coord(10, 10))

    character.motion.chain_paths([p1, p2], loop=True)
    assert (EventHandler.Event.PATH_COMPLETE, p1) in character.event_handler.registered_events
    assert character.event_handler.registered_events[(EventHandler.Event.PATH_COMPLETE, p1)] == [
        (EventHandler.Action.ACTIVATE_PATH, p2),
    ]
    assert (EventHandler.Event.PATH_COMPLETE, p2) in character.event_handler.registered_events
    assert character.event_handler.registered_events[(EventHandler.Event.PATH_COMPLETE, p2)] == [
        (EventHandler.Action.ACTIVATE_PATH, p1),
    ]


def test_motion_activate_path_first_waypoint_bezier(character: EffectCharacter) -> None:
    """Test activating a path with the first waypoint having a bezier control point."""
    p = character.motion.new_path(path_id="0")
    p.new_waypoint(Coord(0, 0), bezier_control=Coord(0, 10))
    p.new_waypoint(Coord(10, 0))
    character.motion.activate_path(p)
    assert character.motion.active_path == p


def test_motion_activate_path_no_waypoints(character: EffectCharacter) -> None:
    """Test activating a path with no waypoints raises an error."""
    p = character.motion.new_path(path_id="0")
    with pytest.raises(ActivateEmptyPathError):
        character.motion.activate_path(p)


def test_motion_active_path_with_layer(character: EffectCharacter) -> None:
    """Test activating a path with a layer."""
    p = character.motion.new_path(path_id="0", layer=1)
    p.new_waypoint(Coord(0, 0), bezier_control=Coord(0, 10))
    p.new_waypoint(Coord(10, 0))
    character.motion.activate_path(p)
    assert character.motion.active_path == p
    assert character.layer == 1


def test_motion_activate_path_previously_deactivated(character: EffectCharacter) -> None:
    """Test reactivating a path that was previously deactivated."""
    character.motion.set_coordinate(Coord(5, 5))
    p = character.motion.new_path(path_id="0")
    p.new_waypoint(Coord(0, 0))
    p.new_waypoint(Coord(10, 0))
    character.motion.activate_path(p)
    first_origin_distance = p.origin_segment.distance if p.origin_segment else 0
    character.motion.deactivate_path(p)
    character.motion.set_coordinate(Coord(2, 2))
    character.motion.activate_path(p)
    second_origin_distance = p.origin_segment.distance if p.origin_segment else 0
    assert character.motion.active_path == p
    assert second_origin_distance < first_origin_distance


def test_motion_move_no_active_path(character: EffectCharacter) -> None:
    """Test moving a character with no active path."""
    assert character.motion.active_path is None
    character.motion.move()


def test_motion_move_path_hold_time(character: EffectCharacter) -> None:
    """Test moving a character along a path with hold time."""
    p = character.motion.new_path(path_id="0", hold_time=5)
    p.new_waypoint(Coord(0, 0))
    p.new_waypoint(Coord(10, 0))
    character.motion.activate_path(p)
    character.motion.move()
    while character.motion.active_path:
        character.motion.move()
    assert character.motion.active_path is None


def test_motion_move_path_looping(character: EffectCharacter) -> None:
    """Test moving a character along a looping path."""
    p = character.motion.new_path(path_id="0", loop=True)
    p.new_waypoint(Coord(0, 0))
    p.new_waypoint(Coord(10, 0))
    p.new_waypoint(Coord(10, 10))
    character.motion.activate_path(p)
    for _ in range(100):
        character.motion.move()