diff --git a/Game/scripts/training_mode.gd b/Game/scripts/training_mode.gd index 2655578b..17b66344 100644 --- a/Game/scripts/training_mode.gd +++ b/Game/scripts/training_mode.gd @@ -595,12 +595,26 @@ func _place_wall_state(rebound: bool) -> void: randf_range(1.0, minf(FIELD_MAX_Y, 7.0)), randf_range(-FIELD_HALF_Z, FIELD_HALF_Z) ) - var toward_field := Vector3(-side, randf_range(-0.15, 0.15), randf_range(-0.15, 0.15)).normalized() - var direction := -toward_field if rebound else toward_field - var velocity := direction * randf_range(WALL_PLAY_SPEED.x, WALL_PLAY_SPEED.y) + var velocity := wall_state_velocity( + rebound, side, randf_range(WALL_PLAY_SPEED.x, WALL_PLAY_SPEED.y), + randf_range(-0.15, 0.15), randf_range(-0.15, 0.15) + ) _place_body(ball, Transform3D(Basis.IDENTITY, ball_position), velocity, Vector3.ZERO) +# Pure geometry seam for adversarial tests. `side` identifies the selected +# wall (+1 or -1); a wall-play vector points into the field and a rebound +# vector points into that wall. Normalize the perturbed normal before applying +# speed so random tangential components cannot accidentally change the speed +# distribution between the two state types. +static func wall_state_velocity(rebound: bool, side: float, speed: float, vertical: float, lateral: float) -> Vector3: + if speed < 0.0: + return Vector3.ZERO + var wall_side := -1.0 if side < 0.0 else 1.0 + var toward_field := Vector3(-wall_side, vertical, lateral).normalized() + return (-toward_field if rebound else toward_field) * speed + + # Air-intercept drill geometry. These six ranges are not free tuning knobs — # together they decide whether the drill is solvable at all, and the original # values made it arithmetically impossible (see the Round 9 note in diff --git a/Game/tests/cases/test_teamplay_rewards.gd b/Game/tests/cases/test_teamplay_rewards.gd index 04930d3a..4085dc4d 100644 --- a/Game/tests/cases/test_teamplay_rewards.gd +++ b/Game/tests/cases/test_teamplay_rewards.gd @@ -1,4 +1,4 @@ -extends TestCase +extends "res://tests/test_case.gd" const ShipAIControllerScript = preload("res://scripts/ship_ai_controller.gd") diff --git a/Game/tests/cases/test_wall_play_states.gd b/Game/tests/cases/test_wall_play_states.gd new file mode 100644 index 00000000..2a328635 --- /dev/null +++ b/Game/tests/cases/test_wall_play_states.gd @@ -0,0 +1,21 @@ +extends "res://tests/test_case.gd" + +const TrainingModeScript = preload("res://scripts/training_mode.gd") + +func test_wall_play_points_into_the_field() -> void: + var velocity: Vector3 = TrainingModeScript.wall_state_velocity(false, 1.0, 8.0, 0.1, -0.2) + assert_true(velocity.x < 0.0, "positive-side wall play travels inward") + assert_almost_eq(velocity.length(), 8.0, 0.0001, "wall-play speed is preserved") + +func test_rebound_points_into_the_selected_wall() -> void: + var velocity: Vector3 = TrainingModeScript.wall_state_velocity(true, 1.0, 8.0, 0.1, -0.2) + assert_true(velocity.x > 0.0, "positive-side rebound travels toward the wall") + assert_almost_eq(velocity.length(), 8.0, 0.0001, "rebound speed is preserved") + +func test_opposite_walls_mirror_the_normal_component() -> void: + var positive: Vector3 = TrainingModeScript.wall_state_velocity(false, 1.0, 6.0, 0.0, 0.0) + var negative: Vector3 = TrainingModeScript.wall_state_velocity(false, -1.0, 6.0, 0.0, 0.0) + assert_eq(positive.x, -negative.x, "opposite wall starts mirror the x direction") + +func test_negative_speed_fails_closed() -> void: + assert_eq(TrainingModeScript.wall_state_velocity(false, 1.0, -1.0, 0.0, 0.0), Vector3.ZERO, "negative speed cannot create velocity")