From 8709cf8fee038654c34876c65604b139bb4c7f24 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 21 Dec 2018 09:00:06 +0000 Subject: [PATCH 001/140] [BUILD] Added cheat config --- prebuild/preprocess.py | 4 +- sonic-2d-tech-demo.sublime-project | 66 +++++++++++++++++++++++------- 2 files changed, 54 insertions(+), 16 deletions(-) diff --git a/prebuild/preprocess.py b/prebuild/preprocess.py index c61a40e4..0bf3b821 100644 --- a/prebuild/preprocess.py +++ b/prebuild/preprocess.py @@ -19,7 +19,7 @@ # but for busted unit tests we prefer using --#if utest (which is never defined) to make clear that # the code is only needed for a purpose of redundancy and unit test harnessing in general. defined_symbols_table = { - 'debug': ['pico8', 'assert', 'log', 'visual_logger', 'tuner', 'profiler', 'mouse'], + 'debug': ['pico8', 'assert', 'log', 'visual_logger', 'tuner', 'profiler', 'mouse', 'cheat'], 'assert': ['pico8', 'assert', 'log', 'visual_logger'], 'itest': ['pico8', 'log', 'visual_logger', 'test'], 'itest_light': ['pico8', 'log', 'test'], @@ -27,6 +27,7 @@ 'visual_log': ['pico8', 'log', 'visual_logger'], 'pico8_utest': ['pico8', 'assert', 'log'], 'log': ['pico8', 'log'], + 'cheat': ['pico8', 'cheat'], 'release': ['pico8'] } @@ -44,6 +45,7 @@ 'visual_log': ['assert'], 'pico8_utest': [], 'log': ['assert'], + 'cheat': ['assert', 'log'], 'release': ['assert', 'log', 'warn', 'err'] } diff --git a/sonic-2d-tech-demo.sublime-project b/sonic-2d-tech-demo.sublime-project index 1b238f1b..e6604039 100644 --- a/sonic-2d-tech-demo.sublime-project +++ b/sonic-2d-tech-demo.sublime-project @@ -2,7 +2,7 @@ "build_systems": [ { - "name": "p8tool: build current", + "name": "p8tool: build context", "working_dir": "${project_path}", "shell": true, "cmd": ["./build.sh $file_base_name debug"], @@ -13,6 +13,10 @@ }, "variants": [ + { + "name": "debug", + "cmd": ["./build.sh $file_base_name debug"] + }, { "name": "assert", "cmd": ["./build.sh $file_base_name assert"] @@ -34,17 +38,21 @@ "cmd": ["./build.sh $file_base_name visual_log"] }, { - "name": "pico8_utest (headless)", + "name": "pico8_utest", "cmd": ["./build.sh $file_base_name pico8_utest"] }, { - "name": "pico8_utest (headless) (--no-minify)", + "name": "pico8_utest (--no-minify)", "cmd": ["./build.sh $file_base_name pico8_utest -m"] }, { "name": "log", "cmd": ["./build.sh $file_base_name log"] }, + { + "name": "cheat", + "cmd": ["./build.sh $file_base_name cheat"] + }, { "name": "release", "cmd": ["./build.sh $file_base_name release"] @@ -52,7 +60,7 @@ ] }, { - "name": "p8tool: build and run or reload current", + "name": "p8tool: build and run or reload context", "working_dir": "${project_path}", "shell": true, "cmd": ["./build.sh $file_base_name debug && (./reload.sh || ./run.sh $file_base_name debug)"], @@ -67,6 +75,10 @@ "name": "assert", "cmd": ["./build.sh $file_base_name assert && (./reload.sh || ./run.sh $file_base_name assert)"] }, + { + "name": "debug", + "cmd": ["./build.sh $file_base_name debug && (./reload.sh || ./run.sh $file_base_name debug)"] + }, { "name": "itest", "cmd": ["./build.sh $file_base_name itest && (./reload.sh || ./run.sh $file_base_name itest)"] @@ -84,17 +96,21 @@ "cmd": ["./build.sh $file_base_name visual_log && (./reload.sh || ./run.sh $file_base_name visual_log)"] }, { - "name": "pico8_utest (headless)", + "name": "pico8_utest", "cmd": ["./build.sh $file_base_name pico8_utest && (./reload.sh || ./run.sh $file_base_name pico8_utest '-x; exec bash')"] }, { - "name": "pico8_utest (headless) (--no-minify)", + "name": "pico8_utest (--no-minify)", "cmd": ["./build.sh $file_base_name pico8_utest -m && (./reload.sh || ./run.sh $file_base_name pico8_utest '-x; exec bash')"] }, { "name": "log", "cmd": ["./build.sh $file_base_name log && (./reload.sh || ./run.sh $file_base_name log)"] }, + { + "name": "cheat", + "cmd": ["./build.sh $file_base_name cheat && (./reload.sh || ./run.sh $file_base_name cheat)"] + }, { "name": "release", "cmd": ["./build.sh $file_base_name release && (./reload.sh || ./run.sh $file_base_name release)"] @@ -102,7 +118,7 @@ ] }, { - "name": "p8tool: run current", + "name": "p8tool: run context", "working_dir": "${project_path}", "shell": true, "cmd": ["./run.sh $file_base_name debug"], @@ -111,6 +127,10 @@ }, "variants": [ + { + "name": "debug", + "cmd": ["./run.sh $file_base_name debug"] + }, { "name": "assert", "cmd": ["./run.sh $file_base_name assert"] @@ -132,13 +152,17 @@ "cmd": ["./run.sh $file_base_name visual_log"] }, { - "name": "pico8_utest (headless)", + "name": "pico8_utest", "cmd": ["./run.sh $file_base_name pico8_utest"] }, { "name": "log", "cmd": ["./run.sh $file_base_name log"] }, + { + "name": "cheat", + "cmd": ["./run.sh $file_base_name cheat"] + }, { "name": "release", "cmd": ["./run.sh $file_base_name release"] @@ -180,13 +204,21 @@ "cmd": ["./install.sh visual_log"], }, { - "name": "pico8_utest (headless)", + "name": "pico8_utest", "cmd": ["./install.sh pico8_utest"], }, { "name": "log", "cmd": ["./install.sh log"], }, + { + "name": "cheat", + "cmd": ["./install.sh cheat"], + }, + { + "name": "release", + "cmd": ["./install.sh release"], + }, { "name": "debug (png)", "cmd": ["./install.sh debug png"], @@ -212,13 +244,17 @@ "cmd": ["./install.sh visual_log png"], }, { - "name": "pico8_utest (headless) (png)", + "name": "pico8_utest (png)", "cmd": ["./install.sh pico8_utest png"], }, { "name": "log (png)", "cmd": ["./install.sh log png"], }, + { + "name": "cheat (png)", + "cmd": ["./install.sh cheat png"], + }, { "name": "release (png)", "cmd": ["./install.sh release png"], @@ -287,11 +323,11 @@ "variants": [ { - "name": "current (solo)", + "name": "current - solo", "cmd": ["./test.sh $file_base_name solo"], }, { - "name": "current (all)", + "name": "current - all", "cmd": ["./test.sh $file_base_name all"], }, { @@ -299,11 +335,11 @@ "cmd": ["./test.sh all"], }, { - "name": "all (solo)", + "name": "all - solo", "cmd": ["./test.sh all solo"], }, { - "name": "all (all)", + "name": "all - all", "cmd": ["./test.sh all all"], }, { @@ -383,7 +419,7 @@ "cmd": ["./test.sh headless_itests"], }, { - "name": "headless_itests (solo)", + "name": "headless_itests - solo", "cmd": ["./test.sh headless_itests solo"], }, ], From 543958a74942417fb0acd611c015ca899716a527 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 21 Dec 2018 09:11:35 +0000 Subject: [PATCH 002/140] [BUILD] Replace playercharacter enums with constants --- prebuild/replace_strings.py | 11 +++++++++++ src/game/ingame/playercharacter.lua | 4 ++++ 2 files changed, 15 insertions(+) diff --git a/prebuild/replace_strings.py b/prebuild/replace_strings.py index 246e2a8f..26dd5457 100755 --- a/prebuild/replace_strings.py +++ b/prebuild/replace_strings.py @@ -87,6 +87,17 @@ 'input_modes.native': 0, 'input_modes.simulated': 1, + # playercharacter + 'control_modes.human': 1, + 'control_modes.ai': 2, + 'control_modes.puppet': 3, + + 'motion_modes.platformer': 1, + 'motion_modes.debug': 2, + + 'motion_states.grounded': 1, + 'motion_states.airborne': 2, + # itest 'itest_dsl_command_types.spawn': 1, 'itest_dsl_command_types.move': 2, diff --git a/src/game/ingame/playercharacter.lua b/src/game/ingame/playercharacter.lua index 398349a3..071a28e7 100644 --- a/src/game/ingame/playercharacter.lua +++ b/src/game/ingame/playercharacter.lua @@ -7,6 +7,8 @@ local collision = require("engine/physics/collision") local world = require("engine/physics/world") local pc_data = require("game/data/playercharacter_data") +--#ifn pico8 + -- enum for character control control_modes = { human = 1, -- player controls character @@ -26,6 +28,8 @@ motion_states = { airborne = 2 -- character is in the air } +--#endif + local player_char = new_class() -- parameters From 2a48fc72076bf02a64253904525724cffdf77f53 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 21 Dec 2018 09:31:56 +0000 Subject: [PATCH 003/140] [BUILD] Debug motion is enabled for cheat config only --- src/game/ingame/playercharacter.lua | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/game/ingame/playercharacter.lua b/src/game/ingame/playercharacter.lua index 071a28e7..87fa15de 100644 --- a/src/game/ingame/playercharacter.lua +++ b/src/game/ingame/playercharacter.lua @@ -42,7 +42,7 @@ local player_char = new_class() -- state vars -- control_mode control_modes control mode: human (default) or ai --- motion_mode motion_modes motion mode: platformer (under gravity) or debug (fly around) +-- motion_mode (cheat) motion_modes motion mode: platformer (under gravity) or debug (fly around) -- motion_state motion_states motion state (platformer mode only) -- position vector current position (character center "between" pixels) -- ground_speed float current speed along the ground (~px/frame) @@ -66,7 +66,9 @@ end function player_char:_setup() self.control_mode = control_modes.human +--#if cheat self.motion_mode = motion_modes.platformer +--#endif self.motion_state = motion_states.grounded self.position = vector.zero() @@ -130,11 +132,14 @@ end -- update player position function player_char:update() - if self.motion_mode == motion_modes.platformer then - self:_update_platformer_motion() - else -- self.motion_mode == motion_modes.debug +--#if cheat + if self.motion_mode == motion_modes.debug then self:_update_debug() end + -- else: self.motion_mode == motion_modes.platformer +--#endif + + self:_update_platformer_motion() end -- return (signed_distance, slope_angle) where: @@ -667,6 +672,8 @@ function player_char:_check_hold_jump() end end +--#if cheat + -- update the velocity and position of the character following debug motion rules function player_char:_update_debug() self:_update_velocity_debug() @@ -680,6 +687,8 @@ function player_char:_update_velocity_debug() self:_update_velocity_component_debug("y") end +--#endif + -- update the velocity component for coordinate "x" or "y" with debug motion -- coord string "x" or "y" function player_char:_update_velocity_component_debug(coord) From 5523a8f0a56d4b881a51c6af68e1fb077f346937 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 21 Dec 2018 09:59:34 +0000 Subject: [PATCH 004/140] [REFACTOR] Moved handle_input from stage to playercharacter --- src/game/ingame/playercharacter.lua | 31 +++++++ src/game/ingame/stage.lua | 35 ------- tests/utestplayercharacter.lua | 116 ++++++++++++++++++++++- tests/uteststage.lua | 138 +--------------------------- 4 files changed, 150 insertions(+), 170 deletions(-) diff --git a/src/game/ingame/playercharacter.lua b/src/game/ingame/playercharacter.lua index 87fa15de..1b7aac53 100644 --- a/src/game/ingame/playercharacter.lua +++ b/src/game/ingame/playercharacter.lua @@ -3,6 +3,7 @@ require("engine/core/class") require("engine/core/helper") require("engine/core/math") require("engine/render/sprite") +local input = require("engine/input/input") local collision = require("engine/physics/collision") local world = require("engine/physics/world") local pc_data = require("game/data/playercharacter_data") @@ -130,11 +131,41 @@ function player_char:move_by(delta_vector) self.position = self.position + delta_vector end +-- update intention based on current input +function player_char:handle_input() + if self.control_mode == control_modes.human then + -- move + local player_move_intention = vector.zero() + + if input:is_down(button_ids.left) then + player_move_intention:add_inplace(vector(-1, 0)) + elseif input:is_down(button_ids.right) then + player_move_intention:add_inplace(vector(1, 0)) + end + + if input:is_down(button_ids.up) then + player_move_intention:add_inplace(vector(0, -1)) + elseif input:is_down(button_ids.down) then + player_move_intention:add_inplace(vector(0, 1)) + end + + self.move_intention = player_move_intention + + -- jump + local is_jump_input_down = input:is_down(button_ids.o) -- convenient var for optional pre-check + -- set jump intention each frame, don't set it to true for later consumption to avoid sticky input + -- without needing a reset later during update + self.jump_intention = is_jump_input_down and input:is_just_pressed(button_ids.o) + self.hold_jump_intention = is_jump_input_down + end +end + -- update player position function player_char:update() --#if cheat if self.motion_mode == motion_modes.debug then self:_update_debug() + return end -- else: self.motion_mode == motion_modes.platformer --#endif diff --git a/src/game/ingame/stage.lua b/src/game/ingame/stage.lua index 97a2b327..e482c9a6 100644 --- a/src/game/ingame/stage.lua +++ b/src/game/ingame/stage.lua @@ -6,7 +6,6 @@ local player_char = require("game/ingame/playercharacter") local gamestate = require("game/application/gamestate") local flow = require("engine/application/flow") local audio = require("game/resources/audio") -local input = require("engine/input/input") local ui = require("engine/ui/ui") local stage_data = require("game/data/stage_data") @@ -80,7 +79,6 @@ function state:update() self:update_coroutines() if self.current_substate == stage.substates.play then - self:handle_input() self.player_char:update() self:check_reached_goal() self:update_camera() @@ -148,39 +146,6 @@ function state:spawn_player_char() end --- input - --- refactor: move to player_character --- handle player input -function state:handle_input() - if self.player_char.control_mode == control_modes.human then - -- move - local player_move_intention = vector.zero() - - if input:is_down(button_ids.left) then - player_move_intention:add_inplace(vector(-1, 0)) - elseif input:is_down(button_ids.right) then - player_move_intention:add_inplace(vector(1, 0)) - end - - if input:is_down(button_ids.up) then - player_move_intention:add_inplace(vector(0, -1)) - elseif input:is_down(button_ids.down) then - player_move_intention:add_inplace(vector(0, 1)) - end - - self.player_char.move_intention = player_move_intention - - -- jump - local is_jump_input_down = input:is_down(button_ids.o) -- convenient var for optional pre-check - -- set jump intention each frame, don't set it to true for later consumption to avoid sticky input - -- without needing a reset later during update - self.player_char.jump_intention = is_jump_input_down and input:is_just_pressed(button_ids.o) - self.player_char.hold_jump_intention = is_jump_input_down -- set each frame - end -end - - -- gameplay events function state:check_reached_goal() diff --git a/tests/utestplayercharacter.lua b/tests/utestplayercharacter.lua index 37950ef9..2486d4b7 100644 --- a/tests/utestplayercharacter.lua +++ b/tests/utestplayercharacter.lua @@ -1,6 +1,7 @@ require("bustedhelper") require("engine/core/math") local player_char = require("game/ingame/playercharacter") +local input = require("engine/input/input") local collision = require("engine/physics/collision") local ground_query_info = collision.ground_query_info local pc_data = require("game/data/playercharacter_data") @@ -294,6 +295,116 @@ describe('player_char', function () end) end) + describe('handle_input', function () + + after_each(function () + input.players_btn_states[0] = generate_initial_btn_states() + end) + + describe('(when player character control mode is not human)', function () + + before_each(function () + pc.control_mode = control_modes.ai -- or puppet + end) + + it('should do nothing', function () + input.players_btn_states[0][button_ids.left] = btn_states.pressed + pc:handle_input() + assert.are_equal(vector:zero(), pc.move_intention) + input.players_btn_states[0][button_ids.up] = btn_states.pressed + pc:handle_input() + assert.are_equal(vector:zero(), pc.move_intention) + end) + + end) + + -- control mode is human by default + + it('(when input left in down) it should update the player character\'s move intention by (-1, 0)', function () + input.players_btn_states[0][button_ids.left] = btn_states.pressed + pc:handle_input() + assert.are_equal(vector(-1, 0), pc.move_intention) + end) + + it('(when input right in down) it should update the player character\'s move intention by (1, 0)', function () + input.players_btn_states[0][button_ids.right] = btn_states.just_pressed + pc:handle_input() + assert.are_equal(vector(1, 0), pc.move_intention) + end) + + it('(when input left and right are down) it should update the player character\'s move intention by (-1, 0)', function () + input.players_btn_states[0][button_ids.left] = btn_states.pressed + input.players_btn_states[0][button_ids.right] = btn_states.just_pressed + pc:handle_input() + assert.are_equal(vector(-1, 0), pc.move_intention) + end) + + it('(when input up in down) it should update the player character\'s move intention by (-1, 0)', function () + input.players_btn_states[0][button_ids.up] = btn_states.pressed + pc:handle_input() + assert.are_equal(vector(0, -1), pc.move_intention) + end) + + it('(when input down in down) it should update the player character\'s move intention by (0, 1)', function () + input.players_btn_states[0][button_ids.down] = btn_states.pressed + pc:handle_input() + assert.are_equal(vector(0, 1), pc.move_intention) + end) + + it('(when input up and down are down) it should update the player character\'s move intention by (0, -1)', function () + input.players_btn_states[0][button_ids.up] = btn_states.just_pressed + input.players_btn_states[0][button_ids.down] = btn_states.pressed + pc:handle_input() + assert.are_equal(vector(0, -1), pc.move_intention) + end) + + it('(when input left and up are down) it should update the player character\'s move intention by (-1, -1)', function () + input.players_btn_states[0][button_ids.left] = btn_states.just_pressed + input.players_btn_states[0][button_ids.up] = btn_states.just_pressed + pc:handle_input() + assert.are_equal(vector(-1, -1), pc.move_intention) + end) + + it('(when input left and down are down) it should update the player character\'s move intention by (-1, 1)', function () + input.players_btn_states[0][button_ids.left] = btn_states.just_pressed + input.players_btn_states[0][button_ids.down] = btn_states.just_pressed + pc:handle_input() + assert.are_equal(vector(-1, 1), pc.move_intention) + end) + + it('(when input right and up are down) it should update the player character\'s move intention by (1, -1)', function () + input.players_btn_states[0][button_ids.right] = btn_states.just_pressed + input.players_btn_states[0][button_ids.up] = btn_states.just_pressed + pc:handle_input() + assert.are_equal(vector(1, -1), pc.move_intention) + end) + + it('(when input right and down are down) it should update the player character\'s move intention by (1, 1)', function () + input.players_btn_states[0][button_ids.right] = btn_states.just_pressed + input.players_btn_states[0][button_ids.down] = btn_states.just_pressed + pc:handle_input() + assert.are_equal(vector(1, 1), pc.move_intention) + end) + + it('(when input o is released) it should update the player character\'s jump intention to false, hold jump intention to false', function () + pc:handle_input() + assert.are_same({false, false}, {pc.jump_intention, pc.hold_jump_intention}) + end) + + it('(when input o is just pressed) it should update the player character\'s jump intention to true, hold jump intention to true', function () + input.players_btn_states[0][button_ids.o] = btn_states.just_pressed + pc:handle_input() + assert.are_same({true, true}, {pc.jump_intention, pc.hold_jump_intention}) + end) + + it('(when input o is pressed) it should update the player character\'s jump intention to false, hold jump intention to true', function () + input.players_btn_states[0][button_ids.o] = btn_states.pressed + pc:handle_input() + assert.are_same({false, true}, {pc.jump_intention, pc.hold_jump_intention}) + end) + + end) + describe('update', function () local update_platformer_motion_stub @@ -331,7 +442,10 @@ describe('player_char', function () pc.motion_mode = motion_modes.debug end) - it('. should call _update_debug', function () + -- bugfix history + -- . + -- * the test revealed a missing return, as _update_platformer_motion was called but shouldn't + it('should call _update_debug', function () pc:update() assert.spy(update_platformer_motion_stub).was_not_called() assert.spy(update_debug_stub).was_called(1) diff --git a/tests/uteststage.lua b/tests/uteststage.lua index 82239b8f..eccaaf98 100644 --- a/tests/uteststage.lua +++ b/tests/uteststage.lua @@ -1,5 +1,4 @@ require("bustedhelper") -local input = require("engine/input/input") local ui = require("engine/ui/ui") local stage = require("game/ingame/stage") local state = stage.state @@ -345,7 +344,7 @@ describe('stage', function () describe('on_enter', function () local spawn_player_char_stub - local handle_input_stub + local start_coroutine_method_stub local play_bgm_stub setup(function () @@ -402,7 +401,7 @@ describe('stage', function () describe('on_exit', function () local title_overlay_clear_labels_stub - local handle_input_stub + local start_coroutine_method_stub local stop_bgm_stub setup(function () @@ -503,128 +502,6 @@ describe('stage', function () flow.curr_state = nil end) - describe('player_char', function () - - describe('handle_input', function () - - after_each(function () - input.players_btn_states[0][button_ids.left] = btn_states.released - input.players_btn_states[0][button_ids.right] = btn_states.released - input.players_btn_states[0][button_ids.up] = btn_states.released - input.players_btn_states[0][button_ids.down] = btn_states.released - input.players_btn_states[0][button_ids.o] = btn_states.released - - state.player_char.move_intention = vector.zero() - state.player_char.jump_intention = false - state.player_char.hold_jump_intention = false - end) - - describe('(when player character control mode is not human)', function () - - before_each(function () - state.player_char.control_mode = control_modes.ai - end) - - it('should do nothing', function () - input.players_btn_states[0][button_ids.left] = btn_states.pressed - state:handle_input() - assert.are_equal(vector:zero(), state.player_char.move_intention) - input.players_btn_states[0][button_ids.up] = btn_states.pressed - state:handle_input() - assert.are_equal(vector:zero(), state.player_char.move_intention) - end) - - end) - - -- control mode is human by default - - it('(when input left in down) it should update the player character\'s move intention by (-1, 0)', function () - input.players_btn_states[0][button_ids.left] = btn_states.pressed - state:handle_input() - assert.are_equal(vector(-1, 0), state.player_char.move_intention) - end) - - it('(when input right in down) it should update the player character\'s move intention by (1, 0)', function () - input.players_btn_states[0][button_ids.right] = btn_states.just_pressed - state:handle_input() - assert.are_equal(vector(1, 0), state.player_char.move_intention) - end) - - it('(when input left and right are down) it should update the player character\'s move intention by (-1, 0)', function () - input.players_btn_states[0][button_ids.left] = btn_states.pressed - input.players_btn_states[0][button_ids.right] = btn_states.just_pressed - state:handle_input() - assert.are_equal(vector(-1, 0), state.player_char.move_intention) - end) - - it('(when input up in down) it should update the player character\'s move intention by (-1, 0)', function () - input.players_btn_states[0][button_ids.up] = btn_states.pressed - state:handle_input() - assert.are_equal(vector(0, -1), state.player_char.move_intention) - end) - - it('(when input down in down) it should update the player character\'s move intention by (0, 1)', function () - input.players_btn_states[0][button_ids.down] = btn_states.pressed - state:handle_input() - assert.are_equal(vector(0, 1), state.player_char.move_intention) - end) - - it('(when input up and down are down) it should update the player character\'s move intention by (0, -1)', function () - input.players_btn_states[0][button_ids.up] = btn_states.just_pressed - input.players_btn_states[0][button_ids.down] = btn_states.pressed - state:handle_input() - assert.are_equal(vector(0, -1), state.player_char.move_intention) - end) - - it('(when input left and up are down) it should update the player character\'s move intention by (-1, -1)', function () - input.players_btn_states[0][button_ids.left] = btn_states.just_pressed - input.players_btn_states[0][button_ids.up] = btn_states.just_pressed - state:handle_input() - assert.are_equal(vector(-1, -1), state.player_char.move_intention) - end) - - it('(when input left and down are down) it should update the player character\'s move intention by (-1, 1)', function () - input.players_btn_states[0][button_ids.left] = btn_states.just_pressed - input.players_btn_states[0][button_ids.down] = btn_states.just_pressed - state:handle_input() - assert.are_equal(vector(-1, 1), state.player_char.move_intention) - end) - - it('(when input right and up are down) it should update the player character\'s move intention by (1, -1)', function () - input.players_btn_states[0][button_ids.right] = btn_states.just_pressed - input.players_btn_states[0][button_ids.up] = btn_states.just_pressed - state:handle_input() - assert.are_equal(vector(1, -1), state.player_char.move_intention) - end) - - it('(when input right and down are down) it should update the player character\'s move intention by (1, 1)', function () - input.players_btn_states[0][button_ids.right] = btn_states.just_pressed - input.players_btn_states[0][button_ids.down] = btn_states.just_pressed - state:handle_input() - assert.are_equal(vector(1, 1), state.player_char.move_intention) - end) - - it('(when input o is released) it should update the player character\'s jump intention to false, hold jump intention to false', function () - state:handle_input() - assert.are_same({false, false}, {state.player_char.jump_intention, state.player_char.hold_jump_intention}) - end) - - it('(when input o is just pressed) it should update the player character\'s jump intention to true, hold jump intention to true', function () - input.players_btn_states[0][button_ids.o] = btn_states.just_pressed - state:handle_input() - assert.are_same({true, true}, {state.player_char.jump_intention, state.player_char.hold_jump_intention}) - end) - - it('(when input o is pressed) it should update the player character\'s jump intention to false, hold jump intention to true', function () - input.players_btn_states[0][button_ids.o] = btn_states.pressed - state:handle_input() - assert.are_same({false, true}, {state.player_char.jump_intention, state.player_char.hold_jump_intention}) - end) - - end) - - end) - describe('update_camera', function () before_each(function () @@ -641,14 +518,12 @@ describe('stage', function () describe('update', function () local update_coroutines_stub - local handle_input_stub local player_char_update_stub local check_reached_goal_stub local update_camera_stub setup(function () update_coroutines_stub = stub(state, "update_coroutines") - handle_input_stub = stub(state, "handle_input") player_char_update_stub = stub(player_char, "update") check_reached_goal_stub = stub(state, "check_reached_goal") update_camera_stub = stub(state, "update_camera") @@ -656,7 +531,6 @@ describe('stage', function () teardown(function () update_coroutines_stub:revert() - handle_input_stub:revert() player_char_update_stub:revert() check_reached_goal_stub:revert() update_camera_stub:revert() @@ -664,7 +538,6 @@ describe('stage', function () after_each(function () update_coroutines_stub:clear() - handle_input_stub:clear() player_char_update_stub:clear() check_reached_goal_stub:clear() update_camera_stub:clear() @@ -672,13 +545,11 @@ describe('stage', function () describe('(current substate is play)', function () - it('should call handle_input, player_char:update, check_reached_goal and update_camera', function () + it('should call player_char:update, check_reached_goal and update_camera', function () state.current_substate = stage.substates.play state:update() assert.spy(update_coroutines_stub).was_called(1) assert.spy(update_coroutines_stub).was_called_with(match.ref(state)) - assert.spy(handle_input_stub).was_called(1) - assert.spy(handle_input_stub).was_called_with(match.ref(state)) assert.spy(player_char_update_stub).was_called(1) assert.spy(player_char_update_stub).was_called_with(match.ref(state.player_char)) assert.spy(check_reached_goal_stub).was_called(1) @@ -689,12 +560,11 @@ describe('stage', function () describe('(current substate is result)', function () - it('should call handle_input, player_char:update, check_reached_goal and update_camera', function () + it('should call player_char:update, check_reached_goal and update_camera', function () state.current_substate = stage.substates.result state:update() assert.spy(update_coroutines_stub).was_called(1) assert.spy(update_coroutines_stub).was_called_with(match.ref(state)) - assert.spy(handle_input_stub).was_not_called() assert.spy(player_char_update_stub).was_not_called() assert.spy(check_reached_goal_stub).was_not_called() assert.spy(update_camera_stub).was_not_called() From 461ed91695151496899f7f4196ff48bc8f0bbc83 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 21 Dec 2018 10:14:26 +0000 Subject: [PATCH 005/140] [TEST] Added missing utests for tile_data --- tests/pico8api.lua | 3 +++ tests/utesttile_data.lua | 34 +++++++++++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/tests/pico8api.lua b/tests/pico8api.lua index 7ae6afaf..caf4917a 100644 --- a/tests/pico8api.lua +++ b/tests/pico8api.lua @@ -1,4 +1,7 @@ -- pico-8 api placeholders for tests run under vanilla lua +-- pico8:method calls in non-test scripts should be surrounded by +-- "--#ifn pico8" but don't need a require("pico8api") (since they will +-- always be required by a utest script already requiring bustedhelper) -- functions taken from gamax92's fork of picolove -- https://github.com/gamax92/picolove/blob/master/api.lua diff --git a/tests/utesttile_data.lua b/tests/utesttile_data.lua index 456b1399..b32dca55 100644 --- a/tests/utesttile_data.lua +++ b/tests/utesttile_data.lua @@ -1,20 +1,48 @@ require("bustedhelper") require("game/data/tile_data") +local tile_test_data = require("game/test_data/tile_test_data") describe('tiledata', function () + setup(function () + stub(tile_test_data, "setup") + stub(tile_test_data, "teardown") + stub(pico8, "clear_map") + end) + + teardown(function () + tile_test_data.setup:revert() + tile_test_data.teardown:revert() + pico8.clear_map:revert() + end) + + after_each(function () + tile_test_data.setup:clear() + tile_test_data.teardown:clear() + pico8.clear_map:clear() + end) + describe('setup_map_data', function () - pending('should call setup on tile_test_data (busted only)', function () + it('should call setup on tile_test_data (busted only)', function () + setup_map_data() + assert.spy(tile_test_data.setup).was_called(1) + assert.spy(tile_test_data.setup).was_called_with() end) end) describe('teardown_map_data', function () - pending('should call teardown on tile_test_data (busted only)', function () + it('should call teardown on tile_test_data (busted only)', function () + teardown_map_data() + assert.spy(tile_test_data.teardown).was_called(1) + assert.spy(tile_test_data.teardown).was_called_with() end) end) describe('clear_map', function () - pending('should call clear_map from pico8api (busted only)', function () + it('should call clear_map from pico8api (busted only)', function () + clear_map() + assert.spy(pico8.clear_map).was_called(1) + assert.spy(pico8.clear_map).was_called_with(pico8) end) end) From ff3c0d507ec45584e782d23d000ac6bfa8d6418a Mon Sep 17 00:00:00 2001 From: hsandt Date: Mon, 24 Dec 2018 00:00:18 +0100 Subject: [PATCH 006/140] [DEBUG] Press x to toggle debug motion --- src/game/ingame/playercharacter.lua | 13 +++++++++++ tests/utestplayercharacter.lua | 36 +++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/game/ingame/playercharacter.lua b/src/game/ingame/playercharacter.lua index 1b7aac53..b58f2252 100644 --- a/src/game/ingame/playercharacter.lua +++ b/src/game/ingame/playercharacter.lua @@ -157,9 +157,22 @@ function player_char:handle_input() -- without needing a reset later during update self.jump_intention = is_jump_input_down and input:is_just_pressed(button_ids.o) self.hold_jump_intention = is_jump_input_down + +--#if cheat + if input:is_just_pressed(button_ids.x) then + self:_toggle_debug_motion() + end +--#endif end end +--#if cheat +function player_char:_toggle_debug_motion() + self.motion_mode = self.motion_mode == motion_modes.debug and + motion_modes.platformer or motion_modes.debug +end +--#endif + -- update player position function player_char:update() --#if cheat diff --git a/tests/utestplayercharacter.lua b/tests/utestplayercharacter.lua index 2486d4b7..932436ba 100644 --- a/tests/utestplayercharacter.lua +++ b/tests/utestplayercharacter.lua @@ -297,8 +297,18 @@ describe('player_char', function () describe('handle_input', function () + setup(function () + stub(player_char, "_toggle_debug_motion") + end) + + teardown(function () + player_char._toggle_debug_motion:revert() + end) + after_each(function () input.players_btn_states[0] = generate_initial_btn_states() + + player_char._toggle_debug_motion:clear() end) describe('(when player character control mode is not human)', function () @@ -403,6 +413,32 @@ describe('player_char', function () assert.are_same({false, true}, {pc.jump_intention, pc.hold_jump_intention}) end) + it('(when input x is pressed) it should call _toggle_debug_motion', function () + input.players_btn_states[0][button_ids.x] = btn_states.just_pressed + + pc:handle_input() + + -- implementation + assert.spy(pc._toggle_debug_motion).was_called(1) + assert.spy(pc._toggle_debug_motion).was_called_with(match.ref(pc)) + end) + + end) + + describe('_toggle_debug_motion', function () + + it('(motion mode is platformer) it should toggle motion mode to debug', function () + pc.motion_mode = motion_modes.platformer + pc:_toggle_debug_motion() + assert.are_equal(motion_modes.debug, pc.motion_mode) + end) + + it('(motion mode is debug) it should toggle motion mode to platformer', function () + pc.motion_mode = motion_modes.debug + pc:_toggle_debug_motion() + assert.are_equal(motion_modes.platformer, pc.motion_mode) + end) + end) describe('update', function () From c9d53ea09e623a3c9b018e7253df7f89b02b40ce Mon Sep 17 00:00:00 2001 From: hsandt Date: Mon, 24 Dec 2018 00:37:15 +0100 Subject: [PATCH 007/140] [MOTION] Fixed handle_input not being called on update --- src/game/ingame/playercharacter.lua | 9 +++- tests/utestplayercharacter.lua | 70 ++++++++++++++++++++--------- 2 files changed, 57 insertions(+), 22 deletions(-) diff --git a/src/game/ingame/playercharacter.lua b/src/game/ingame/playercharacter.lua index b58f2252..6a611585 100644 --- a/src/game/ingame/playercharacter.lua +++ b/src/game/ingame/playercharacter.lua @@ -131,8 +131,13 @@ function player_char:move_by(delta_vector) self.position = self.position + delta_vector end +function player_char:update() + self:_handle_input() + self:_update_motion() +end + -- update intention based on current input -function player_char:handle_input() +function player_char:_handle_input() if self.control_mode == control_modes.human then -- move local player_move_intention = vector.zero() @@ -174,7 +179,7 @@ end --#endif -- update player position -function player_char:update() +function player_char:_update_motion() --#if cheat if self.motion_mode == motion_modes.debug then self:_update_debug() diff --git a/tests/utestplayercharacter.lua b/tests/utestplayercharacter.lua index 932436ba..9a1be2ef 100644 --- a/tests/utestplayercharacter.lua +++ b/tests/utestplayercharacter.lua @@ -295,7 +295,37 @@ describe('player_char', function () end) end) - describe('handle_input', function () + describe('_handle_input', function () + + setup(function () + stub(player_char, "_handle_input") + stub(player_char, "_update_motion") + end) + + teardown(function () + player_char._handle_input:revert() + player_char._update_motion:revert() + end) + + after_each(function () + player_char._handle_input:clear() + player_char._update_motion:clear() + end) + + it('should call _handle_input and _update_motion', function () + pc:update() + + -- implementation + assert.spy(pc._handle_input).was_called(1) + assert.spy(pc._handle_input).was_called_with(match.ref(pc)) + assert.spy(pc._update_motion).was_called(1) + assert.spy(pc._update_motion).was_called_with(match.ref(pc)) + end) + + end) + + + describe('_handle_input', function () setup(function () stub(player_char, "_toggle_debug_motion") @@ -319,10 +349,10 @@ describe('player_char', function () it('should do nothing', function () input.players_btn_states[0][button_ids.left] = btn_states.pressed - pc:handle_input() + pc:_handle_input() assert.are_equal(vector:zero(), pc.move_intention) input.players_btn_states[0][button_ids.up] = btn_states.pressed - pc:handle_input() + pc:_handle_input() assert.are_equal(vector:zero(), pc.move_intention) end) @@ -332,91 +362,91 @@ describe('player_char', function () it('(when input left in down) it should update the player character\'s move intention by (-1, 0)', function () input.players_btn_states[0][button_ids.left] = btn_states.pressed - pc:handle_input() + pc:_handle_input() assert.are_equal(vector(-1, 0), pc.move_intention) end) it('(when input right in down) it should update the player character\'s move intention by (1, 0)', function () input.players_btn_states[0][button_ids.right] = btn_states.just_pressed - pc:handle_input() + pc:_handle_input() assert.are_equal(vector(1, 0), pc.move_intention) end) it('(when input left and right are down) it should update the player character\'s move intention by (-1, 0)', function () input.players_btn_states[0][button_ids.left] = btn_states.pressed input.players_btn_states[0][button_ids.right] = btn_states.just_pressed - pc:handle_input() + pc:_handle_input() assert.are_equal(vector(-1, 0), pc.move_intention) end) it('(when input up in down) it should update the player character\'s move intention by (-1, 0)', function () input.players_btn_states[0][button_ids.up] = btn_states.pressed - pc:handle_input() + pc:_handle_input() assert.are_equal(vector(0, -1), pc.move_intention) end) it('(when input down in down) it should update the player character\'s move intention by (0, 1)', function () input.players_btn_states[0][button_ids.down] = btn_states.pressed - pc:handle_input() + pc:_handle_input() assert.are_equal(vector(0, 1), pc.move_intention) end) it('(when input up and down are down) it should update the player character\'s move intention by (0, -1)', function () input.players_btn_states[0][button_ids.up] = btn_states.just_pressed input.players_btn_states[0][button_ids.down] = btn_states.pressed - pc:handle_input() + pc:_handle_input() assert.are_equal(vector(0, -1), pc.move_intention) end) it('(when input left and up are down) it should update the player character\'s move intention by (-1, -1)', function () input.players_btn_states[0][button_ids.left] = btn_states.just_pressed input.players_btn_states[0][button_ids.up] = btn_states.just_pressed - pc:handle_input() + pc:_handle_input() assert.are_equal(vector(-1, -1), pc.move_intention) end) it('(when input left and down are down) it should update the player character\'s move intention by (-1, 1)', function () input.players_btn_states[0][button_ids.left] = btn_states.just_pressed input.players_btn_states[0][button_ids.down] = btn_states.just_pressed - pc:handle_input() + pc:_handle_input() assert.are_equal(vector(-1, 1), pc.move_intention) end) it('(when input right and up are down) it should update the player character\'s move intention by (1, -1)', function () input.players_btn_states[0][button_ids.right] = btn_states.just_pressed input.players_btn_states[0][button_ids.up] = btn_states.just_pressed - pc:handle_input() + pc:_handle_input() assert.are_equal(vector(1, -1), pc.move_intention) end) it('(when input right and down are down) it should update the player character\'s move intention by (1, 1)', function () input.players_btn_states[0][button_ids.right] = btn_states.just_pressed input.players_btn_states[0][button_ids.down] = btn_states.just_pressed - pc:handle_input() + pc:_handle_input() assert.are_equal(vector(1, 1), pc.move_intention) end) it('(when input o is released) it should update the player character\'s jump intention to false, hold jump intention to false', function () - pc:handle_input() + pc:_handle_input() assert.are_same({false, false}, {pc.jump_intention, pc.hold_jump_intention}) end) it('(when input o is just pressed) it should update the player character\'s jump intention to true, hold jump intention to true', function () input.players_btn_states[0][button_ids.o] = btn_states.just_pressed - pc:handle_input() + pc:_handle_input() assert.are_same({true, true}, {pc.jump_intention, pc.hold_jump_intention}) end) it('(when input o is pressed) it should update the player character\'s jump intention to false, hold jump intention to true', function () input.players_btn_states[0][button_ids.o] = btn_states.pressed - pc:handle_input() + pc:_handle_input() assert.are_same({false, true}, {pc.jump_intention, pc.hold_jump_intention}) end) it('(when input x is pressed) it should call _toggle_debug_motion', function () input.players_btn_states[0][button_ids.x] = btn_states.just_pressed - pc:handle_input() + pc:_handle_input() -- implementation assert.spy(pc._toggle_debug_motion).was_called(1) @@ -441,7 +471,7 @@ describe('player_char', function () end) - describe('update', function () + describe('_update_motion', function () local update_platformer_motion_stub local update_debug_stub @@ -464,7 +494,7 @@ describe('player_char', function () describe('(when motion mode is platformer)', function () it('should call _update_platformer_motion', function () - pc:update() + pc:_update_motion() assert.spy(update_platformer_motion_stub).was_called(1) assert.spy(update_platformer_motion_stub).was_called_with(match.ref(pc)) assert.spy(update_debug_stub).was_not_called() @@ -482,7 +512,7 @@ describe('player_char', function () -- . -- * the test revealed a missing return, as _update_platformer_motion was called but shouldn't it('should call _update_debug', function () - pc:update() + pc:_update_motion() assert.spy(update_platformer_motion_stub).was_not_called() assert.spy(update_debug_stub).was_called(1) assert.spy(update_debug_stub).was_called_with(match.ref(pc)) From 0d09da921e08923c5f75e453a4b53e89fff438e7 Mon Sep 17 00:00:00 2001 From: hsandt Date: Mon, 24 Dec 2018 00:53:09 +0100 Subject: [PATCH 008/140] [DEBUG] Fix motion state not updated on toggle_debug_motion --- src/game/ingame/playercharacter.lua | 11 +++++++++-- tests/utestplayercharacter.lua | 24 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/game/ingame/playercharacter.lua b/src/game/ingame/playercharacter.lua index 6a611585..b6a3864a 100644 --- a/src/game/ingame/playercharacter.lua +++ b/src/game/ingame/playercharacter.lua @@ -173,8 +173,15 @@ end --#if cheat function player_char:_toggle_debug_motion() - self.motion_mode = self.motion_mode == motion_modes.debug and - motion_modes.platformer or motion_modes.debug + if self.motion_mode == motion_modes.debug then + -- respawn character at current position. this will in particular: + -- - set the motion mode back to platformer + -- - detect ground and update the motion state correctly + self:spawn_at(self.position) + else -- self.motion_mode == motion_modes.platformer + self.motion_mode = motion_modes.debug + self.debug_velocity = vector.zero() + end end --#endif diff --git a/tests/utestplayercharacter.lua b/tests/utestplayercharacter.lua index 9a1be2ef..4f792d15 100644 --- a/tests/utestplayercharacter.lua +++ b/tests/utestplayercharacter.lua @@ -457,16 +457,40 @@ describe('player_char', function () describe('_toggle_debug_motion', function () + setup(function () + -- don't stub, we need to check if the motion mode actually changed after toggle > spawn_at + spy.on(player_char, "spawn_at") + end) + + teardown(function () + player_char.spawn_at:revert() + end) + + after_each(function () + input.players_btn_states[0] = generate_initial_btn_states() + + player_char.spawn_at:clear() + end) + it('(motion mode is platformer) it should toggle motion mode to debug', function () pc.motion_mode = motion_modes.platformer pc:_toggle_debug_motion() assert.are_equal(motion_modes.debug, pc.motion_mode) + assert.are_equal(vector.zero(), pc.debug_velocity) end) it('(motion mode is debug) it should toggle motion mode to platformer', function () + local previous_position = pc.position -- in case we change it during the spawn pc.motion_mode = motion_modes.debug + pc:_toggle_debug_motion() + + -- interface (partial) assert.are_equal(motion_modes.platformer, pc.motion_mode) + + -- implementation + assert.spy(pc.spawn_at).was_called(1) + assert.spy(pc.spawn_at).was_called_with(match.ref(pc), previous_position) end) end) From 990fc50aea5fbfd876a36033095bbb0e94ef531b Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 28 Dec 2018 14:50:34 +0100 Subject: [PATCH 009/140] [PROJECT] Exclude backup folder from sublime-project --- sonic-2d-tech-demo.sublime-project | 1 + 1 file changed, 1 insertion(+) diff --git a/sonic-2d-tech-demo.sublime-project b/sonic-2d-tech-demo.sublime-project index e6604039..69809c45 100644 --- a/sonic-2d-tech-demo.sublime-project +++ b/sonic-2d-tech-demo.sublime-project @@ -439,6 +439,7 @@ "folder_exclude_patterns": [ ".git", + "backup", "build", "intermediate", "__pycache__" From de1e81428131104f59888e83c975f700a28bd0a3 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 28 Dec 2018 14:51:13 +0100 Subject: [PATCH 010/140] [DEBUG] Don't show trace logs by default --- src/engine/debug/logging.lua | 2 +- tests/utestlogging.lua | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/engine/debug/logging.lua b/src/engine/debug/logging.lua index 087aa763..b971c076 100644 --- a/src/engine/debug/logging.lua +++ b/src/engine/debug/logging.lua @@ -77,7 +77,7 @@ local logger = singleton(function (self) codetuner = true, itest = true, -- trace is considered a category, not a level, so we can toggle it independently from the rest - trace = true + trace = false } self.current_level = logging.level.info self.dump_max_recursion_level = 5 diff --git a/tests/utestlogging.lua b/tests/utestlogging.lua index 7de645b2..76eca1e0 100644 --- a/tests/utestlogging.lua +++ b/tests/utestlogging.lua @@ -109,9 +109,13 @@ describe('logging', function () describe('init', function () - it('should set all active categories flags to true', function () + it('should set all active categories flags to true, except trace', function () for category, _ in pairs(logger.active_categories) do - assert.is_true(logger.active_categories[category], "category '"..category.."' is not active") + if category == "trace" then + assert.is_false(logger.active_categories[category], "category '"..category.."' is active") + else + assert.is_true(logger.active_categories[category], "category '"..category.."' is not active") + end end end) From e81ff4b911ca7a88f1530e1b691897baee9dff87 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 28 Dec 2018 18:18:38 +0100 Subject: [PATCH 011/140] [BUILD] Removed all lines and comment removal from preprocess step (to fix itest DSL multilines) Fixed lines matching (.*) being stripped in configs with no stripped functions at all --- prebuild/preprocess.py | 110 ++---- prebuild/test_preprocess.py | 563 ++++++++++++----------------- sonic-2d-tech-demo.sublime-project | 8 + 3 files changed, 260 insertions(+), 421 deletions(-) diff --git a/prebuild/preprocess.py b/prebuild/preprocess.py index 0bf3b821..e2a3c884 100644 --- a/prebuild/preprocess.py +++ b/prebuild/preprocess.py @@ -7,11 +7,9 @@ from enum import Enum # This script applies preprocessing and stripping to the code: -# 1. it will strip leading and trailing whitespace, ignoring empty lines completely -# 2. it will remove all line comments (doesn't support block comments) -# 3. it will strip all code between #if [symbol] and #endif if symbol is not defined for this config. -# 4. it will enable all code between --[[#pico8 and --#pico8]] -# 5. it will strip debug function calls like log() or assert() +# 1. it will strip all code between #if [symbol] and #endif if symbol is not defined for this config. +# 2. it will enable all code between --[[#pico8 and --#pico8]] (unless stripped by 1.) +# 3. it will strip debug function calls like log() or assert() in configs that don't use those symbols # Config for defined symbols (all configs have pico8, to distinguish from busted using the scripts directly) # Remember that busted will not preprocess at all and will therefore go through all the blocks. @@ -34,7 +32,7 @@ # Functions to strip for each config (not all configs need to be present as keys) # Make sure you never insert gameplay code inside a log or assert (such as assert(coresume(coroutine))) # and always split gameplay/debug code in 2 lines -# Also make sure all your sripped function calls are on 1 line (multi-line regex catch is currently not supported) +# Also make sure all your stripped function calls are on 1 line (multi-line regex catch is currently not supported) # or that you surround them with the corresponding #if (e.g. #if assert for a multi-line assert) stripped_functions_table = { 'debug': [], @@ -67,28 +65,27 @@ class ParsingMode(Enum): pico8_start_pattern = re.compile(r"--\[\[#pico8") # closing tag for pico8-only block. Unlike normal comment blocks, we expect to match from the line start and we ignore anything after the block end! pico8_end_pattern = re.compile(r"--#pico8]]") -# capture the previous part (we recommend to start a line with --[[, but in case it is found in the middle of a line) -block_comment_start_pattern = re.compile(r"(.*)--\[\[") -# capture the part after (same, we recommend to end a line with --]]) -block_comment_end_pattern = re.compile(r"(?:.*)]](.*)") -# Known limitation: an open/close block comment on a single line won't be detected. Use a line comment in this case! if_pattern = re.compile(r"--#if (\w+)") # ! ignore anything after 1st symbol ifn_pattern = re.compile(r"--#ifn (\w+)") # ! ignore anything after 1st symbol endif_pattern = re.compile(r"--#endif") -comment_pattern = re.compile(r'("[^"\\]*(?:\\.[^"\\]*)*")|(?:--.*)') stripped_function_call_patterns_table = {} for config, stripped_functions in stripped_functions_table.items(): - # many good regex exist to match open and closing brackets, unfortunately they use PCRE features like ?> unsupported in Python re - # so we use a very simple regex, but remember to never put anything fancy on a log/assert line that may have side effects, since they will be stripped on release - # ex: '^(?:log|warn|err)\(.*\)$' - # for better regex with PCRE, see: - # https://stackoverflow.com/questions/2148587/finding-quoted-strings-with-escaped-quotes-in-c-sharp-using-a-regular-expression - # https://stackoverflow.com/questions/4568410/match-comments-with-regex-but-not-inside-a-quote adapted to lua comments - # https://stackoverflow.com/questions/546433/regular-expression-to-match-outer-brackets#546457 - # https://stackoverflow.com/questions/18906514/regex-for-matching-functions-and-capturing-their-arguments#18908330 - function_name_alternative_pattern = f"(?:{'|'.join(stripped_functions)})" - stripped_function_call_patterns_table[config] = re.compile(f'^{function_name_alternative_pattern}\\(.*\\)$') + # if there is nothing to strip, avoid creating a regex with just "(?:)\(\)" that would match a line starting with brackets + if stripped_functions: + # many good regex exist to match open and closing brackets, unfortunately they use PCRE features like ?> unsupported in Python re + # so we use a very simple regex, but remember to never put anything fancy on a log/assert line that may have side effects, since they will be stripped on release + # comments after call are ok + # for better regex with PCRE to detect surrounding brackets and quotes, see: + # https://stackoverflow.com/questions/2148587/finding-quoted-strings-with-escaped-quotes-in-c-sharp-using-a-regular-expression + # https://stackoverflow.com/questions/4568410/match-comments-with-regex-but-not-inside-a-quote adapted to lua comments + # https://stackoverflow.com/questions/546433/regular-expression-to-match-outer-brackets#546457 + # https://stackoverflow.com/questions/18906514/regex-for-matching-functions-and-capturing-their-arguments#18908330 + + # ex: '(?:log|warn|err)' + function_name_alternative_pattern = f"(?:{'|'.join(stripped_functions)})" + # ex: '^\s*(?:log|warn|err)\(.*\)\s*(?:--.*)?$' + stripped_function_call_patterns_table[config] = re.compile(rf'^\s*{function_name_alternative_pattern}\(.*\)\s*(?:--.*)?$') def preprocess_dir(dirpath, config): """Apply preprocessor directives to all the source files inside the given directory, for the given config""" @@ -148,7 +145,6 @@ def preprocess_lines(lines, config): preprocessed_lines = [] inside_pico8_block = False - inside_comment_block = False # explore the tree of #if by storing the current stack of ifs encountered from top to bottom if_block_modes_stack = [] # can only be filled with [IfBlockMode.ACCEPTED*, IfBlockMode.REFUSED?, IfBlockMode.IGNORED* (only if 1 REFUSED)] @@ -196,77 +192,31 @@ def preprocess_lines(lines, config): if last_mode is IfBlockMode.REFUSED: current_mode = ParsingMode.ACTIVE elif current_mode is ParsingMode.ACTIVE: - force_append = False if pico8_start_pattern.match(line): - if not inside_comment_block: + # we detected a pico8 block and should continue appending the lines normally (since we are building for pico8) + # the bool flag is only here to check that 1 end pattern will match 1 start pattern + # since we don't really need embedded pico8 blocks, we assume only 1 level and don't use a stack here + if not inside_pico8_block: inside_pico8_block = True - # we must not append this line - continue else: - logging.warning('a pico8 block start was encountered inside a comment block. It will be ignored') + logging.warning('a pico8 block start was encountered inside a pico8 block. It will be ignored') elif pico8_end_pattern.match(line): if inside_pico8_block: - if inside_comment_block: - logging.warning('a pico8 block end was encountered inside a pico8 block, but also inside a comment block. It will still end the pico8 block, but crossing blocks like this will end in weird behavior in busted') inside_pico8_block = False - # we must not append this line - continue else: logging.warning('a pico8 block end was encountered outside a pico8 block. It will be ignored') else: - block_comment_start_match = block_comment_start_pattern.match(line) - if block_comment_start_match: - inside_comment_block = True - # preserve part just before block start if any - # you need to force append in this case because inside_comment_block is now True - line = block_comment_start_match.group(1) - if line: - force_append = True - else: - block_comment_end_match = block_comment_end_pattern.match(line) - if block_comment_end_match: - # only end block comment if inside a comment - # else, this is a legit Lua case, where ]] will be interpreted as normal code - # of course, --]] would still be stripped as a comment - if inside_comment_block: - inside_comment_block = False - # technically we are now outside the block, so it's important to only retrieve the part of the line after comment closure - # so we don't append the whole line containing the block end itself - line = block_comment_end_match.group(1) - - # inside a pico8 block, we continue appending the lines (since we are preprocessing, so we are building for pico8) - if not inside_comment_block or force_append: - line = strip_line_content(line, config) - # if resulting line is empty, ignore it - if line: - # we stripped eol, so re-add it now - preprocessed_lines.append(line + '\n') + if not is_function_call_to_strip(line, config): + preprocessed_lines.append(line) if if_block_modes_stack: logging.warning('file ended inside an --#if block. Make sure the block is closed by an --#endif directive') return preprocessed_lines -def strip_line_content(line, config): - """Strip line content as much as possible. Return line without eol. May be empty.""" - # 2. strip comments first (so we can trim whitespace left by after-code comment afterward) - line = strip_comments(line) - # 1. strip blanks (this includes any remaining end of line) - line = line.strip() - # 4. strip debug function calls if not debug - line = strip_function_calls(line, config) - return line - -def strip_comments(line): - # this will keep trailing whitespaces as well as eol, but we count on strip to finish the job - # \1 will preserve the original code - return comment_pattern.sub('\\1', line) - - -def strip_function_calls(line, config): - if config in stripped_function_call_patterns_table and stripped_function_call_patterns_table[config].match(line): - return '' - else: - return line + +def is_function_call_to_strip(line, config): + """Return true iff the line contains a function call (and optionally a comment) that should be stripped in the passed config""" + return config in stripped_function_call_patterns_table and bool(stripped_function_call_patterns_table[config].match(line)) if __name__ == '__main__': diff --git a/prebuild/test_preprocess.py b/prebuild/test_preprocess.py index 8aa01694..8aa38024 100644 --- a/prebuild/test_preprocess.py +++ b/prebuild/test_preprocess.py @@ -6,103 +6,74 @@ import shutil, tempfile -class TestPreprocess(unittest.TestCase): +class TestPreprocessLines(unittest.TestCase): - def test_strip_comments_full_line(self): - test_line = '-- my comment\n' - self.assertEqual(preprocess.strip_comments(test_line), '\n') + def test_is_function_call_to_strip_pure_brackets_false(self): + test_line = '(5)\n' + self.assertFalse(preprocess.is_function_call_to_strip(test_line, 'debug')) - def test_strip_comments_after_code(self): - test_line = 'print("hi") -- prints hi\n' - self.assertEqual(preprocess.strip_comments(test_line), 'print("hi") \n') - - def test_strip_comments_outside_quotes(self): - test_line = 'print("hi -- this is \"not\" a comment") -- prints hi\n' - self.assertEqual(preprocess.strip_comments(test_line), 'print("hi -- this is \"not\" a comment") \n') + def test_is_function_call_to_strip_log_in_debug_false(self): + test_line = 'log(5)\n' + self.assertFalse(preprocess.is_function_call_to_strip(test_line, 'debug')) - def test_strip_comments_after_code(self): - test_line = '"some \\"text" print("hi -- this is \"not\" a comment") -- prints hi "more text" -- more comment\n' - self.assertEqual(preprocess.strip_comments(test_line), '"some \\"text" print("hi -- this is "not" a comment") \n') + def test_is_function_call_to_strip_log_in_debug_with_comment_false(self): + test_line = ' log("character moves", "[character]") -- logging\n' + self.assertFalse(preprocess.is_function_call_to_strip(test_line, 'debug')) - def test_strip_function_calls_no_change(self): - test_line = 'log(5)\n' - self.assertEqual(preprocess.strip_function_calls(test_line, 'debug'), test_line) + def test_is_function_call_to_strip_log_in_release_with_comment_true(self): + test_line = ' log("character moves", "[character]") -- logging\n' + expected_processed_lines = '' + self.assertTrue(preprocess.is_function_call_to_strip(test_line, 'release')) - def test_strip_function_calls_empty_string(self): + def test_is_function_call_to_strip_log_in_release_true(self): test_line = 'log("character moves", "[character]")\n' - self.assertEqual(preprocess.strip_function_calls(test_line, 'release'), '') + self.assertTrue(preprocess.is_function_call_to_strip(test_line, 'release')) - def test_strip_function_calls_tricky_bracket(self): + def test_is_function_call_to_strip_tricky_bracket(self): test_line = 'log("inside quotes )", "[character]")\n' - self.assertEqual(preprocess.strip_function_calls(test_line, 'release'), '') + self.assertTrue(preprocess.is_function_call_to_strip(test_line, 'release')) - def test_strip_function_calls_embedded_brackets(self): + def test_is_function_call_to_strip_embedded_brackets(self): test_line = 'log(value.evaluate(with.style()), "[character]")\n' - self.assertEqual(preprocess.strip_function_calls(test_line, 'release'), '') + self.assertTrue(preprocess.is_function_call_to_strip(test_line, 'release')) - def test_strip_function_calls_tricky_quotes(self): + def test_is_function_call_to_strip_tricky_quotes(self): test_line = 'log("inside quotes )\\"", "[character]")\n' - self.assertEqual(preprocess.strip_function_calls(test_line, 'release'), '') + self.assertTrue(preprocess.is_function_call_to_strip(test_line, 'release')) def test_strip_function_after_something_else(self): test_line = 'dont strip log(this)\n' - self.assertEqual(preprocess.strip_function_calls(test_line, 'release'), test_line) + self.assertFalse(preprocess.is_function_call_to_strip(test_line, 'release')) def test_strip_function_after_something_else(self): test_line = 'log(this) shouldnt be stripped\n' - self.assertEqual(preprocess.strip_function_calls(test_line, 'release'), test_line) + self.assertFalse(preprocess.is_function_call_to_strip(test_line, 'release')) - def test_strip_function_calls_not_alone(self): + def test_is_function_call_to_strip_not_alone(self): test_line = 'log("inside quotes", "[character]") or a = 3\n' - self.assertEqual(preprocess.strip_function_calls(test_line, 'release'), test_line) + self.assertFalse(preprocess.is_function_call_to_strip(test_line, 'release')) - def test_strip_function_calls_not_alone2(self): + def test_is_function_call_to_strip_not_alone2(self): test_line = 'log("inside quotes )\\"", "[character]") or a = 3\n' - self.assertEqual(preprocess.strip_function_calls(test_line, 'release'), test_line) + self.assertFalse(preprocess.is_function_call_to_strip(test_line, 'release')) @unittest.skip("regex is not good enough to detect last bracket does not belong to the log") - def test_strip_function_calls_not_alone_end_bracket(self): + def test_is_function_call_to_strip_not_alone_end_bracket(self): test_line = 'log("inside quotes )\\"", "[character]") or fancy_side_effect()\n' - self.assertEqual(preprocess.strip_function_calls(test_line, 'release'), test_line) + self.assertFalse(preprocess.is_function_call_to_strip(test_line, 'release')) - def test_strip_line_content_debug(self): - test_line = ' log("character moves", "[character]") -- logging\n' - expected_processed_line = 'log("character moves", "[character]")' - self.assertEqual(preprocess.strip_line_content(test_line, 'debug'), expected_processed_line) - - def test_strip_line_content_release(self): - test_line = ' log("character moves", "[character]") -- logging\n' - expected_processed_lines = '' - self.assertEqual(preprocess.strip_line_content(test_line, 'release'), '') - - def test_preprocess_strip_blanks_after_comments(self): + def test_preprocess_lines_no_directives_preserve(self): test_lines = [ 'print ("hi") \n', '\n', 'if true: \n', ' -- prints hello\n', ' print("hello") -- comment\n', - '\n' - ] - expected_processed_lines = [ - 'print ("hi")\n', - 'if true:\n', - 'print("hello")\n' - ] - self.assertEqual(preprocess.preprocess_lines(test_lines, 'debug'), expected_processed_lines) - - def test_preprocess_no_directives(self): - test_lines = [ - 'if true:\n', - ' print("hello")\n' - ] - expected_processed_lines = [ - 'if true:\n', - 'print("hello")\n' + '\n', ] - self.assertEqual(preprocess.preprocess_lines(test_lines, 'debug'), expected_processed_lines) + self.assertEqual(preprocess.preprocess_lines(test_lines, 'debug'), test_lines) - def test_preprocess_if_log_in_debug(self): + def test_preprocess_lines_if_log_in_debug(self): test_lines = [ 'print("always")\n', '\n', @@ -111,17 +82,19 @@ def test_preprocess_if_log_in_debug(self): '--#endif\n', '\n', 'if true:\n', - ' print("hello") -- prints hello\n' + ' print("hello") -- prints hello\n', ] expected_processed_lines = [ 'print("always")\n', + '\n', 'print("debug")\n', + '\n', 'if true:\n', - 'print("hello")\n' + ' print("hello") -- prints hello\n', ] self.assertEqual(preprocess.preprocess_lines(test_lines, 'debug'), expected_processed_lines) - def test_preprocess_if_log_in_release(self): + def test_preprocess_lines_if_log_in_release(self): test_lines = [ 'print("always")\n', '\n', @@ -130,16 +103,18 @@ def test_preprocess_if_log_in_release(self): '--#endif\n', '\n', 'if true:\n', - 'print("hello") -- prints hello\n' + ' print("hello") -- prints hello\n', ] expected_processed_lines = [ 'print("always")\n', + '\n', + '\n', 'if true:\n', - 'print("hello")\n' + ' print("hello") -- prints hello\n', ] self.assertEqual(preprocess.preprocess_lines(test_lines, 'release'), expected_processed_lines) - def test_preprocess_2nd_if_refused(self): + def test_preprocess_lines_2nd_if_refused(self): test_lines = [ '--#if log\n', 'print("debug")\n', @@ -148,17 +123,18 @@ def test_preprocess_2nd_if_refused(self): '--#endif\n', '\n', 'if true:\n', - 'print("hello") -- prints hello\n' + ' print("hello") -- prints hello\n', '--#endif\n', ] expected_processed_lines = [ 'print("debug")\n', + '\n', 'if true:\n', - 'print("hello")\n' + ' print("hello") -- prints hello\n', ] self.assertEqual(preprocess.preprocess_lines(test_lines, 'debug'), expected_processed_lines) - def test_preprocess_3rd_if_still_ignore(self): + def test_preprocess_lines_3rd_if_still_ignore(self): test_lines = [ '--#if log\n', 'print("debug")\n', @@ -171,17 +147,18 @@ def test_preprocess_3rd_if_still_ignore(self): '--#endif\n', '\n', 'if true:\n', - 'print("hello") -- prints hello\n' + ' print("hello") -- prints hello\n', '--#endif\n', ] expected_processed_lines = [ 'print("debug")\n', + '\n', 'if true:\n', - 'print("hello")\n' + ' print("hello") -- prints hello\n', ] self.assertEqual(preprocess.preprocess_lines(test_lines, 'debug'), expected_processed_lines) - def test_preprocess_3rd_if_ignored_even_if_true(self): + def test_preprocess_lines_3rd_if_ignored_even_if_true(self): test_lines = [ '--#if log\n', 'print("debug")\n', @@ -194,31 +171,32 @@ def test_preprocess_3rd_if_ignored_even_if_true(self): '--#endif\n', '\n', 'if true:\n', - 'print("hello") -- prints hello\n' + ' print("hello") -- prints hello\n', '--#endif\n', ] expected_processed_lines = [ 'print("debug")\n', + '\n', 'if true:\n', - 'print("hello")\n' + ' print("hello") -- prints hello\n', ] self.assertEqual(preprocess.preprocess_lines(test_lines, 'debug'), expected_processed_lines) - def test_preprocess_ifn(self): + def test_preprocess_lines_ifn(self): test_lines = [ 'print("always")\n', '--#ifn log\n', 'print("no log")\n', '--#endif\n', - 'print("hello")\n' + 'print("hello")\n', ] expected_processed_lines = [ 'print("always")\n', - 'print("hello")\n' + 'print("hello")\n', ] self.assertEqual(preprocess.preprocess_lines(test_lines, 'debug'), expected_processed_lines) - def test_preprocess_if_and_ifn(self): + def test_preprocess_lines_if_and_ifn(self): test_lines = [ 'print("always")\n', '--#if log\n', @@ -227,16 +205,16 @@ def test_preprocess_if_and_ifn(self): '--#ifn log\n', 'print("no log")\n', '--#endif\n', - 'print("hello")\n' + 'print("hello")\n', ] expected_processed_lines = [ 'print("always")\n', 'print("log")\n', - 'print("hello")\n' + 'print("hello")\n', ] self.assertEqual(preprocess.preprocess_lines(test_lines, 'debug'), expected_processed_lines) - def test_preprocess_ifn_inside_if(self): + def test_preprocess_lines_ifn_inside_if(self): test_lines = [ 'print("always")\n', '--#if log\n', @@ -246,17 +224,17 @@ def test_preprocess_ifn_inside_if(self): '--#endif\n', 'print("log 2")\n', '--#endif\n', - 'print("hello")\n' + 'print("hello")\n', ] expected_processed_lines = [ 'print("always")\n', 'print("log")\n', 'print("log 2")\n', - 'print("hello")\n' + 'print("hello")\n', ] self.assertEqual(preprocess.preprocess_lines(test_lines, 'debug'), expected_processed_lines) - def test_preprocess_if_inside_ifn(self): + def test_preprocess_lines_if_inside_ifn(self): test_lines = [ 'print("always")\n', '--#ifn log\n', @@ -266,175 +244,81 @@ def test_preprocess_if_inside_ifn(self): '--#endif\n', 'print("no log 2")\n', '--#endif\n', - 'print("hello")\n' + 'print("hello")\n', ] expected_processed_lines = [ 'print("always")\n', - 'print("hello")\n' + 'print("hello")\n', ] self.assertEqual(preprocess.preprocess_lines(test_lines, 'debug'), expected_processed_lines) - def test_preprocess_ifn_log_in_release(self): + def test_preprocess_lines_ifn_log_in_release(self): test_lines = [ 'print("always")\n', '--#ifn log\n', 'print("no log")\n', '--#endif\n', - 'print("hello")\n' + 'print("hello")\n', ] expected_processed_lines = [ 'print("always")\n', 'print("no log")\n', - 'print("hello")\n' + 'print("hello")\n', ] self.assertEqual(preprocess.preprocess_lines(test_lines, 'release'), expected_processed_lines) - def test_preprocess_immediate_endif_ignored(self): + def test_preprocess_lines_immediate_endif_ignored(self): test_lines = [ '--#endif\n', 'if true:\n', - ' print("hello") -- prints hello\n' + ' print("hello") -- prints hello\n', ] expected_processed_lines = [ 'if true:\n', - 'print("hello")\n' + ' print("hello") -- prints hello\n', ] # this will also trigger a warning, but we don't test it self.assertEqual(preprocess.preprocess_lines(test_lines, 'debug'), expected_processed_lines) - def test_preprocess_missing_endif_ignored(self): + def test_preprocess_lines_missing_endif_ignored(self): test_lines = [ '--#if log\n', 'print("debug")\n', '\n', 'if true:\n', - 'print("hello") -- prints hello\n' + ' print("hello") -- prints hello\n', ] expected_processed_lines = [ ] # this will also trigger a warning, but we don't test it self.assertEqual(preprocess.preprocess_lines(test_lines, 'release'), expected_processed_lines) - def test_preprocess_comment_block(self): - test_lines = [ - 'print("start")\n', - '--[[ comment start\n', - 'more comment\n', - '--]]\n', - 'print("end")\n' - ] - expected_processed_lines = [ - 'print("start")\n', - 'print("end")\n' - ] - logging.info("NOW") - self.assertEqual(preprocess.preprocess_lines(test_lines, 'release'), expected_processed_lines) - - def test_preprocess_comment_block_in_the_middle(self): - test_lines = [ - 'print("start")\n', - 'some code --[[ comment start\n', - 'more comment\n', - 'end comment ]] more core\n', - 'print("end")\n' - ] - expected_processed_lines = [ - 'print("start")\n', - 'some code\n', - 'more core\n', - 'print("end")\n' - ] - logging.info("NOW") - self.assertEqual(preprocess.preprocess_lines(test_lines, 'release'), expected_processed_lines) - - def test_preprocess_stop_comment_outside_comment_block(self): - test_lines = [ - 'print("start")\n', - 'legit ]] code\n', - 'print("end")\n' - ] - expected_processed_lines = [ - 'print("start")\n', - 'legit ]] code\n', # warning, ignore - 'print("end")\n' - ] - logging.info("NOW") - self.assertEqual(preprocess.preprocess_lines(test_lines, 'release'), expected_processed_lines) - - def test_preprocess_pico8_block(self): + def test_preprocess_lines_pico8_block(self): test_lines = [ 'print("start")\n', '--[[#pico8 pico8 start\n', 'real pico8 code\n', '--#pico8]] exceptionally ignored\n', - 'print("end")\n' + 'print("end")\n', ] expected_processed_lines = [ 'print("start")\n', 'real pico8 code\n', - 'print("end")\n' + 'print("end")\n', ] self.assertEqual(preprocess.preprocess_lines(test_lines, 'release'), expected_processed_lines) - def test_preprocess_pico8_comment_block(self): - test_lines = [ - 'print("start")\n', - '--[[#pico8 pico8 start\n', - 'real pico8 code\n', - 'and --[[ comment inside\n', - 'more comment\n', - 'and ]] over\n', - 'more pico8 code', - '--#pico8]] exceptionally ignored\n', - 'print("end")\n' - ] - expected_processed_lines = [ - 'print("start")\n', - 'real pico8 code\n', - 'and\n', - 'over\n', - 'more pico8 code\n', - 'print("end")\n' - ] - self.assertEqual(preprocess.preprocess_lines(test_lines, 'release'), expected_processed_lines) - - def test_preprocess_comment_pico8_block(self): - test_lines = [ - '--[[ comment\n', - '--[[#pico8\n', # warning here, ignored - 'comment\n', - ']] over\n' - ] - expected_processed_lines = [ - 'over\n' - ] - self.assertEqual(preprocess.preprocess_lines(test_lines, 'release'), expected_processed_lines) - - def test_preprocess_stop_pico8_inside_comment_block(self): - test_lines = [ - '--[[#pico8\n', - '--[[ comment\n', - 'comment\n', - '--#pico8]]\n', # warning here, will still close - 'still comment\n', # warning here, will still close - ']] code is back\n' - ] - expected_processed_lines = [ - 'code is back\n' - ] - self.assertEqual(preprocess.preprocess_lines(test_lines, 'release'), expected_processed_lines) - - def test_preprocess_stop_pico8_outside_pico8_block(self): + def test_preprocess_lines_stop_pico8_outside_pico8_block(self): test_lines = [ '--#pico8]]\n', # warning here, ignored - 'code\n' + 'code\n', ] expected_processed_lines = [ - 'code\n' + 'code\n', ] self.assertEqual(preprocess.preprocess_lines(test_lines, 'release'), expected_processed_lines) - def test_preprocess_ignored_if_inside_pico8_block(self): + def test_preprocess_lines_refused_if_inside_pico8_block(self): test_lines = [ 'print("start")\n', '--[[#pico8 pico8 start\n', @@ -443,16 +327,16 @@ def test_preprocess_ignored_if_inside_pico8_block(self): 'log only\n', '--#endif\n', '--#pico8]] exceptionally ignored\n', - 'print("end")\n' + 'print("end")\n', ] expected_processed_lines = [ 'print("start")\n', 'real pico8 code\n', - 'print("end")\n' + 'print("end")\n', ] self.assertEqual(preprocess.preprocess_lines(test_lines, 'release'), expected_processed_lines) - def test_preprocess_accepted_if_inside_pico8_block(self): + def test_preprocess_lines_accepted_if_inside_pico8_block(self): test_lines = [ 'print("start")\n', '--[[#pico8 pico8 start\n', @@ -461,17 +345,17 @@ def test_preprocess_accepted_if_inside_pico8_block(self): 'log only\n', '--#endif\n', '--#pico8]] exceptionally ignored\n', - 'print("end")\n' + 'print("end")\n', ] expected_processed_lines = [ 'print("start")\n', 'real pico8 code\n', 'log only\n', - 'print("end")\n' + 'print("end")\n', ] self.assertEqual(preprocess.preprocess_lines(test_lines, 'debug'), expected_processed_lines) - def test_preprocess_ignored_ifn_inside_pico8_block(self): + def test_preprocess_lines_refused_ifn_inside_pico8_block(self): test_lines = [ 'print("start")\n', '--[[#pico8 pico8 start\n', @@ -480,16 +364,16 @@ def test_preprocess_ignored_ifn_inside_pico8_block(self): 'release only\n', '--#endif\n', '--#pico8]] exceptionally ignored\n', - 'print("end")\n' + 'print("end")\n', ] expected_processed_lines = [ 'print("start")\n', 'real pico8 code\n', - 'print("end")\n' + 'print("end")\n', ] self.assertEqual(preprocess.preprocess_lines(test_lines, 'debug'), expected_processed_lines) - def test_preprocess_accepted_ifn_inside_pico8_block(self): + def test_preprocess_lines_accepted_ifn_inside_pico8_block(self): test_lines = [ 'print("start")\n', '--[[#pico8 pico8 start\n', @@ -498,43 +382,17 @@ def test_preprocess_accepted_ifn_inside_pico8_block(self): 'release only\n', '--#endif\n', '--#pico8]] exceptionally ignored\n', - 'print("end")\n' + 'print("end")\n', ] expected_processed_lines = [ 'print("start")\n', 'real pico8 code\n', 'release only\n', - 'print("end")\n' - ] - self.assertEqual(preprocess.preprocess_lines(test_lines, 'release'), expected_processed_lines) - - @unittest.skip("we don't use a perfect regex able to detect start and end of block comment on the same line") - def test_preprocess_comment_block_start_end_in_the_middle(self): - test_lines = [ - 'print("start")\n', - '--[[ --]] outside comment\n', - ] - expected_processed_lines = [ - 'print("start")\n', - 'outside comment\n' - ] - self.assertEqual(preprocess.preprocess_lines(test_lines, 'release'), expected_processed_lines) - - @unittest.skip("we don't use a perfect regex able to detect end and start of block comment on the same line") - def test_preprocess_comment_block_end_start_in_the_middle(self): - test_lines = [ - 'print("start")\n', - '--[[\n', - ']] outside comment --[[\n', - ']]\n' - ] - expected_processed_lines = [ - 'print("start")\n', - 'outside comment\n' + 'print("end")\n', ] self.assertEqual(preprocess.preprocess_lines(test_lines, 'release'), expected_processed_lines) - def test_preprocess_with_unknown_config(self): + def test_preprocess_lines_with_unknown_config(self): test_lines = [] self.assertRaises(ValueError, preprocess.preprocess_lines, test_lines, 'unknown') @@ -550,51 +408,62 @@ def tearDown(self): shutil.rmtree(self.test_dir) def test_preprocess_file_in_debug(self): - test_lines = [ - 'print("always")\n', - '\n', - '--#if log\n', - 'print("debug")\n', - '--#endif\n', - '\n', - 'if true:\n', - 'print("hello") -- prints hello\n' - ] - expected_processed_lines = [ - 'print("always")\n', - 'print("debug")\n', - 'if true:\n', - 'print("hello")\n' - ] + test_code = """ +print("always") + +--#if log +print("debug") +--#endif + +if true: + print("hello") -- prints hello + log("debug only") -- +""" + + expected_processed_code = """ +print("always") + +print("debug") + +if true: + print("hello") -- prints hello + log("debug only") -- +""" + test_filepath = path.join(self.test_dir, 'test.lua') with open(test_filepath, 'w') as f: - f.write('\n'.join(test_lines)) + f.write(test_code) preprocess.preprocess_file(test_filepath, 'debug') with open(test_filepath, 'r') as f: - self.assertEqual(f.read(), ''.join(expected_processed_lines)) + self.assertEqual(f.read(), expected_processed_code) def test_preprocess_file_in_release(self): - test_lines = [ - 'print("always")\n', - '\n', - '--#if log\n', - 'print("debug")\n', - '--#endif\n', - '\n', - 'if true:\n', - ' print("hello") -- prints hello\n' - ] - expected_processed_lines = [ - 'print("always")\n', - 'if true:\n', - 'print("hello")\n' - ] + test_code = """ +print("always") + +--#if log +print("debug") +--#endif + +if true: + print("hello") -- prints hello + log("debug only") -- +""" + + expected_processed_code = """ +print("always") + + +if true: + print("hello") -- prints hello +""" + test_filepath = path.join(self.test_dir, 'test.lua') with open(test_filepath, 'w') as f: - f.write('\n'.join(test_lines)) + f.write(test_code) preprocess.preprocess_file(test_filepath, 'release') with open(test_filepath, 'r') as f: - self.assertEqual(f.read(), ''.join(expected_processed_lines)) + self.assertEqual(f.read(), expected_processed_code) class TestPreprocessDir(unittest.TestCase): @@ -607,94 +476,106 @@ def tearDown(self): shutil.rmtree(self.test_dir) def test_preprocess_dir_in_debug(self): - test_lines1 = [ - 'print("file1")\n', - '\n', - '--#if log\n', - 'print("debug1")\n', - '--#endif\n', - '\n', - 'if true:\n', - ' print("hello") -- prints hello\n' - ] - test_lines2 = [ - 'print("file2")\n', - '\n', - '--#if log\n', - 'print("debug2")\n', - '--#endif\n', - '\n', - 'if true:\n', - ' print("hello2") -- prints hello\n' - ] - expected_processed_lines1 = [ - 'print("file1")\n', - 'print("debug1")\n', - 'if true:\n', - 'print("hello")\n' - ] - expected_processed_lines2 = [ - 'print("file2")\n', - 'print("debug2")\n', - 'if true:\n', - 'print("hello2")\n' - ] + test_code1 = """ +print("file1") + +--#if log +print("debug1") +--#endif + +if true: + print("hello") -- prints hello +""" + + test_code2 = """ +print("file2") + +--#if log +print("debug2") +--#endif + +if true: + print("hello2") -- prints hello +""" + + expected_processed_code1 = """ +print("file1") +print("debug1") +if true: + print("hello") +""" + + expected_processed_code2 = """ +print("file2") +print("debug2") +if true: + print("hello2") +""" + # files must end with .lua to be processed test_filepath1 = path.join(self.test_dir, 'test1.lua') with open(test_filepath1, 'w') as f1: - f1.write('\n'.join(test_lines1)) + f1.write(test_code1) test_filepath2 = path.join(self.test_dir, 'test2.lua') with open(test_filepath2, 'w') as f2: - f2.write('\n'.join(test_lines2)) + f2.write(test_code2) preprocess.preprocess_dir(self.test_dir, 'debug') with open(test_filepath1, 'r') as f1: - self.assertEqual(f1.read(), '\n'.join(expected_processed_lines1)) + self.assertEqual(f1.read(), expected_processed_code1) with open(test_filepath2, 'r') as f2: - self.assertEqual(f2.read(), '\n'.join(expected_processed_lines2)) + self.assertEqual(f2.read(), expected_processed_code2) def test_preprocess_dir_in_debug(self): - test_lines1 = [ - 'print("file1")\n', - '\n', - '--#if log\n', - 'print("debug1")\n', - '--#endif\n', - '\n', - 'if true:\n', - ' print("hello") -- prints hello\n' - ] - test_lines2 = [ - 'print("file2")\n', - '\n', - '--#if log\n', - 'print("debug2")\n', - '--#endif\n', - '\n', - 'if true:\n', - ' print("hello2") -- prints hello\n' - ] - expected_processed_lines1 = [ - 'print("file1")\n', - 'if true:\n', - 'print("hello")\n' - ] - expected_processed_lines2 = [ - 'print("file2")\n', - 'if true:\n', - 'print("hello2")\n' - ] + test_code1 = """ +print("file1") + +--#if log +print("debug1") +--#endif + +if true: + print("hello") -- prints hello +""" + + test_code2 = """ +print("file2") + +--#if log +print("debug2") +--#endif + +if true: + print("hello2") -- prints hello +""" + + expected_processed_code1 = """ +print("file1") + + +if true: + print("hello") -- prints hello +""" + + expected_processed_code2 = """ +print("file2") + + +if true: + print("hello2") -- prints hello +""" + # files must end with .lua to be processed test_filepath1 = path.join(self.test_dir, 'test1.lua') with open(test_filepath1, 'w') as f1: - f1.write('\n'.join(test_lines1)) + f1.write(test_code1) test_filepath2 = path.join(self.test_dir, 'test2.lua') with open(test_filepath2, 'w') as f2: - f2.write('\n'.join(test_lines2)) + f2.write(test_code2) preprocess.preprocess_dir(self.test_dir, 'release') with open(test_filepath1, 'r') as f1: - self.assertEqual(f1.read(), ''.join(expected_processed_lines1)) + self.assertEqual(f1.read(), expected_processed_code1) with open(test_filepath2, 'r') as f2: - self.assertEqual(f2.read(), ''.join(expected_processed_lines2)) + self.assertEqual(f2.read(), expected_processed_code2) if __name__ == '__main__': logging.basicConfig(level=logging.ERROR) diff --git a/sonic-2d-tech-demo.sublime-project b/sonic-2d-tech-demo.sublime-project index 69809c45..8b97d883 100644 --- a/sonic-2d-tech-demo.sublime-project +++ b/sonic-2d-tech-demo.sublime-project @@ -267,6 +267,14 @@ "cmd": ["pico8 -run data/data.p8"], "working_dir": "${project_path}" }, + { + "name": "python: test preprocess (TestPreprocessLines)", + "working_dir": "${project_path}", + "shell": true, + "cmd": ["python3 -m unittest prebuild.test_preprocess.TestPreprocessLines"], + "file_regex": "\"(.*\\.py)\"(?:, line )([\\d]+)(?:\\:)?([\\d]+)?, (.*)", + "syntax": "Packages/Python/Python.sublime-syntax" + }, { "name": "python: test preprocess", "working_dir": "${project_path}", From 1b779f4dc8acbc3e684d400c5804cf4553de05b8 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 28 Dec 2018 18:22:20 +0100 Subject: [PATCH 012/140] [TEST] Switched back to multi-line itest DSL --- src/game/itests/itestplayercharacter.lua | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/game/itests/itestplayercharacter.lua b/src/game/itests/itestplayercharacter.lua index 65a6a421..41718797 100644 --- a/src/game/itests/itestplayercharacter.lua +++ b/src/game/itests/itestplayercharacter.lua @@ -18,16 +18,16 @@ local itest -- pico8 doesn't like [[]] and will replace lines after the 3rd with -- empty lines... need "text \n".. or "text \ to continue to next line -itest_dsl.register('#solo platformer accel right flat', -"@stage # \ -... \ -### \ - \ -warp 4 8 \ -move right \ -wait 30 \ -expect pc_bottom_pos 14.8984375 8. \ -" +itest_dsl.register('#solo platformer accel right flat', [[ +@stage # +... +### + +warp 4 8 +move right +wait 30 +expect pc_bottom_pos 14.8984375 8. +]] ) --[[ original From ab838bfb7c965006adf90f3f5b83a6cd5310bc80 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 28 Dec 2018 18:22:45 +0100 Subject: [PATCH 013/140] [SANDBOX] Added comments in sandbox to test minification --- src/game/sandbox.lua | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/game/sandbox.lua b/src/game/sandbox.lua index 429cf739..4023cd81 100644 --- a/src/game/sandbox.lua +++ b/src/game/sandbox.lua @@ -1,4 +1,5 @@ require("engine/core/math") +require("engine/core/helper") -- caveats @@ -9,3 +10,20 @@ require("engine/core/math") printh("27"..vector(11, 45)) -- or -- printh(tostr(27)..vector(11, 45)) + +s = [[ +1 + +2]] + +lines = strspl(s, "\n") + +-- COMMENT +--[[BLOCk +COMMENT]] +for line in all(lines) do + print("line: "..line) +end + +--[[ +--]] From cf1cbe200879688735c3d214ecbe60ea901d7678 Mon Sep 17 00:00:00 2001 From: hsandt Date: Sat, 29 Dec 2018 11:27:41 +0100 Subject: [PATCH 014/140] [TEST] DSL: support multiple expectations --- src/engine/test/itest_dsl.lua | 164 +++-- src/game/itests/itestplayercharacter.lua | 5 +- tests/utestitest_dsl.lua | 842 ++++++++++++----------- tests/uteststage.lua | 3 +- 4 files changed, 567 insertions(+), 447 deletions(-) diff --git a/src/engine/test/itest_dsl.lua b/src/engine/test/itest_dsl.lua index 100cbf85..2c618081 100644 --- a/src/engine/test/itest_dsl.lua +++ b/src/engine/test/itest_dsl.lua @@ -10,13 +10,9 @@ local flow = require("engine/application/flow") local stage = require("game/ingame/stage") -- required local pc_data = require("game/data/playercharacter_data") --- itest dsl parser singleton, with parser context state --- _itest integration_test current integration test in construction --- _last_time_trigger time_trigger last time trigger registered with wait command -local itest_dsl = singleton(function (self) - self._itest = nil - self._last_time_trigger = nil -end) + +-- module +local itest_dsl = {} -- type of commands available @@ -52,13 +48,27 @@ function command:_init(cmd_type, args) end +-- expectation struct + +-- attributes +-- gp_value_type itest_dsl_gp_value_types type of gameplay value to compare +-- expected_value {type used for gp_value_type} expected gameplay value +local expectation = new_struct() +itest_dsl.expectation = expectation + +function expectation:_init(gp_value_type, expected_value) + self.gp_value_type = gp_value_type + self.expected_value = expected_value +end + + -- dsl itest struct -- attributes --- gamestate_type string gamestate type to start test in (also the only active gamestate) --- stage_name string|nil stage name to play if gamestate type is 'stage', nil else +-- gamestate_type string gamestate type to start test in (also the only active gamestate) +-- stage_name string|nil stage name to play if gamestate type is 'stage', nil else -- tilemap tilemap|nil tilemap data if gamestate type is 'stage', nil else --- commands {commands} sequence of commands to apply +-- commands {command} sequence of commands to apply local dsl_itest = new_struct() itest_dsl.dsl_itest = dsl_itest @@ -67,10 +77,21 @@ function dsl_itest:_init() end +-- itest dsl parser singleton, with parser context state +-- _itest integration_test current integration test in construction +-- _last_time_trigger time_trigger last time trigger registered with wait command +-- _final_expectations {expectation} sequence of expectations to verify +local itest_dsl_parser = singleton(function (self) + self._itest = nil + self._last_time_trigger = nil + self._final_expectations = {} +end) +itest_dsl.itest_dsl_parser = itest_dsl_parser + -- parse, create and register itest from dsl -function itest_dsl.register(name, dsli_source) - local dsli = itest_dsl.parse(dsli_source) - local test = itest_dsl.create_itest(name, dsli) +function itest_dsl_parser.register(name, dsli_source) + local dsli = itest_dsl_parser.parse(dsli_source) + local test = itest_dsl_parser.create_itest(name, dsli) itest_manager:register(test) end @@ -90,7 +111,7 @@ end -- wait 30 < wait delays the next action (here, the nil action) -- expect pc_bottom_pos 14. 8. < expectation (only final assertion is supported) -- ]] -function itest_dsl.parse(dsli_source) +function itest_dsl_parser.parse(dsli_source) -- create dsl itest local dsli = dsl_itest() @@ -99,8 +120,8 @@ function itest_dsl.parse(dsli_source) -- parse in 2 steps: gamestate and action sequence local next_line_index - dsli.gamestate_type, dsli.stage_name, dsli.tilemap, next_line_index = itest_dsl.parse_gamestate_definition(lines) - dsli.commands = itest_dsl.parse_action_sequence(lines, next_line_index) + dsli.gamestate_type, dsli.stage_name, dsli.tilemap, next_line_index = itest_dsl_parser.parse_gamestate_definition(lines) + dsli.commands = itest_dsl_parser.parse_action_sequence(lines, next_line_index) return dsli end @@ -113,7 +134,7 @@ end -- [tilemap row n] -- < blank after tilemap (or one-line gamestate definition) to mark the end -- ? < we don't check what's after, just return this line's index -function itest_dsl.parse_gamestate_definition(lines) +function itest_dsl_parser.parse_gamestate_definition(lines) -- parse first line to get state and optional stage local gamestate_header = lines[1] assert(sub(gamestate_header, 1, 1) == '@', "gamestate_header '"..gamestate_header.."' doesn't start with @") @@ -129,13 +150,13 @@ function itest_dsl.parse_gamestate_definition(lines) local next_line_index = 3 if stage_name == '#' then -- we are defining a custom tilemap, let's parse it - tm, next_line_index = itest_dsl.parse_tilemap(lines) + tm, next_line_index = itest_dsl_parser.parse_tilemap(lines) end return gamestate_type, stage_name, tm, next_line_index end -function itest_dsl.parse_tilemap(lines) +function itest_dsl_parser.parse_tilemap(lines) -- tilemap should always start at line 2 -- first line will give the tilemap width assert(#lines >= 2, "only "..#lines.." line(s), need at least 2") @@ -182,7 +203,7 @@ function itest_dsl.parse_tilemap(lines) end -function itest_dsl.parse_action_sequence(lines, next_line_index) +function itest_dsl_parser.parse_action_sequence(lines, next_line_index) local commands = {} for i = next_line_index, #lines do words = strspl(lines[i], ' ', true) @@ -195,8 +216,8 @@ function itest_dsl.parse_action_sequence(lines, next_line_index) end local cmd_type = itest_dsl_command_types[cmd_type_str] local parse_fn_name = '_parse_args_'..cmd_type_str - assert(itest_dsl[parse_fn_name], "parse function '"..parse_fn_name.."' is not defined") - local args = {itest_dsl[parse_fn_name](arg_strings)} + assert(itest_dsl_parser[parse_fn_name], "parse function '"..parse_fn_name.."' is not defined") + local args = {itest_dsl_parser[parse_fn_name](arg_strings)} add(commands, command(cmd_type, args)) end end @@ -204,27 +225,27 @@ function itest_dsl.parse_action_sequence(lines, next_line_index) end -- convert string args to vector -function itest_dsl._parse_args_warp(args) +function itest_dsl_parser._parse_args_warp(args) assert(#args == 2, "got "..#args.." args") return vector(tonum(args[1]), tonum(args[2])) -- bottom position end -- convert string args to vector -function itest_dsl._parse_args_move(args) +function itest_dsl_parser._parse_args_move(args) assert(#args == 1, "got "..#args.." args") return horizontal_dirs[args[1]] -- move intention end -- convert string args to vector -function itest_dsl._parse_args_wait(args) +function itest_dsl_parser._parse_args_wait(args) assert(#args == 1, "got "..#args.." args") return tonum(args[1]) -- frames to wait end -- convert string args to vector -function itest_dsl._parse_args_expect(args) +function itest_dsl_parser._parse_args_expect(args) assert(#args > 1, "got "..#args.." args") - -- same principle as itest_dsl.parse, the type of the first arg + -- same principle as itest_dsl_parser.parse, the type of the first arg -- determines how we parse the rest of the args, named "value components" local value_type_str = args[1] -- gather all the value components as strings (e.g. {"3", "4"} for vector(3, 4)) @@ -236,22 +257,22 @@ function itest_dsl._parse_args_expect(args) local value_type = itest_dsl_gp_value_types[value_type_str] -- parse the value components to semantical type (e.g. vector) local parse_fn_name = '_parse_value_'..value_type_str - assert(itest_dsl[parse_fn_name], "parse function '"..parse_fn_name.."' is not defined") - local expected_value = itest_dsl[parse_fn_name](expected_value_comps) + assert(itest_dsl_parser[parse_fn_name], "parse function '"..parse_fn_name.."' is not defined") + local expected_value = itest_dsl_parser[parse_fn_name](expected_value_comps) return value_type, expected_value end -- convert string args to vector -function itest_dsl._parse_value_pc_bottom_pos(args) +function itest_dsl_parser._parse_value_pc_bottom_pos(args) assert(#args == 2, "got "..#args.." args") return vector(tonum(args[1]), tonum(args[2])) end -- create and return an itest from a dsli, providing a name -function itest_dsl.create_itest(name, dsli) - itest_dsl._itest = integration_test(name, {dsli.gamestate_type}) +function itest_dsl_parser.create_itest(name, dsli) + itest_dsl_parser._itest = integration_test(name, {dsli.gamestate_type}) - itest_dsl._itest.setup = function () + itest_dsl_parser._itest.setup = function () flow:change_gamestate_by_type(dsli.gamestate_type) if dsli.gamestate_type == "stage" then -- puppet control @@ -266,7 +287,7 @@ function itest_dsl.create_itest(name, dsli) end end - itest_dsl._itest.teardown = function () + itest_dsl_parser._itest.teardown = function () flow:change_gamestate_by_type(dsli.gamestate_type) if dsli.gamestate_type == "stage" then if dsli.stage_name == '#' then @@ -279,37 +300,39 @@ function itest_dsl.create_itest(name, dsli) for cmd in all(dsli.commands) do if cmd.type == itest_dsl_command_types.warp then - itest_dsl:_act(function () + itest_dsl_parser:_act(function () stage.state.player_char:warp_bottom_to(vector(cmd.args[1].x, cmd.args[1].y)) end) elseif cmd.type == itest_dsl_command_types.move then - itest_dsl:_act(function () + itest_dsl_parser:_act(function () stage.state.player_char.move_intention = horizontal_dir_vectors[cmd.args[1]] end) elseif cmd.type == itest_dsl_command_types.wait then - itest_dsl:_wait(cmd.args[1]) + itest_dsl_parser:_wait(cmd.args[1]) elseif cmd.type == itest_dsl_command_types.expect then -- we currently don't support live assertions, only final assertion - itest_dsl:_final_assert(unpack(cmd.args)) + itest_dsl_parser:_add_final_expectation(unpack(cmd.args)) end end + -- if we finished with a wait (with or without final assertion), -- we need to close the itest with a wait-action - if itest_dsl._last_time_trigger then - itest_dsl._itest:add_action(itest_dsl._last_time_trigger, nil) + if itest_dsl_parser._last_time_trigger then + itest_dsl_parser._itest:add_action(itest_dsl_parser._last_time_trigger, nil) + itest_dsl_parser._last_time_trigger = nil -- consume and cleanup for next itest end - local test = itest_dsl._itest + -- glue code to remain retro-compatible with function-based final assertion + itest_dsl_parser:_define_final_assertion() - -- cleanup - itest_dsl._itest = nil - itest_dsl._last_time_trigger = nil + local test = itest_dsl_parser._itest + itest_dsl_parser._itest = nil -- consume and cleanup for next itest return test end -function itest_dsl:_act(callback) +function itest_dsl_parser:_act(callback) if self._last_time_trigger then self._itest:add_action(self._last_time_trigger, callback) self._last_time_trigger = nil -- consume so we know no final wait-action is needed @@ -319,7 +342,7 @@ function itest_dsl:_act(callback) end end -function itest_dsl:_wait(interval) +function itest_dsl_parser:_wait(interval) if self._last_time_trigger then -- we were already waiting, so finish last wait with empty action self._itest:add_action(self._last_time_trigger, nil) @@ -328,22 +351,51 @@ function itest_dsl:_wait(interval) self._last_time_trigger = integrationtest.time_trigger(interval, true) end -function itest_dsl:_final_assert(gp_value_type, expected_gp_value) - local gp_value_name = value_type_strings[gp_value_type] - assert(gp_value_name, "invalid gp_value_type: "..gp_value_type) +-- add final expectation to sequence, for future evaluation +function itest_dsl_parser:_add_final_expectation(gp_value_type, expected_gp_value) + add(self._final_expectations, expectation(gp_value_type, expected_gp_value)) +end + +-- define final assertion based on sequence of final expectations +-- this is a glue method to make it retro-compatible with the function-based final assertion +-- eventually, the itest will only hold expectations (possibly predefined functions for currying) +-- to avoid creating lambda +function itest_dsl_parser:_define_final_assertion() + -- define an intermediate local variable to avoid the "local variable closure issue" + -- i.e. if we access "self._final_expectations" directly from inside the function + -- constructed below, it would get the actual value of self._final_expectations + -- at evaluation time (too late, the temporary table reference would have been lost + -- and the table gc-ed). So we either need to copy the table content (then clear table) + -- or store the reference in an intermediate variable like this one (then create new table) + local final_expectations_proxy = self._final_expectations + self._final_expectations = {} -- consume and cleanup for next itest + self._itest.final_assertion = function () - local gp_value = self._evaluate(gp_value_type) - return gp_value == expected_gp_value, - "Passed gameplay value '"..gp_value_name.."':\n".. - gp_value.."\n".. - "Expected:\n".. - expected_gp_value + local success = true + local full_message = "" + + -- check each expectation one by one + for exp in all(final_expectations_proxy) do + local gp_value = self._evaluate(exp.gp_value_type) + if gp_value ~= exp.expected_value then + success = false + local gp_value_name = value_type_strings[exp.gp_value_type] + assert(gp_value_name, "invalid exp.gp_value_type: "..exp.gp_value_type) + local message = "Passed gameplay value '"..gp_value_name.."':\n".. + gp_value.."\n".. + "Expected:\n".. + exp.expected_value + full_message = full_message..message.."\n" + end + end + + return success, full_message end end -- evaluate gameplay value. it is important to call this at expect -- time, not when defining the test, to get the actual runtime value -function itest_dsl._evaluate(gp_value_type) +function itest_dsl_parser._evaluate(gp_value_type) if gp_value_type == itest_dsl_gp_value_types.pc_bottom_pos then return stage.state.player_char:get_bottom_center() else diff --git a/src/game/itests/itestplayercharacter.lua b/src/game/itests/itestplayercharacter.lua index 41718797..d2973f72 100644 --- a/src/game/itests/itestplayercharacter.lua +++ b/src/game/itests/itestplayercharacter.lua @@ -1,6 +1,7 @@ -- gamestates: stage local integrationtest = require("engine/test/integrationtest") local itest_dsl = require("engine/test/itest_dsl") +local itest_dsl_parser = itest_dsl.itest_dsl_parser local itest_manager, integration_test, time_trigger = integrationtest.itest_manager, integrationtest.integration_test, integrationtest.time_trigger local input = require("engine/input/input") local flow = require("engine/application/flow") @@ -14,11 +15,12 @@ local tile_test_data = require("game/test_data/tile_test_data") local itest + -- dsl training -- pico8 doesn't like [[]] and will replace lines after the 3rd with -- empty lines... need "text \n".. or "text \ to continue to next line -itest_dsl.register('#solo platformer accel right flat', [[ +itest_dsl_parser.register('#solo platformer accel right flat', [[ @stage # ... ### @@ -30,6 +32,7 @@ expect pc_bottom_pos 14.8984375 8. ]] ) + --[[ original itest.setup = function () -- stage.state.player_char.motion_mode = motion_modes.platformer diff --git a/tests/utestitest_dsl.lua b/tests/utestitest_dsl.lua index 4d73ae28..7734ba1b 100644 --- a/tests/utestitest_dsl.lua +++ b/tests/utestitest_dsl.lua @@ -1,7 +1,7 @@ require("bustedhelper") require("math") local itest_dsl = require("engine/test/itest_dsl") -local dsl_itest, command = itest_dsl.dsl_itest, itest_dsl.command +local dsl_itest, command, expectation, itest_dsl_parser = itest_dsl.dsl_itest, itest_dsl.command, itest_dsl.expectation, itest_dsl.itest_dsl_parser local integrationtest = require("engine/test/integrationtest") local itest_manager, time_trigger, integration_test = integrationtest.itest_manager, integrationtest.time_trigger, integrationtest.integration_test local flow = require("engine/application/flow") @@ -15,28 +15,6 @@ local pc_data = require("game/data/playercharacter_data") describe('itest_dsl', function () - setup(function () - -- spying should be enough, but we stub so it's easier to call these functions - -- without calling the symmetrical one (e.g. teardown may fail with nil reference - -- if setup is not called first) - stub(_G, "setup_map_data") - stub(_G, "teardown_map_data") - end) - - teardown(function () - setup_map_data:revert() - teardown_map_data:revert() - end) - - after_each(function () - itest_dsl:init() - flow:init() - stage.state:init() - pico8:clear_map() - setup_map_data:clear() - teardown_map_data:clear() - end) - describe('command', function () describe('_init', function () @@ -49,6 +27,18 @@ describe('itest_dsl', function () end) + describe('expectation', function () + + describe('_init', function () + it('should create a new dsl itest', function () + local exp = expectation(itest_dsl_gp_value_types.pc_bottom_pos, 24) + assert.is_not_nil(exp) + assert.are_same({itest_dsl_gp_value_types.pc_bottom_pos, 24}, {exp.gp_value_type, exp.expected_value}) + end) + end) + + end) + describe('dsl_itest', function () describe('_init', function () @@ -61,42 +51,79 @@ describe('itest_dsl', function () end) - describe('register', function () + describe('itest_dsl_parser', function () setup(function () - -- mock parse - stub(itest_dsl, "parse", function (dsli_source) - return dsli_source.."_parsed" - end) - -- mock create_itest - stub(itest_dsl, "create_itest", function (name, dsli) - return name..": "..dsli.."_itest" - end) + -- spying should be enough, but we stub so it's easier to call these functions + -- without calling the symmetrical one (e.g. teardown may fail with nil reference + -- if setup is not called first) + stub(_G, "setup_map_data") + stub(_G, "teardown_map_data") end) teardown(function () - itest_dsl.parse:revert() - itest_dsl.create_itest:revert() + setup_map_data:revert() + teardown_map_data:revert() end) after_each(function () - itest_manager:init() + itest_dsl_parser:init() + flow:init() + stage.state:init() + pico8:clear_map() + setup_map_data:clear() + teardown_map_data:clear() end) - it('should parse, create and register an itest by name and source', function () - itest_dsl.register("my test", "dsl_source") - assert.are_equal(1, #itest_manager.itests) - assert.are_equal("my test: dsl_source_parsed_itest", itest_manager.itests[1]) + describe('init', function () + assert.are_same({ + nil, + nil, + {} + }, + { + itest_dsl_parser._itest, + itest_dsl_parser._last_time_trigger, + itest_dsl_parser._final_expectations + }) end) - end) + describe('register', function () + + setup(function () + -- mock parse + stub(itest_dsl_parser, "parse", function (dsli_source) + return dsli_source.."_parsed" + end) + -- mock create_itest + stub(itest_dsl_parser, "create_itest", function (name, dsli) + return name..": "..dsli.."_itest" + end) + end) - describe('parse', function () + teardown(function () + itest_dsl_parser.parse:revert() + itest_dsl_parser.create_itest:revert() + end) - -- bugfix history: - -- + spot tilemap not being set, although parse_gamestate_definition worked, so the error is in the glue code - it('should parse the itest source written in domain-specific language into a dsl itest', function () - local dsli_source = [[@stage # + after_each(function () + itest_manager:init() + end) + + it('should parse, create and register an itest by name and source', function () + itest_dsl_parser.register("my test", "dsl_source") + assert.are_equal(1, #itest_manager.itests) + assert.are_equal("my test: dsl_source_parsed_itest", itest_manager.itests[1]) + end) + + end) + + describe('parse', function () + + -- bugfix history: + -- + spot tilemap not being set, although parse_gamestate_definition worked, so the error is in the glue code + it('should parse the itest source written in domain-specific language into a dsl itest', function () + local dsli_source = [[@stage # ..## ##.. @@ -106,416 +133,453 @@ move left wait 2 expect pc_bottom_pos 10 45 ]] - local dsli = itest_dsl.parse(dsli_source) + local dsli = itest_dsl_parser.parse(dsli_source) - -- interface - assert.is_not_nil(dsli) - assert.are_same( - { - 'stage', - '#', - tilemap({ - { 0, 0, 64, 64}, - {64, 64, 0, 0} - }), + -- interface + assert.is_not_nil(dsli) + assert.are_same( { - command(itest_dsl_command_types.warp, { vector(12, 45) } ), - command(itest_dsl_command_types.wait, { 1 } ), - command(itest_dsl_command_types.move, { horizontal_dirs.left } ), - command(itest_dsl_command_types.wait, { 2 } ), - command(itest_dsl_command_types.expect, {itest_dsl_gp_value_types.pc_bottom_pos, vector(10, 45)}), - } - }, - { - dsli.gamestate_type, - dsli.stage_name, - dsli.tilemap, - dsli.commands - }) + 'stage', + '#', + tilemap({ + { 0, 0, 64, 64}, + {64, 64, 0, 0} + }), + { + command(itest_dsl_command_types.warp, { vector(12, 45) } ), + command(itest_dsl_command_types.wait, { 1 } ), + command(itest_dsl_command_types.move, { horizontal_dirs.left } ), + command(itest_dsl_command_types.wait, { 2 } ), + command(itest_dsl_command_types.expect, {itest_dsl_gp_value_types.pc_bottom_pos, vector(10, 45)}), + } + }, + { + dsli.gamestate_type, + dsli.stage_name, + dsli.tilemap, + dsli.commands + }) + + -- implementation + -- todo: check call to parse_gamestate_definition and parse_action_sequence + -- to avoid test redundancy + end) - -- implementation - -- todo: check call to parse_gamestate_definition and parse_action_sequence - -- to avoid test redundancy end) - end) + describe('parse_gamestate_definition', function () - describe('parse_gamestate_definition', function () + it('should return gamestate name, nil, nil and 3 for a non-stage gamestate and no extra line', function () + local dsli_lines = {"@titlemenu"} + local gamestate_type, stage_name, tm, next_line_index = itest_dsl_parser.parse_gamestate_definition(dsli_lines) + assert.are_same( + { + 'titlemenu', + nil, + nil, + 3 + }, + { + gamestate_type, + stage_name, + tm, + next_line_index + }) + end) - it('should return gamestate name, nil, nil and 3 for a non-stage gamestate and no extra line', function () - local dsli_lines = {"@titlemenu"} - local gamestate_type, stage_name, tm, next_line_index = itest_dsl.parse_gamestate_definition(dsli_lines) - assert.are_same( - { - 'titlemenu', - nil, - nil, - 3 - }, - { - gamestate_type, - stage_name, - tm, - next_line_index - }) - end) + it('should return \'stage\', the stage name, nil and 4 for a pre-defined stage definition after 1 blank line', function () + local dsli_lines = { + "@stage test1", + "", + "", + "???" + } + local gamestate_type, stage_name, tm, next_line_index = itest_dsl_parser.parse_gamestate_definition(dsli_lines) + assert.are_same( + { + 'stage', + "test1", + nil, + 3 + }, + { + gamestate_type, + stage_name, + tm, + next_line_index + }) + end) - it('should return \'stage\', the stage name, nil and 4 for a pre-defined stage definition after 1 blank line', function () - local dsli_lines = { - "@stage test1", - "", - "", - "???" - } - local gamestate_type, stage_name, tm, next_line_index = itest_dsl.parse_gamestate_definition(dsli_lines) - assert.are_same( - { - 'stage', - "test1", - nil, - 3 - }, - { - gamestate_type, - stage_name, - tm, - next_line_index - }) - end) + it('should return \'stage\', \'#\', tilemap data and 6 for a custom stage definition finishing at line 5 (including blank line)', function () + local dsli_lines = { + "@stage #", + "....", + "##..", + "..##", + "", + "???" + } + local gamestate_type, stage_name, tm, next_line_index = itest_dsl_parser.parse_gamestate_definition(dsli_lines) + assert.are_same( + { + 'stage', + '#', + tilemap({ + { 0, 0, 0, 0}, + {64, 64, 0, 0}, + { 0, 0, 64, 64} + }), + 6 + }, + { + gamestate_type, + stage_name, + tm, + next_line_index + }) + end) - it('should return \'stage\', \'#\', tilemap data and 6 for a custom stage definition finishing at line 5 (including blank line)', function () - local dsli_lines = { - "@stage #", - "....", - "##..", - "..##", - "", - "???" - } - local gamestate_type, stage_name, tm, next_line_index = itest_dsl.parse_gamestate_definition(dsli_lines) - assert.are_same( - { - 'stage', - '#', - tilemap({ - { 0, 0, 0, 0}, - {64, 64, 0, 0}, - { 0, 0, 64, 64} - }), - 6 - }, - { - gamestate_type, - stage_name, - tm, - next_line_index - }) end) - end) + -- bugfix history: + -- + removed "local" in "local width =" inside loop after applying trimming + -- to lines to support " \" multilines + describe('parse_tilemap', function () + + it('should return an empty tilemap data if the 2nd line is blank', function () + local tilemap_text = { + "@stage # (ignored)", + "", + ".... (ignored)", -- next line: 3 + ".... (ignored)" + } + local tm, next_line_index = itest_dsl_parser.parse_tilemap(tilemap_text) + assert.are_same( + { + tilemap({}), + 3 + }, + {tm, next_line_index}) + end) - -- bugfix history: - -- + removed "local" in "local width =" inside loop after applying trimming - -- to lines to support " \" multilines - describe('parse_tilemap', function () - - it('should return an empty tilemap data if the 2nd line is blank', function () - local tilemap_text = { - "@stage # (ignored)", - "", - ".... (ignored)", -- next line: 3 - ".... (ignored)" - } - local tm, next_line_index = itest_dsl.parse_tilemap(tilemap_text) - assert.are_same( - { - tilemap({}), - 3 - }, - {tm, next_line_index}) - end) + it('should return a tilemap data with tiles corresponding to the tile symbols in the string', function () + local tilemap_text = { + "@stage # (ignored)", + "....", + "##..", + "..##", + "", + "(ignored)", -- next line: 6 + "(ignored)" + } + local tm, next_line_index = itest_dsl_parser.parse_tilemap(tilemap_text) + assert.are_same( + { + tilemap({ + { 0, 0, 0, 0}, + {64, 64, 0, 0}, + { 0, 0, 64, 64} + }), + 6 + }, + {tm, next_line_index}) + end) - it('should return a tilemap data with tiles corresponding to the tile symbols in the string', function () - local tilemap_text = { - "@stage # (ignored)", - "....", - "##..", - "..##", - "", - "(ignored)", -- next line: 6 - "(ignored)" - } - local tm, next_line_index = itest_dsl.parse_tilemap(tilemap_text) - assert.are_same( - { - tilemap({ - { 0, 0, 0, 0}, - {64, 64, 0, 0}, - { 0, 0, 64, 64} - }), - 6 - }, - {tm, next_line_index}) - end) + it('should assert if there as fewer than 2 lines', function () + local tilemap_text = { + "?" + } + assert.has_error(function () + itest_dsl_parser.parse_tilemap(tilemap_text) + end, "only 1 line(s), need at least 2") + end) - it('should assert if there as fewer than 2 lines', function () - local tilemap_text = { - "?" - } - assert.has_error(function () - itest_dsl.parse_tilemap(tilemap_text) - end, "only 1 line(s), need at least 2") - end) + it('should assert if there are too many blocks', function () + local tilemap_text = { + "@stage # (ignored)", + "... ..." + } + assert.has_error(function () + itest_dsl_parser.parse_tilemap(tilemap_text) + end, "too many blocks: 2, expected 1") + end) - it('should assert if there are too many blocks', function () - local tilemap_text = { - "@stage # (ignored)", - "... ..." - } - assert.has_error(function () - itest_dsl.parse_tilemap(tilemap_text) - end, "too many blocks: 2, expected 1") - end) + it('should assert if line width is inconsistent', function () + local tilemap_text = { + "@stage # (ignored)", + "....", + "..." + } + assert.has_error(function () + itest_dsl_parser.parse_tilemap(tilemap_text) + end, "inconsistent line length: 3 vs 4") + end) - it('should assert if line width is inconsistent', function () - local tilemap_text = { - "@stage # (ignored)", - "....", - "..." - } - assert.has_error(function () - itest_dsl.parse_tilemap(tilemap_text) - end, "inconsistent line length: 3 vs 4") - end) + it('should assert if unknown tile symbol is found', function () + local tilemap_text = { + "@stage # (ignored)", + "?" + } + assert.has_error(function () + itest_dsl_parser.parse_tilemap(tilemap_text) + end, "unknown tile symbol: ?") + end) - it('should assert if unknown tile symbol is found', function () - local tilemap_text = { - "@stage # (ignored)", - "?" - } - assert.has_error(function () - itest_dsl.parse_tilemap(tilemap_text) - end, "unknown tile symbol: ?") end) - end) + describe('parse_action_sequence', function () + + it('should return ', function () + local dsli_lines = { + "???", + "???", + "???", + "", + "warp 12 45", + "wait 1", + "move left", + "wait 2", + "expect pc_bottom_pos 10 45" + } + local commands = itest_dsl_parser.parse_action_sequence(dsli_lines, 5) + assert.are_same( + { + command(itest_dsl_command_types.warp, { vector(12, 45) } ), + command(itest_dsl_command_types.wait, { 1 } ), + command(itest_dsl_command_types.move, { horizontal_dirs.left } ), + command(itest_dsl_command_types.wait, { 2 } ), + command(itest_dsl_command_types.expect, {itest_dsl_gp_value_types.pc_bottom_pos, vector(10, 45)}), + }, + commands) + end) - describe('parse_action_sequence', function () - - it('should return ', function () - local dsli_lines = { - "???", - "???", - "???", - "", - "warp 12 45", - "wait 1", - "move left", - "wait 2", - "expect pc_bottom_pos 10 45" - } - local commands = itest_dsl.parse_action_sequence(dsli_lines, 5) - assert.are_same( - { - command(itest_dsl_command_types.warp, { vector(12, 45) } ), - command(itest_dsl_command_types.wait, { 1 } ), - command(itest_dsl_command_types.move, { horizontal_dirs.left } ), - command(itest_dsl_command_types.wait, { 2 } ), - command(itest_dsl_command_types.expect, {itest_dsl_gp_value_types.pc_bottom_pos, vector(10, 45)}), - }, - commands) end) - end) + describe('create_itest', function () - describe('create_itest', function () - - it('should create an itest with a name and a dsl itest', function () - local dsli = dsl_itest() - dsli.gamestate_type = 'stage' - dsli.stage_name = "test1" - dsli.tilemap = nil - dsli.commands = { - command(itest_dsl_command_types.warp, { vector(12, 45) } ), - command(itest_dsl_command_types.wait, { 10 } ), - command(itest_dsl_command_types.wait, { 1 } ), - command(itest_dsl_command_types.move, { horizontal_dirs.left } ), - command(itest_dsl_command_types.wait, { 2 } ), - command(itest_dsl_command_types.expect, {itest_dsl_gp_value_types.pc_bottom_pos, vector(10, 45)}), - } - - local test = itest_dsl.create_itest("test 1", dsli) - - -- interface - assert.is_not_nil(test) - assert.are_equal(4, #test.action_sequence) - assert.are_same({ - "test 1", - {'stage'}, - time_trigger(0, true), -- warp immediately - scripted_action(time_trigger(10, true), nil), -- empty action after 10 frames - time_trigger(1, true), -- start moving after 1 frame - scripted_action(time_trigger(2, true), nil) -- empty action after 2 frames - }, - { - test.name, - test.active_gamestates, - test.action_sequence[1].trigger, - test.action_sequence[2], - test.action_sequence[3].trigger, - test.action_sequence[4] - }) + it('should create an itest with a name and a dsl itest', function () + local dsli = dsl_itest() + dsli.gamestate_type = 'stage' + dsli.stage_name = "test1" + dsli.tilemap = nil + dsli.commands = { + command(itest_dsl_command_types.warp, { vector(12, 45) } ), + command(itest_dsl_command_types.wait, { 10 } ), + command(itest_dsl_command_types.wait, { 1 } ), + command(itest_dsl_command_types.move, { horizontal_dirs.left } ), + command(itest_dsl_command_types.wait, { 2 } ), + command(itest_dsl_command_types.expect, {itest_dsl_gp_value_types.pc_bottom_pos, vector(10, 45)}), + } + + local test = itest_dsl_parser.create_itest("test 1", dsli) - -- we could not directly test if generated functions are correct - -- they were generated from parameters passed dynamically, - -- so it's impossible to find the references back (except for dummy) - -- instead, we call the functions one by one and see if we get - -- the expected result - -- note that most actions depend on the previous one, so we exceptionally - -- assert multiple times in chain in a single utest + -- interface + assert.is_not_nil(test) + assert.are_equal(4, #test.action_sequence) + assert.are_same({ + "test 1", + {'stage'}, + time_trigger(0, true), -- warp immediately + scripted_action(time_trigger(10, true), nil), -- empty action after 10 frames + time_trigger(1, true), -- start moving after 1 frame + scripted_action(time_trigger(2, true), nil) -- empty action after 2 frames + }, + { + test.name, + test.active_gamestates, + test.action_sequence[1].trigger, + test.action_sequence[2], + test.action_sequence[3].trigger, + test.action_sequence[4] + }) + + -- we could not directly test if generated functions are correct + -- they were generated from parameters passed dynamically, + -- so it's impossible to find the references back (except for dummy) + -- instead, we call the functions one by one and see if we get + -- the expected result + -- note that most actions depend on the previous one, so we exceptionally + -- assert multiple times in chain in a single utest + + -- simulate the itest runner behavior by initializing gameapp to inject active gamestates + gameapp.init(test.active_gamestates) - -- simulate the itest runner behavior by initializing gameapp to inject active gamestates - gameapp.init(test.active_gamestates) + -- verify setup callback behavior + test.setup() + assert.are_equal(gamestate.types.stage, flow.curr_state.type) + + -- verify warp callback behavior + test.action_sequence[1].callback() + assert.is_not_nil(stage.state.player_char) + assert.are_equal(vector(12, 45 - pc_data.center_height_standing), stage.state.player_char.position) + + -- verify move callback behavior + test.action_sequence[3].callback() + assert.are_equal(vector(-1, 0), stage.state.player_char.move_intention) + + -- we have not passed time so the character cannot have reached expected position + local expected_message = "Passed gameplay value 'player character bottom position':\nvector(12, 45)\nExpected:\nvector(10, 45)\n" + assert.are_same({false, expected_message}, {test.final_assertion()}) + + -- but if we cheat and warp him on the spot, final assertion will work + stage.state.player_char:set_bottom_center(vector(10, 45)) + assert.are_same({true, ""}, {test.final_assertion()}) + + -- verify that parser is cleaned up, ready for next parsing + assert.are_same({ + nil, + nil, + {} + }, + { + itest_dsl_parser._itest, + itest_dsl_parser._last_time_trigger, + itest_dsl_parser._final_expectations + }) + end) - -- verify setup callback behavior - test.setup() - assert.are_equal(gamestate.types.stage, flow.curr_state.type) + describe('(spying tilemap load)', function () - -- verify warp callback behavior - test.action_sequence[1].callback() - assert.is_not_nil(stage.state.player_char) - assert.are_equal(vector(12, 45 - pc_data.center_height_standing), stage.state.player_char.position) + setup(function () + spy.on(tilemap, "load") + end) - -- verify move callback behavior - test.action_sequence[3].callback() - assert.are_equal(vector(-1, 0), stage.state.player_char.move_intention) + teardown(function () + tilemap.load:revert() + end) - -- we have not passed time so the character cannot have reached expected position - assert.is_false(test.final_assertion()) + it('setup should call setup_map_data and load on the tilemap if custom stage definition', function () + local dsli = dsl_itest() + dsli.gamestate_type = 'stage' + dsli.stage_name = "#" + dsli.tilemap = tilemap({}) + dsli.commands = {} - -- but if we cheat and warp him on the spot, final assertion will work - stage.state.player_char:set_bottom_center(vector(10, 45)) - assert.is_true(test.final_assertion()) - end) + local test = itest_dsl_parser.create_itest("test 1", dsli) - describe('(spying tilemap load)', function () + gameapp.init(test.active_gamestates) + test.setup() - setup(function () - spy.on(tilemap, "load") - end) + -- interface + assert.are_equal(control_modes.puppet, stage.state.player_char.control_mode) - teardown(function () - tilemap.load:revert() - end) + -- implementation + assert.spy(setup_map_data).was_called(1) + assert.spy(setup_map_data).was_called_with() + assert.spy(tilemap.load).was_called(1) + assert.spy(tilemap.load).was_called_with(match.ref(dsli.tilemap)) + end) - it('setup should call setup_map_data and load on the tilemap if custom stage definition', function () - local dsli = dsl_itest() - dsli.gamestate_type = 'stage' - dsli.stage_name = "#" - dsli.tilemap = tilemap({}) - dsli.commands = {} + it('teardown should call clear_map and teardown_map_data if custom stage definition', function () + local dsli = dsl_itest() + dsli.gamestate_type = 'stage' + dsli.stage_name = "#" + dsli.tilemap = tilemap({}) + dsli.commands = {} - local test = itest_dsl.create_itest("test 1", dsli) + local test = itest_dsl_parser.create_itest("test 1", dsli) - gameapp.init(test.active_gamestates) - test.setup() + gameapp.init(test.active_gamestates) + test.teardown() - -- interface - assert.are_equal(control_modes.puppet, stage.state.player_char.control_mode) + -- implementation + assert.spy(teardown_map_data).was_called(1) + assert.spy(teardown_map_data).was_called_with() + assert.spy(tilemap.load).was_called(1) + assert.spy(tilemap.load).was_called_with(match.ref(dsli.tilemap)) + end) - -- implementation - assert.spy(setup_map_data).was_called(1) - assert.spy(setup_map_data).was_called_with() - assert.spy(tilemap.load).was_called(1) - assert.spy(tilemap.load).was_called_with(match.ref(dsli.tilemap)) end) - it('teardown should call clear_map and teardown_map_data if custom stage definition', function () - local dsli = dsl_itest() - dsli.gamestate_type = 'stage' - dsli.stage_name = "#" - dsli.tilemap = tilemap({}) - dsli.commands = {} - - local test = itest_dsl.create_itest("test 1", dsli) + end) - gameapp.init(test.active_gamestates) - test.teardown() + describe('_evaluate', function () - -- implementation - assert.spy(teardown_map_data).was_called(1) - assert.spy(teardown_map_data).was_called_with() - assert.spy(tilemap.load).was_called(1) - assert.spy(tilemap.load).was_called_with(match.ref(dsli.tilemap)) + it('should assert if an unknown gameplay value type is passed', function () + assert.has_error(function () + itest_dsl_parser._evaluate(-1) + end, "unknown gameplay value: -1") end) end) - end) + describe('_add_final_expectation', function () - describe('_evaluate', function () + before_each(function () + itest_dsl_parser._itest = integration_test('test', {}) + end) + + it('should add to the final expectation an expectation with gameplay value type and expected value', function () + itest_dsl_parser:_add_final_expectation(itest_dsl_gp_value_types.pc_bottom_pos, 27) + local message = "Passed gameplay value 'player character bottom position':\n27\nExpected:\n27" + assert.are_equal(1, #itest_dsl_parser._final_expectations) + assert.are_equal(expectation(itest_dsl_gp_value_types.pc_bottom_pos, 27), itest_dsl_parser._final_expectations[1]) + end) - it('should assert if an unknown gameplay value type is passed', function () - assert.has_error(function () - itest_dsl._evaluate(-1) - end, "unknown gameplay value: -1") end) - end) + describe('_define_final_assertion', function () - describe('_final_assert', function () + setup(function () + -- mock _evaluate (we won't care about the 1st argument thx to this) + stub(itest_dsl_parser, "_evaluate", function (gameplay_value_type) + return vector(27, 30) + end) + end) - setup(function () - -- mock _evaluate (we won't care about the 1st argument thx to this) - stub(itest_dsl, "_evaluate", function (gameplay_value_type) - return 27 + teardown(function () + itest_dsl_parser._evaluate:revert() end) - end) - teardown(function () - itest_dsl._evaluate:revert() - end) + before_each(function () + itest_dsl_parser._itest = integration_test('test', {}) + end) - it('should set the final assertion as returning true, message when the gameplay value is expected', function () - itest_dsl._itest = integration_test('test', {}) - itest_dsl:_final_assert(itest_dsl_gp_value_types.pc_bottom_pos, 27) - local message = "Passed gameplay value 'player character bottom position':\n27\nExpected:\n27" - assert.are_same({true, message}, {itest_dsl._itest.final_assertion()}) - end) + it('should set the final assertion as returning true, message when the gameplay value is expected', function () + itest_dsl_parser._final_expectations = { + expectation(itest_dsl_gp_value_types.pc_bottom_pos, vector(27, 30)) + } + itest_dsl_parser:_define_final_assertion() + assert.are_same({true, ""}, {itest_dsl_parser._itest.final_assertion()}) + end) - it('should set the final assertion as returning false, message when the gameplay value is not expected', function () - itest_dsl._itest = integration_test('test', {}) - itest_dsl:_final_assert(itest_dsl_gp_value_types.pc_bottom_pos, 28) - local message = "Passed gameplay value 'player character bottom position':\n27\nExpected:\n28" - assert.are_same({false, message}, {itest_dsl._itest.final_assertion()}) - end) + it('should set the final assertion as returning false, message when the gameplay value is not expected', function () + itest_dsl_parser._final_expectations = { + expectation(itest_dsl_gp_value_types.pc_bottom_pos, vector(28, 30)) + } + itest_dsl_parser:_define_final_assertion() + local expected_message = "Passed gameplay value 'player character bottom position':\nvector(27, 30)\nExpected:\nvector(28, 30)\n" + assert.are_same({false, expected_message}, {itest_dsl_parser._itest.final_assertion()}) + end) - it('should assert when the passed gameplay value type is invalid', function () - itest_dsl._itest = integration_test('test', {}) - assert.has_error(function () - itest_dsl:_final_assert(-1, 20) + it('should assert when the passed gameplay value type is invalid', function () + itest_dsl_parser._itest = integration_test('test', {}) + assert.has_error(function () + itest_dsl_parser._itest.final_assertion() + end) end) + end) - end) + describe('_evaluate', function () - describe('_evaluate', function () + -- add gameplay value types tests here - -- add gameplay value types tests here + it('should return the player character bottom position for ', function () + -- simulate stage state on_enter by just creating pc + stage.state.player_char = player_char() + stage.state.player_char:spawn_bottom_at(vector(2, 8)) - it('should return the player character bottom position for ', function () - -- simulate stage state on_enter by just creating pc - stage.state.player_char = player_char() - stage.state.player_char:spawn_bottom_at(vector(2, 8)) + assert.are_equal(vector(2, 8), itest_dsl_parser._evaluate(itest_dsl_gp_value_types.pc_bottom_pos)) + end) - assert.are_equal(vector(2, 8), itest_dsl._evaluate(itest_dsl_gp_value_types.pc_bottom_pos)) - end) + it('should assert if an unknown gameplay value type is passed', function () + assert.has_error(function () + itest_dsl_parser._evaluate(-1) + end, "unknown gameplay value: -1") + end) - it('should assert if an unknown gameplay value type is passed', function () - assert.has_error(function () - itest_dsl._evaluate(-1) - end, "unknown gameplay value: -1") end) end) diff --git a/tests/uteststage.lua b/tests/uteststage.lua index eccaaf98..d0cf2676 100644 --- a/tests/uteststage.lua +++ b/tests/uteststage.lua @@ -318,7 +318,8 @@ describe('stage', function () state:init() end) - it('. init', function () + -- bugifx history: . + it('init', function () assert.are_same({ gamestate.types.stage, {}, From 23845bc5423670b3dff0acdba61578f1892ffb35 Mon Sep 17 00:00:00 2001 From: hsandt Date: Sat, 29 Dec 2018 11:59:18 +0100 Subject: [PATCH 015/140] [TEST] DSL: Added support for pc_velocity --- prebuild/replace_strings.py | 6 +++-- src/engine/test/itest_dsl.lua | 26 +++++++++++++++------ tests/utestitest_dsl.lua | 43 +++++++++++++++++++++++++++-------- 3 files changed, 56 insertions(+), 19 deletions(-) diff --git a/prebuild/replace_strings.py b/prebuild/replace_strings.py index 26dd5457..62001af2 100755 --- a/prebuild/replace_strings.py +++ b/prebuild/replace_strings.py @@ -99,12 +99,14 @@ 'motion_states.airborne': 2, # itest - 'itest_dsl_command_types.spawn': 1, + 'itest_dsl_command_types.warp': 1, 'itest_dsl_command_types.move': 2, 'itest_dsl_command_types.wait': 11, 'itest_dsl_command_types.expect': 21, - 'itest_dsl_value_types.pc_pos': 1, + 'itest_dsl_gp_value_types.pc_bottom_pos': 1, + 'itest_dsl_gp_value_types.pc_velocity': 11, + 'itest_dsl_gp_value_types.pc_ground_spd': 12, } # prefix of all arg identifiers diff --git a/src/engine/test/itest_dsl.lua b/src/engine/test/itest_dsl.lua index 2c618081..c805f0f4 100644 --- a/src/engine/test/itest_dsl.lua +++ b/src/engine/test/itest_dsl.lua @@ -17,20 +17,24 @@ local itest_dsl = {} -- type of commands available itest_dsl_command_types = { - warp = 1, -- warp player character bottom args: {bottom position: vector} - move = 2, -- set sticky pc move intention args: {move_dir: horizontal_dirs} + warp = 1, -- warp player character bottom args: {bottom position: vector} + move = 2, -- set sticky pc move intention args: {move_dir: horizontal_dirs} wait = 11, -- - expect = 21 + expect = 21, } -- type of gameplay values available for expectations itest_dsl_gp_value_types = { - pc_bottom_pos = 1 + pc_bottom_pos = 1, + pc_velocity = 11, + pc_ground_spd = 12, } --- string mapping for itest messages +-- string mapping for itest messages (to debug failing itests) local value_type_strings = { - "player character bottom position" + [1] = "player character bottom position", + [11] = "player character velocity", + [12] = "player character ground speed", } @@ -268,6 +272,12 @@ function itest_dsl_parser._parse_value_pc_bottom_pos(args) return vector(tonum(args[1]), tonum(args[2])) end +-- convert string args to vector +function itest_dsl_parser._parse_value_pc_velocity(args) + assert(#args == 2, "got "..#args.." args") + return vector(tonum(args[1]), tonum(args[2])) +end + -- create and return an itest from a dsli, providing a name function itest_dsl_parser.create_itest(name, dsli) itest_dsl_parser._itest = integration_test(name, {dsli.gamestate_type}) @@ -380,7 +390,7 @@ function itest_dsl_parser:_define_final_assertion() if gp_value ~= exp.expected_value then success = false local gp_value_name = value_type_strings[exp.gp_value_type] - assert(gp_value_name, "invalid exp.gp_value_type: "..exp.gp_value_type) + assert(gp_value_name, "value_type_strings["..exp.gp_value_type.."] is not defined") local message = "Passed gameplay value '"..gp_value_name.."':\n".. gp_value.."\n".. "Expected:\n".. @@ -398,6 +408,8 @@ end function itest_dsl_parser._evaluate(gp_value_type) if gp_value_type == itest_dsl_gp_value_types.pc_bottom_pos then return stage.state.player_char:get_bottom_center() + elseif gp_value_type == itest_dsl_gp_value_types.pc_velocity then + return stage.state.player_char.velocity else assert(false, "unknown gameplay value: "..gp_value_type) end diff --git a/tests/utestitest_dsl.lua b/tests/utestitest_dsl.lua index 7734ba1b..d4e20b75 100644 --- a/tests/utestitest_dsl.lua +++ b/tests/utestitest_dsl.lua @@ -132,6 +132,7 @@ wait 1 move left wait 2 expect pc_bottom_pos 10 45 +expect pc_velocity -2 3.5 ]] local dsli = itest_dsl_parser.parse(dsli_source) @@ -151,6 +152,7 @@ expect pc_bottom_pos 10 45 command(itest_dsl_command_types.move, { horizontal_dirs.left } ), command(itest_dsl_command_types.wait, { 2 } ), command(itest_dsl_command_types.expect, {itest_dsl_gp_value_types.pc_bottom_pos, vector(10, 45)}), + command(itest_dsl_command_types.expect, {itest_dsl_gp_value_types.pc_velocity, vector(-2, 3.5)}), } }, { @@ -340,7 +342,8 @@ expect pc_bottom_pos 10 45 "wait 1", "move left", "wait 2", - "expect pc_bottom_pos 10 45" + "expect pc_bottom_pos 10 45", + "expect pc_velocity 2 -3.5" } local commands = itest_dsl_parser.parse_action_sequence(dsli_lines, 5) assert.are_same( @@ -350,6 +353,7 @@ expect pc_bottom_pos 10 45 command(itest_dsl_command_types.move, { horizontal_dirs.left } ), command(itest_dsl_command_types.wait, { 2 } ), command(itest_dsl_command_types.expect, {itest_dsl_gp_value_types.pc_bottom_pos, vector(10, 45)}), + command(itest_dsl_command_types.expect, {itest_dsl_gp_value_types.pc_velocity, vector(2, -3.5)}), }, commands) end) @@ -370,6 +374,7 @@ expect pc_bottom_pos 10 45 command(itest_dsl_command_types.move, { horizontal_dirs.left } ), command(itest_dsl_command_types.wait, { 2 } ), command(itest_dsl_command_types.expect, {itest_dsl_gp_value_types.pc_bottom_pos, vector(10, 45)}), + command(itest_dsl_command_types.expect, {itest_dsl_gp_value_types.pc_velocity, vector(2, -3.5)}), } local test = itest_dsl_parser.create_itest("test 1", dsli) @@ -419,11 +424,13 @@ expect pc_bottom_pos 10 45 assert.are_equal(vector(-1, 0), stage.state.player_char.move_intention) -- we have not passed time so the character cannot have reached expected position - local expected_message = "Passed gameplay value 'player character bottom position':\nvector(12, 45)\nExpected:\nvector(10, 45)\n" + local expected_message = "Passed gameplay value 'player character bottom position':\nvector(12, 45)\nExpected:\nvector(10, 45)\n".. + "Passed gameplay value 'player character velocity':\nvector(0, 0)\nExpected:\nvector(2, -3.5)\n" assert.are_same({false, expected_message}, {test.final_assertion()}) -- but if we cheat and warp him on the spot, final assertion will work stage.state.player_char:set_bottom_center(vector(10, 45)) + stage.state.player_char.velocity = vector(2, -3.5) assert.are_same({true, ""}, {test.final_assertion()}) -- verify that parser is cleaned up, ready for next parsing @@ -511,10 +518,20 @@ expect pc_bottom_pos 10 45 end) it('should add to the final expectation an expectation with gameplay value type and expected value', function () - itest_dsl_parser:_add_final_expectation(itest_dsl_gp_value_types.pc_bottom_pos, 27) - local message = "Passed gameplay value 'player character bottom position':\n27\nExpected:\n27" + itest_dsl_parser:_add_final_expectation(itest_dsl_gp_value_types.pc_bottom_pos, vector(27, 30)) assert.are_equal(1, #itest_dsl_parser._final_expectations) - assert.are_equal(expectation(itest_dsl_gp_value_types.pc_bottom_pos, 27), itest_dsl_parser._final_expectations[1]) + assert.are_equal(expectation(itest_dsl_gp_value_types.pc_bottom_pos, vector(27, 30)), itest_dsl_parser._final_expectations[1]) + end) + + it('should add to the final expectation an expectation with gameplay value type and expected value', function () + itest_dsl_parser._final_expectations = { + expectation(itest_dsl_gp_value_types.pc_bottom_pos, vector(27, 30)) + } + + itest_dsl_parser:_add_final_expectation(itest_dsl_gp_value_types.pc_velocity, vector(-5, 3)) + + assert.are_equal(2, #itest_dsl_parser._final_expectations) + assert.are_equal(expectation(itest_dsl_gp_value_types.pc_velocity, vector(-5, 3)), itest_dsl_parser._final_expectations[2]) end) end) @@ -523,8 +540,12 @@ expect pc_bottom_pos 10 45 setup(function () -- mock _evaluate (we won't care about the 1st argument thx to this) - stub(itest_dsl_parser, "_evaluate", function (gameplay_value_type) - return vector(27, 30) + stub(itest_dsl_parser, "_evaluate", function (gp_value_type) + if gp_value_type == itest_dsl_gp_value_types.pc_bottom_pos then + return vector(27, 30) + else + return vector(-3, 2.5) + end end) end) @@ -538,7 +559,8 @@ expect pc_bottom_pos 10 45 it('should set the final assertion as returning true, message when the gameplay value is expected', function () itest_dsl_parser._final_expectations = { - expectation(itest_dsl_gp_value_types.pc_bottom_pos, vector(27, 30)) + expectation(itest_dsl_gp_value_types.pc_bottom_pos, vector(27, 30)), + expectation(itest_dsl_gp_value_types.pc_velocity, vector(-3, 2.5)) } itest_dsl_parser:_define_final_assertion() assert.are_same({true, ""}, {itest_dsl_parser._itest.final_assertion()}) @@ -546,10 +568,11 @@ expect pc_bottom_pos 10 45 it('should set the final assertion as returning false, message when the gameplay value is not expected', function () itest_dsl_parser._final_expectations = { - expectation(itest_dsl_gp_value_types.pc_bottom_pos, vector(28, 30)) + expectation(itest_dsl_gp_value_types.pc_bottom_pos, vector(27, 30)), -- ok + expectation(itest_dsl_gp_value_types.pc_velocity, vector(-3, 7.5)) -- different from actual } itest_dsl_parser:_define_final_assertion() - local expected_message = "Passed gameplay value 'player character bottom position':\nvector(27, 30)\nExpected:\nvector(28, 30)\n" + local expected_message = "Passed gameplay value 'player character velocity':\nvector(-3, 2.5)\nExpected:\nvector(-3, 7.5)\n" assert.are_same({false, expected_message}, {itest_dsl_parser._itest.final_assertion()}) end) From 35834e9ef302a6e5681434dfe987beda6c9963f7 Mon Sep 17 00:00:00 2001 From: hsandt Date: Sat, 29 Dec 2018 12:44:16 +0100 Subject: [PATCH 016/140] [TEST] DSL: Remove test redundancies by mocking intermediate functions --- tests/utestitest_dsl.lua | 133 ++++++++++++++++++++++++--------------- 1 file changed, 82 insertions(+), 51 deletions(-) diff --git a/tests/utestitest_dsl.lua b/tests/utestitest_dsl.lua index d4e20b75..e5daae4c 100644 --- a/tests/utestitest_dsl.lua +++ b/tests/utestitest_dsl.lua @@ -1,5 +1,6 @@ require("bustedhelper") -require("math") +require("engine/core/helper") +require("engine/core/math") local itest_dsl = require("engine/test/itest_dsl") local dsl_itest, command, expectation, itest_dsl_parser = itest_dsl.dsl_itest, itest_dsl.command, itest_dsl.expectation, itest_dsl.itest_dsl_parser local integrationtest = require("engine/test/integrationtest") @@ -120,20 +121,42 @@ describe('itest_dsl', function () describe('parse', function () + setup(function () + stub(itest_dsl_parser, "parse_gamestate_definition", function (lines) + local tile_id = tonum(lines[3]) + return lines[1], + lines[2], + tilemap({ + { 0, tile_id}, + {tile_id, 0} + }), + 5 + end) + stub(itest_dsl_parser, "parse_action_sequence", function (lines, next_line_index) + return { + command(itest_dsl_command_types[lines[next_line_index]], { vector(1, 2) } ), + command(itest_dsl_command_types[lines[next_line_index+1]], {itest_dsl_gp_value_types.pc_bottom_pos, vector(3, 4)}) + } + end) + end) + + teardown(function () + itest_dsl_parser.parse_gamestate_definition:revert() + itest_dsl_parser.parse_action_sequence:revert() + end) + -- bugfix history: -- + spot tilemap not being set, although parse_gamestate_definition worked, so the error is in the glue code it('should parse the itest source written in domain-specific language into a dsl itest', function () - local dsli_source = [[@stage # -..## -##.. - -warp 12 45 -wait 1 -move left -wait 2 -expect pc_bottom_pos 10 45 -expect pc_velocity -2 3.5 + local dsli_source = [[ +stage +# +64 + +warp +expect ]] + local dsli = itest_dsl_parser.parse(dsli_source) -- interface @@ -143,16 +166,12 @@ expect pc_velocity -2 3.5 'stage', '#', tilemap({ - { 0, 0, 64, 64}, - {64, 64, 0, 0} + { 0, 64}, + {64, 0} }), { - command(itest_dsl_command_types.warp, { vector(12, 45) } ), - command(itest_dsl_command_types.wait, { 1 } ), - command(itest_dsl_command_types.move, { horizontal_dirs.left } ), - command(itest_dsl_command_types.wait, { 2 } ), - command(itest_dsl_command_types.expect, {itest_dsl_gp_value_types.pc_bottom_pos, vector(10, 45)}), - command(itest_dsl_command_types.expect, {itest_dsl_gp_value_types.pc_velocity, vector(-2, 3.5)}), + command(itest_dsl_command_types.warp, { vector(1, 2) } ), + command(itest_dsl_command_types.expect, {itest_dsl_gp_value_types.pc_bottom_pos, vector(3, 4)}) } }, { @@ -161,10 +180,6 @@ expect pc_velocity -2 3.5 dsli.tilemap, dsli.commands }) - - -- implementation - -- todo: check call to parse_gamestate_definition and parse_action_sequence - -- to avoid test redundancy end) end) @@ -213,33 +228,49 @@ expect pc_velocity -2 3.5 }) end) - it('should return \'stage\', \'#\', tilemap data and 6 for a custom stage definition finishing at line 5 (including blank line)', function () - local dsli_lines = { - "@stage #", - "....", - "##..", - "..##", - "", - "???" - } - local gamestate_type, stage_name, tm, next_line_index = itest_dsl_parser.parse_gamestate_definition(dsli_lines) - assert.are_same( - { - 'stage', - '#', - tilemap({ - { 0, 0, 0, 0}, - {64, 64, 0, 0}, - { 0, 0, 64, 64} - }), - 6 - }, - { - gamestate_type, - stage_name, - tm, - next_line_index - }) + describe('(mocking parse_tilemap)', function () + + setup(function () + stub(itest_dsl_parser, "parse_tilemap", function () + return tilemap({ + {70, 64}, + {64, 70} + }), 5 + end) + end) + + teardown(function () + itest_dsl_parser.parse_tilemap:revert() + end) + + it('should return \'stage\', \'#\', tilemap data and 6 for a custom stage definition finishing at line 5 (including blank line)', function () + local dsli_lines = { + "@stage #", + "[this part is ignored, mocked parse_tilemap]", + "[will return predefined tilemap]" + } + + local gamestate_type, stage_name, tm, next_line_index = itest_dsl_parser.parse_gamestate_definition(dsli_lines) + + -- interface + assert.are_same( + { + 'stage', + '#', + tilemap({ + {70, 64}, + {64, 70} + }), + 5 + }, + { + gamestate_type, + stage_name, + tm, + next_line_index + }) + end) + end) end) @@ -433,7 +464,7 @@ expect pc_velocity -2 3.5 stage.state.player_char.velocity = vector(2, -3.5) assert.are_same({true, ""}, {test.final_assertion()}) - -- verify that parser is cleaned up, ready for next parsing + -- verify that parser state is cleaned up, ready for next parsing assert.are_same({ nil, nil, From a74143e9792628d29fefbd0ad196272fc4a8c16e Mon Sep 17 00:00:00 2001 From: hsandt Date: Sat, 5 Jan 2019 23:59:52 +0000 Subject: [PATCH 017/140] [HELPER] Defined transform, get_members and invert_table --- src/engine/core/helper.lua | 35 +++++++++++++++++++++++++++++++++++ tests/utesthelper.lua | 30 ++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/src/engine/core/helper.lua b/src/engine/core/helper.lua index f38e3f1b..a87c434f 100644 --- a/src/engine/core/helper.lua +++ b/src/engine/core/helper.lua @@ -1,6 +1,28 @@ require("engine/application/constants") +-- implementation of "map" in other languages (but "map" means something else in pico8) +function transform(t, func) + local transformed_t = {} + for value in all(t) do + add(transformed_t, func(value)) + end + return transformed_t +end + +-- return a sequence of module members from their names +-- use it after require("module") to define +-- local a, b = get_members(module, "a", "b") +-- for more simple access +function get_members(module, ...) + local member_names = {...} + return unpack(transform(member_names, + function(member_name) + return module[member_name] + end) + ) +end + -- return true if the table is empty (contrary to #t == 0, -- it also supports non-sequence tables) function is_empty(t) @@ -73,6 +95,18 @@ function unpack(t, from, to) return t[from], unpack(t, from+1, to) end +--#if assert +-- return a table reversing keys and values, assuming the original table is injective +-- this is "assert" only because we mostly need it to generate enum-to-string tables +function invert_table(t) + inverted_t = {} + for key, value in pairs(t) do + inverted_t[value] = key + end + return inverted_t +end +--#endif + --#if log function stringify(value) @@ -106,6 +140,7 @@ end function joinstr(separator, ...) return joinstr_table(separator, {...}) end + --#endif -- https://pastebin.com/NS8rxMwH diff --git a/tests/utesthelper.lua b/tests/utesthelper.lua index ffab060b..c2e0d314 100644 --- a/tests/utesthelper.lua +++ b/tests/utesthelper.lua @@ -2,6 +2,30 @@ require("bustedhelper") helper = require("engine/core/helper") math = require("engine/core/math") -- just to test stringify and are_same +describe('transform', function () + it('should return a table where an operation has been applied to each element of the original table', function () + local t = {-1, 2, 3} + local function double(x) + return 2 * x + end + assert.are_same({-2, 4, 6}, transform(t, double)) + end) +end) + +describe('get_members', function () + it('should return a sequence of module members from their names', function () + local module = { + a = 1, + b = 2, + [3] = function () end + } + assert.are_same({module.a, module.b, module[3]}, + {get_members(module, "a", "b", 3)}) + end) +end) + +-- local a, b = import_members(mymodule, "a", "b") -- v2 + describe('is_empty', function () it('return true if the table is empty', function () assert.is_true(is_empty({})) @@ -150,6 +174,12 @@ describe('unpack', function () end) end) +describe('invert_table', function () + it('should return a table with reversed keys and values', function () + assert.are_same({[41] = "a", foo = 1}, invert_table({a = 41, [1] = "foo"})) + end) +end) + describe('stringify', function () it('nil => "[nil]"', function () assert.are_equal("[nil]", stringify(nil)) From 2e9d4399b969d90fc26fbed70ce381c6ed59c74d Mon Sep 17 00:00:00 2001 From: hsandt Date: Sun, 6 Jan 2019 00:03:13 +0000 Subject: [PATCH 018/140] [PREBUILD] Factorize writing of SYMBOL_SUBSTITUTE_TABLE by namespace Explicitly encode files in utf-8 so script works independently of environment preferred encoding --- prebuild/replace_strings.py | 176 +++++++++++++++++++------------ prebuild/test_replace_strings.py | 27 +++-- 2 files changed, 129 insertions(+), 74 deletions(-) diff --git a/prebuild/replace_strings.py b/prebuild/replace_strings.py index 62001af2..627209be 100755 --- a/prebuild/replace_strings.py +++ b/prebuild/replace_strings.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3.6 # -*- coding: utf-8 -*- import argparse +import logging import os +import re # This script replace glyph identifiers, some functions and symbols in general, and arg substitutes ($arg) # with the corresponding unicode characters and substitute symbol names. @@ -31,82 +33,107 @@ # functions and enum constants to substitute # enums are only substituted for token/char limit reasons +# format: { namespace1: {name1: substitute1, name 2: substitute2, ...}, ... } SYMBOL_SUBSTITUTE_TABLE = { # Functions - # api.print is useful for tests using native print but in runtime, just use print - 'api.print': 'print', + # api.print is useful for tests using native print, but in runtime, just use print + 'api': { + 'print': 'print' + }, # Enums # for every enum added here, surround enum definition with --#ifn pico8 # to strip it from the build, unless you need to map the enum string - # to its value dynamically + # to its value dynamically with enum_values[dynamic_string] # remember to update the values of any preprocessed enum modified # color - 'colors.black': 0, - 'colors.dark_blue': 1, - 'colors.dark_purple': 2, - 'colors.dark_green': 3, - 'colors.brown': 4, - 'colors.dark_gray': 5, - 'colors.light_gray': 6, - 'colors.white': 7, - 'colors.red': 8, - 'colors.orange': 9, - 'colors.yellow': 10, - 'colors.green': 11, - 'colors.blue': 12, - 'colors.indigo': 13, - 'colors.pink': 14, - 'colors.peach': 15, + 'colors': { + 'black': 0, + 'dark_blue': 1, + 'dark_purple': 2, + 'dark_green': 3, + 'brown': 4, + 'dark_gray': 5, + 'light_gray': 6, + 'white': 7, + 'red': 8, + 'orange': 9, + 'yellow': 10, + 'green': 11, + 'blue': 12, + 'indigo': 13, + 'pink': 14, + 'peach': 15, + }, # math - 'directions.left': 0, - 'directions.right': 1, - 'directions.up': 2, - 'directions.down': 3, - - 'horizontal_dirs.left': 1, - 'horizontal_dirs.right': 2, + 'directions': { + 'left': 0, + 'right': 1, + 'up': 2, + 'down': 3, + }, + + 'horizontal_dirs': { + 'left': 1, + 'right': 2, + }, # input - 'button_ids.left': 0, - 'button_ids.right': 1, - 'button_ids.up': 2, - 'button_ids.down': 3, - 'button_ids.o': 4, - 'button_ids.x': 5, - - 'btn_states.released': 0, - 'btn_states.just_pressed': 1, - 'btn_states.pressed': 2, - 'btn_states.just_released': 3, - - 'input_modes.native': 0, - 'input_modes.simulated': 1, + 'button_ids': { + 'left': 0, + 'right': 1, + 'up': 2, + 'down': 3, + 'o': 4, + 'x': 5, + }, + + 'btn_states': { + 'released': 0, + 'just_pressed': 1, + 'pressed': 2, + 'just_released': 3, + }, + + 'input_modes': { + 'native': 0, + 'simulated': 1, + }, # playercharacter - 'control_modes.human': 1, - 'control_modes.ai': 2, - 'control_modes.puppet': 3, - - 'motion_modes.platformer': 1, - 'motion_modes.debug': 2, - - 'motion_states.grounded': 1, - 'motion_states.airborne': 2, - - # itest - 'itest_dsl_command_types.warp': 1, - 'itest_dsl_command_types.move': 2, - 'itest_dsl_command_types.wait': 11, - 'itest_dsl_command_types.expect': 21, - - 'itest_dsl_gp_value_types.pc_bottom_pos': 1, - 'itest_dsl_gp_value_types.pc_velocity': 11, - 'itest_dsl_gp_value_types.pc_ground_spd': 12, + 'control_modes': { + 'human': 1, + 'ai': 2, + 'puppet': 3, + }, + + 'motion_modes': { + 'platformer': 1, + 'debug': 2, + }, + + 'motion_states': { + 'grounded': 1, + 'airborne': 2, + }, + + # itest_dsl + 'itest_dsl_command_types': { + 'warp': 1, + 'move': 2, + 'wait': 11, + 'expect': 21, + }, + + 'itest_dsl_gp_value_types': { + 'pc_bottom_pos': 1, + 'pc_velocity': 11, + 'pc_ground_spd': 12, + }, } # prefix of all arg identifiers @@ -149,7 +176,10 @@ def replace_all_strings_in_file(filepath, arg_substitutes_table): print("press ❎") """ - with open(filepath, 'r+') as f: + # make sure to open files as utf-8 so we can handle glyphs on any platform + # (when locale.getpreferredencoding() and sys.getfilesystemencoding() are not "UTF-8" and "utf-8") + # you can also set PYTHONIOENCODING="UTF-8" to visualize glyphs when debugging if needed + with open(filepath, 'r+', encoding='utf-8') as f: data = f.read() data = replace_all_glyphs_in_string(data) data = replace_all_symbols_in_string(data) @@ -171,21 +201,31 @@ def replace_all_glyphs_in_string(text): text = text.replace(GLYPH_PREFIX + identifier_char, glyph) return text +def generate_get_substitute_from_dict(substitutes): + def get_substitute(match): + member = match.group(1) # "{member}" + if member in substitutes: + return str(substitutes[member]) # enums are substituted with integers, so convert + else: + original_symbol = match.group(0) # "{namespace}.{member}" + # in general, we should substitute all members of a namespace, especially enums + logging.warning(f'no substitute defined for {original_symbol}, but the namespace is present in SYMBOL_SUBSTITUTE_TABLE') + return original_symbol + return get_substitute + def replace_all_symbols_in_string(text): """ - Replace symbols with the corresponding substitutes + Replace symbols "namespace.member" defined in SYMBOL_SUBSTITUTE_TABLE + with the corresponding substitutes Convert integer to string for replacement to support enum constants >>> replace_all_symbols_in_string("api.print(\"hello\")") 'print("hello")' """ - for original_symbol, substitute_symbol in SYMBOL_SUBSTITUTE_TABLE.items(): - # enum constants are defined with integer substitutes for simplicity, - # so convert them to string first - if type(substitute_symbol) == int: - substitute_symbol = str(substitute_symbol) - text = text.replace(original_symbol, substitute_symbol) + for namespace, substitutes in SYMBOL_SUBSTITUTE_TABLE.items(): + SYMBOL_PATTERN = re.compile(rf"{namespace}\.(\w+)") + text = SYMBOL_PATTERN.sub(generate_get_substitute_from_dict(substitutes), text) return text @@ -226,6 +266,8 @@ def parse_arg_substitutes(arg_substitutes): help='extra substitutes table in the format "arg1=substitute1 arg2=substitute2 ...". \ Does not support spaces in names because surrounding quotes would be part of the names') args = parser.parse_args() + + logging.basicConfig(level=logging.INFO) arg_substitutes_table = parse_arg_substitutes(args.substitutes) replace_all_strings_in_dir(args.dirpath, arg_substitutes_table) print(f"Replaced all strings in all files in {args.dirpath} with substitutes: {arg_substitutes_table}.") diff --git a/prebuild/test_replace_strings.py b/prebuild/test_replace_strings.py index 112003cc..91df7e6f 100644 --- a/prebuild/test_replace_strings.py +++ b/prebuild/test_replace_strings.py @@ -1,6 +1,9 @@ +# -*- coding: utf-8 -*- + import unittest from . import replace_strings +import logging from os import path import shutil, tempfile @@ -22,10 +25,19 @@ def test_replace_all_glyphs_in_string(self): test_string = '##d and ##x ##d' self.assertEqual(replace_strings.replace_all_glyphs_in_string(test_string), '⬇️ and ❎ ⬇️') - def test_replace_all_symbols_in_string(self): + def test_replace_all_symbols_in_string_function(self): test_string = 'api.print("hello")' self.assertEqual(replace_strings.replace_all_symbols_in_string(test_string), 'print("hello")') + def test_replace_all_symbols_in_string_enum(self): + test_string = 'local c = colors.dark_purple' + self.assertEqual(replace_strings.replace_all_symbols_in_string(test_string), 'local c = 2') + + def test_replace_all_symbols_in_string_missing_member(self): + test_string = 'local c = colors.unknown' + # this will trigger a warning, hide by setting logging level to ERROR in main + self.assertEqual(replace_strings.replace_all_symbols_in_string(test_string), 'local c = colors.unknown') + def test_replace_all_args_in_string(self): test_string = 'require("itest_$itest")' self.assertEqual(replace_strings.replace_all_args_in_string(test_string, {'itest': 'character'}), 'require("itest_character")') @@ -52,10 +64,10 @@ def tearDown(self): def test_replace_strings(self): """^ Test replacing strings in a whole file, with substitutes being shorter or longer than original symbol to test if file is truncated""" test_filepath = path.join(self.test_dir, 'test.lua') - with open(test_filepath, 'w') as f: + with open(test_filepath, 'w', encoding='utf-8') as f: f.write('require("itest_$itest")\nrequire("$symbol_is_much_longer")\n##d or ##u\nand ##x\napi.print("press ##x")') replace_strings.replace_all_strings_in_file(test_filepath, {'itest': 'character', 'symbol_is_much_longer': 'short'}) - with open(test_filepath, 'r') as f: + with open(test_filepath, 'r', encoding='utf-8') as f: self.assertEqual(f.read(), 'require("itest_character")\nrequire("short")\n⬇️ or ⬆️\nand ❎\nprint("press ❎")') class TestReplaceStringsInDir(unittest.TestCase): @@ -70,16 +82,17 @@ def tearDown(self): def test_replace_all_strings_in_dir(self): test_filepath1 = path.join(self.test_dir, 'test1.lua') - with open(test_filepath1, 'w') as f: + with open(test_filepath1, 'w', encoding='utf-8') as f: f.write('require("itest_$itest")\n##d or ##u\nand ##x\napi.print("press ##x")') test_filepath2 = path.join(self.test_dir, 'test2.lua') - with open(test_filepath2, 'w') as f: + with open(test_filepath2, 'w', encoding='utf-8') as f: f.write('require("itest_$itest")\n##l or ##r\nand ##o\napi.print("press ##o")') replace_strings.replace_all_strings_in_dir(self.test_dir, {'itest': 'character'}) - with open(test_filepath1, 'r') as f: + with open(test_filepath1, 'r', encoding='utf-8') as f: self.assertEqual(f.read(), 'require("itest_character")\n⬇️ or ⬆️\nand ❎\nprint("press ❎")') - with open(test_filepath2, 'r') as f: + with open(test_filepath2, 'r', encoding='utf-8') as f: self.assertEqual(f.read(), 'require("itest_character")\n⬅️ or ➡️\nand 🅾️\nprint("press 🅾️")') if __name__ == '__main__': + logging.basicConfig(level=logging.ERROR) unittest.main() From a086e5bbe0d03933dc3c6fd911a0b1aa3b97097f Mon Sep 17 00:00:00 2001 From: hsandt Date: Sun, 6 Jan 2019 01:56:51 +0000 Subject: [PATCH 019/140] [TEST] DSL: Refactored commands and expectations to use generic parsers, executors and evaluators --- prebuild/replace_strings.py | 11 +- sonic-2d-tech-demo.sublime-project | 2 + src/engine/core/helper.lua | 2 +- src/engine/test/itest_dsl.lua | 286 ++++++++++++++++--------- tests/utesthelper.lua | 2 +- tests/utestitest_dsl.lua | 323 ++++++++++++++++++++++------- 6 files changed, 445 insertions(+), 181 deletions(-) diff --git a/prebuild/replace_strings.py b/prebuild/replace_strings.py index 627209be..3a33950f 100755 --- a/prebuild/replace_strings.py +++ b/prebuild/replace_strings.py @@ -122,14 +122,21 @@ }, # itest_dsl - 'itest_dsl_command_types': { + 'parsable_types': { + 'number': 1, + 'vector': 2, + 'horizontal_dir': 11, + 'expect': 21, + }, + + 'command_types': { 'warp': 1, 'move': 2, 'wait': 11, 'expect': 21, }, - 'itest_dsl_gp_value_types': { + 'gp_value_types': { 'pc_bottom_pos': 1, 'pc_velocity': 11, 'pc_ground_spd': 12, diff --git a/sonic-2d-tech-demo.sublime-project b/sonic-2d-tech-demo.sublime-project index 8b97d883..9f91701e 100644 --- a/sonic-2d-tech-demo.sublime-project +++ b/sonic-2d-tech-demo.sublime-project @@ -267,6 +267,8 @@ "cmd": ["pico8 -run data/data.p8"], "working_dir": "${project_path}" }, + // it's better to use our custom main in unittest scripts to get the correct log level + // for filtering, it's easier to use -m unittest, but it will show all the warnings { "name": "python: test preprocess (TestPreprocessLines)", "working_dir": "${project_path}", diff --git a/src/engine/core/helper.lua b/src/engine/core/helper.lua index a87c434f..64be5998 100644 --- a/src/engine/core/helper.lua +++ b/src/engine/core/helper.lua @@ -10,7 +10,7 @@ function transform(t, func) return transformed_t end --- return a sequence of module members from their names +-- return module members from their names as multiple values -- use it after require("module") to define -- local a, b = get_members(module, "a", "b") -- for more simple access diff --git a/src/engine/test/itest_dsl.lua b/src/engine/test/itest_dsl.lua index c805f0f4..e97f11ed 100644 --- a/src/engine/test/itest_dsl.lua +++ b/src/engine/test/itest_dsl.lua @@ -14,30 +14,168 @@ local pc_data = require("game/data/playercharacter_data") -- module local itest_dsl = {} +local value_parsers +local executors +local evaluators + +-- struct holding data on a gameplay value for expectations + +-- attributes +-- name string descriptive name of the gameplay value (to debug failing itests) +-- parsable_type parsable_type type of gameplay value (for expected args parsing) +local gameplay_value_data = new_struct() +itest_dsl.gameplay_value_data = gameplay_value_data + +function gameplay_value_data:_init(name, parsable_type, eval) + self.name = name + self.parsable_type = parsable_type +end + + +-- optimize tokens: if this is too much, remove proxy function tables +-- altogether and directly access functions via itest_dsl[prefix..type_name] +-- return table containing functions named {prefix}{enum_type_name} +-- inside a module, indexed by enum value +local function generate_function_table(module, enum_types, prefix) + local t = {} + for type_name, enum_type in pairs(enum_types) do + t[enum_type] = module[prefix..type_name] + end + return t +end +--#if utest +itest_dsl.generate_function_table = generate_function_table +--#endif + +-- type of variables that can be parsed +parsable_types = { + number = 1, + vector = 2, + horizontal_dir = 11, + expect = 21, -- meta-type meaning we must check the 1st arg (gp_value_type) to know what the rest should be +} + +--#if assert +parsable_type_strings = invert_table(parsable_types) +--#endif + -- type of commands available -itest_dsl_command_types = { - warp = 1, -- warp player character bottom args: {bottom position: vector} - move = 2, -- set sticky pc move intention args: {move_dir: horizontal_dirs} - wait = 11, -- - expect = 21, +command_types = { + warp = 1, -- warp player character bottom args: {bottom_position: vector} + move = 2, -- set sticky pc move intention args: {move_dir: horizontal_dirs} + wait = 11, -- wait some frames args: {frames: int} + expect = 21, -- expect a gameplay value args: {gp_value_type: gp_value_types, expected_args...: matching gp value parsable type} } +--#if assert +command_type_strings = invert_table(command_types) +--#endif + +-- argument types expected after those commands +command_arg_types = { + [command_types.warp] = parsable_types.vector, + [command_types.move] = parsable_types.horizontal_dir, + [command_types.wait] = parsable_types.number, + [command_types.expect] = parsable_types.expect, +} + + -- type of gameplay values available for expectations -itest_dsl_gp_value_types = { - pc_bottom_pos = 1, - pc_velocity = 11, - pc_ground_spd = 12, +gp_value_types = { + pc_bottom_pos = 1, -- bottom position of player character + pc_velocity = 11, -- velocity of player character + pc_ground_spd = 12, -- ground speed of player character } --- string mapping for itest messages (to debug failing itests) -local value_type_strings = { - [1] = "player character bottom position", - [11] = "player character velocity", - [12] = "player character ground speed", +--#if assert +gp_value_type_strings = invert_table(gp_value_types) +--#endif + +-- data for each gameplay value type +local gp_value_data_t = { + [gp_value_types.pc_bottom_pos] = gameplay_value_data("player character bottom position", parsable_types.vector), + [gp_value_types.pc_velocity] = gameplay_value_data("player character velocity", parsable_types.vector), + [gp_value_types.pc_ground_spd] = gameplay_value_data("player character ground speed", parsable_types.number), } +-- parsing functions + +function itest_dsl.parse_number(arg_strings) + assert(#arg_strings == 1, "parse_number: got "..#arg_strings.." args, expected 1") + return tonum(arg_strings[1]) +end + +function itest_dsl.parse_vector(arg_strings) + assert(#arg_strings == 2, "parse_vector: got "..#arg_strings.." args, expected 2") + return vector(tonum(arg_strings[1]), tonum(arg_strings[2])) +end + +function itest_dsl.parse_horizontal_dir(arg_strings) + assert(#arg_strings == 1, "parse_horizontal_dir: got "..#arg_strings.." args, expected 1") + return horizontal_dirs[arg_strings[1]] +end + +-- convert string args to vector +function itest_dsl.parse_expect(arg_strings) + assert(#arg_strings > 1, "parse_expect: got "..#arg_strings.." args, expected at least 2") + -- same principle as itest_dsl_parser.parse, the type of the first arg + -- determines how we parse the rest of the args, named "value components" + local gp_value_type_str = arg_strings[1] + -- gather all the value components as strings (e.g. {"3", "4"} for vector(3, 4)) + local expected_value_comps = {} + for i = 2, #arg_strings do + add(expected_value_comps, arg_strings[i]) + end + -- determine the type of value reference tested for comparison (e.g. pc position) + local gp_value_type = gp_value_types[gp_value_type_str] + -- parse the value components to semantical type (e.g. vector) + local gp_value_data = gp_value_data_t[gp_value_type] + local expected_value_parser = value_parsers[gp_value_data.parsable_type] + assert(expected_value_parser, "no value parser defined for gp value type '"..parsable_type_strings[gp_value_data.parsable_type].."'") + local expected_value = expected_value_parser(expected_value_comps) + + return gp_value_type, expected_value +end + +-- table of parsers for command args and gameplay values, indexed by parsed type +value_parsers = generate_function_table(itest_dsl, parsable_types, "parse_") +itest_dsl.value_parsers = value_parsers + + +-- functions to execute dsl commands. they take the dsl parser as 1st parameter +-- so they can update its state if needed + +function itest_dsl.execute_warp(args) + stage.state.player_char:warp_bottom_to(args[1]) +end + +function itest_dsl.execute_move(args) + stage.state.player_char.move_intention = horizontal_dir_vectors[args[1]] +end + +-- wait and expect are not timed actions and will be handled as special cases + +-- table of functions to call when applying a command with args, indexed by command type +executors = generate_function_table(itest_dsl, command_types, "execute_") +itest_dsl.executors = executors + + +-- gameplay value evaluation functions + +function itest_dsl.eval_pc_bottom_pos() + return stage.state.player_char:get_bottom_center() +end + +function itest_dsl.eval_pc_velocity() + return stage.state.player_char.velocity +end + +-- table of functions used to evaluate and returns the gameplay value in current game state +evaluators = generate_function_table(itest_dsl, gp_value_types, "eval_") +itest_dsl.evaluators = evaluators + -- command struct -- attributes @@ -55,7 +193,7 @@ end -- expectation struct -- attributes --- gp_value_type itest_dsl_gp_value_types type of gameplay value to compare +-- gp_value_type gp_value_types type of gameplay value to compare -- expected_value {type used for gp_value_type} expected gameplay value local expectation = new_struct() itest_dsl.expectation = expectation @@ -77,7 +215,7 @@ local dsl_itest = new_struct() itest_dsl.dsl_itest = dsl_itest function dsl_itest:_init() - -- all attributes are initially nil or empty + -- all attributes are initially nil (even commands, as we construct the table during parsing) end @@ -110,7 +248,7 @@ end -- ... < for custom stage, provide the tilemap in ascii -- ### < . for empty tile, # for full tile, etc. -- < blank after tilemap to mark the end --- warp 4 8 < initial setup (it's an action like any other) +-- warp 4 8 < initial setup (it's an action like any other) -- move right < more actions... -- wait 30 < wait delays the next action (here, the nil action) -- expect pc_bottom_pos 14. 8. < expectation (only final assertion is supported) @@ -207,6 +345,7 @@ function itest_dsl_parser.parse_tilemap(lines) end +-- return a sequence of commands read in lines, starting at next_line_index function itest_dsl_parser.parse_action_sequence(lines, next_line_index) local commands = {} for i = next_line_index, #lines do @@ -218,65 +357,19 @@ function itest_dsl_parser.parse_action_sequence(lines, next_line_index) for j = 2, #words do add(arg_strings, words[j]) end - local cmd_type = itest_dsl_command_types[cmd_type_str] - local parse_fn_name = '_parse_args_'..cmd_type_str - assert(itest_dsl_parser[parse_fn_name], "parse function '"..parse_fn_name.."' is not defined") - local args = {itest_dsl_parser[parse_fn_name](arg_strings)} + local cmd_type = command_types[cmd_type_str] + assert(cmd_type, "no command type named '"..cmd_type_str.."'") + local arg_parsable_type = command_arg_types[cmd_type] + assert(arg_parsable_type, "no command arg type defined for command '"..command_type_strings[cmd_type].."'") + local arg_parser = value_parsers[arg_parsable_type] + assert(arg_parser, "no value parser defined for arg type '"..parsable_type_strings[arg_parsable_type].."'") + local args = {arg_parser(arg_strings)} add(commands, command(cmd_type, args)) end end return commands end --- convert string args to vector -function itest_dsl_parser._parse_args_warp(args) - assert(#args == 2, "got "..#args.." args") - return vector(tonum(args[1]), tonum(args[2])) -- bottom position -end - --- convert string args to vector -function itest_dsl_parser._parse_args_move(args) - assert(#args == 1, "got "..#args.." args") - return horizontal_dirs[args[1]] -- move intention -end - --- convert string args to vector -function itest_dsl_parser._parse_args_wait(args) - assert(#args == 1, "got "..#args.." args") - return tonum(args[1]) -- frames to wait -end - --- convert string args to vector -function itest_dsl_parser._parse_args_expect(args) - assert(#args > 1, "got "..#args.." args") - -- same principle as itest_dsl_parser.parse, the type of the first arg - -- determines how we parse the rest of the args, named "value components" - local value_type_str = args[1] - -- gather all the value components as strings (e.g. {"3", "4"} for vector(3, 4)) - local expected_value_comps = {} - for i = 2, #args do - add(expected_value_comps, args[i]) - end - -- determine the type of value reference tested for comparison (e.g. pc position) - local value_type = itest_dsl_gp_value_types[value_type_str] - -- parse the value components to semantical type (e.g. vector) - local parse_fn_name = '_parse_value_'..value_type_str - assert(itest_dsl_parser[parse_fn_name], "parse function '"..parse_fn_name.."' is not defined") - local expected_value = itest_dsl_parser[parse_fn_name](expected_value_comps) - return value_type, expected_value -end - --- convert string args to vector -function itest_dsl_parser._parse_value_pc_bottom_pos(args) - assert(#args == 2, "got "..#args.." args") - return vector(tonum(args[1]), tonum(args[2])) -end - --- convert string args to vector -function itest_dsl_parser._parse_value_pc_velocity(args) - assert(#args == 2, "got "..#args.." args") - return vector(tonum(args[1]), tonum(args[2])) -end -- create and return an itest from a dsli, providing a name function itest_dsl_parser.create_itest(name, dsli) @@ -309,23 +402,22 @@ function itest_dsl_parser.create_itest(name, dsli) end for cmd in all(dsli.commands) do - if cmd.type == itest_dsl_command_types.warp then - itest_dsl_parser:_act(function () - stage.state.player_char:warp_bottom_to(vector(cmd.args[1].x, cmd.args[1].y)) - end) - elseif cmd.type == itest_dsl_command_types.move then + if cmd.type == command_types.wait then + itest_dsl_parser:_wait(cmd.args[1]) + + elseif cmd.type == command_types.expect then + -- we currently don't support live assertions, but we support multiple + -- final expectations + add(itest_dsl_parser._final_expectations, expectation(cmd.args[1], cmd.args[2])) + + else + -- common action, store callback for execution during itest_dsl_parser:_act(function () - stage.state.player_char.move_intention = horizontal_dir_vectors[cmd.args[1]] + executors[cmd.type](cmd.args) end) - elseif cmd.type == itest_dsl_command_types.wait then - itest_dsl_parser:_wait(cmd.args[1]) - elseif cmd.type == itest_dsl_command_types.expect then - -- we currently don't support live assertions, only final assertion - itest_dsl_parser:_add_final_expectation(unpack(cmd.args)) end end - -- if we finished with a wait (with or without final assertion), -- we need to close the itest with a wait-action if itest_dsl_parser._last_time_trigger then @@ -342,6 +434,14 @@ function itest_dsl_parser.create_itest(name, dsli) return test end +-- glue code for old callback-based system +-- the time trigger system makes actions and waiting asymmetrical, +-- as waiting is not an action but adds a parameter to the next action, +-- and requires nil actions to chain waiting (they don't even merge) +-- prefer a flat sequence of generic actions that can be actual gameplay +-- changes or waiting. when waiting, just skip frames until waiting ends, +-- at which point you can apply all further actions immediately, until +-- a new wait action is found. function itest_dsl_parser:_act(callback) if self._last_time_trigger then self._itest:add_action(self._last_time_trigger, callback) @@ -361,10 +461,6 @@ function itest_dsl_parser:_wait(interval) self._last_time_trigger = integrationtest.time_trigger(interval, true) end --- add final expectation to sequence, for future evaluation -function itest_dsl_parser:_add_final_expectation(gp_value_type, expected_gp_value) - add(self._final_expectations, expectation(gp_value_type, expected_gp_value)) -end -- define final assertion based on sequence of final expectations -- this is a glue method to make it retro-compatible with the function-based final assertion @@ -386,11 +482,12 @@ function itest_dsl_parser:_define_final_assertion() -- check each expectation one by one for exp in all(final_expectations_proxy) do - local gp_value = self._evaluate(exp.gp_value_type) + local gp_value = evaluators[exp.gp_value_type]() if gp_value ~= exp.expected_value then success = false - local gp_value_name = value_type_strings[exp.gp_value_type] - assert(gp_value_name, "value_type_strings["..exp.gp_value_type.."] is not defined") + local gp_value_data = gp_value_data_t[exp.gp_value_type] + assert(gp_value_data, "gp_value_data_t["..exp.gp_value_type.."] is not defined") + local gp_value_name = gp_value_data.name local message = "Passed gameplay value '"..gp_value_name.."':\n".. gp_value.."\n".. "Expected:\n".. @@ -403,16 +500,5 @@ function itest_dsl_parser:_define_final_assertion() end end --- evaluate gameplay value. it is important to call this at expect --- time, not when defining the test, to get the actual runtime value -function itest_dsl_parser._evaluate(gp_value_type) - if gp_value_type == itest_dsl_gp_value_types.pc_bottom_pos then - return stage.state.player_char:get_bottom_center() - elseif gp_value_type == itest_dsl_gp_value_types.pc_velocity then - return stage.state.player_char.velocity - else - assert(false, "unknown gameplay value: "..gp_value_type) - end -end return itest_dsl diff --git a/tests/utesthelper.lua b/tests/utesthelper.lua index c2e0d314..c1dd20f7 100644 --- a/tests/utesthelper.lua +++ b/tests/utesthelper.lua @@ -13,7 +13,7 @@ describe('transform', function () end) describe('get_members', function () - it('should return a sequence of module members from their names', function () + it('should return module members from their names as multiple values', function () local module = { a = 1, b = 2, diff --git a/tests/utestitest_dsl.lua b/tests/utestitest_dsl.lua index e5daae4c..f07f27cb 100644 --- a/tests/utestitest_dsl.lua +++ b/tests/utestitest_dsl.lua @@ -2,9 +2,14 @@ require("bustedhelper") require("engine/core/helper") require("engine/core/math") local itest_dsl = require("engine/test/itest_dsl") -local dsl_itest, command, expectation, itest_dsl_parser = itest_dsl.dsl_itest, itest_dsl.command, itest_dsl.expectation, itest_dsl.itest_dsl_parser +local gameplay_value_data, generate_function_table = get_members(itest_dsl, "gameplay_value_data", "generate_function_table") +local parse_number, parse_vector, parse_horizontal_dir, parse_expect = get_members(itest_dsl, "parse_number", "parse_vector", "parse_horizontal_dir", "parse_expect") +local execute_warp, execute_move, execute_wait = get_members(itest_dsl, "execute_warp", "execute_move", "execute_wait") +local eval_pc_bottom_pos, eval_pc_velocity = get_members(itest_dsl, "eval_pc_bottom_pos", "eval_pc_velocity") +local command, expectation = get_members(itest_dsl, "command", "expectation") +local dsl_itest, itest_dsl_parser = get_members(itest_dsl, "dsl_itest", "itest_dsl_parser") local integrationtest = require("engine/test/integrationtest") -local itest_manager, time_trigger, integration_test = integrationtest.itest_manager, integrationtest.time_trigger, integrationtest.integration_test +local itest_manager, time_trigger, integration_test = get_members(integrationtest, "itest_manager", "time_trigger", "integration_test") local flow = require("engine/application/flow") local gameapp = require("game/application/gameapp") local gamestate = require("game/application/gamestate") @@ -16,13 +21,171 @@ local pc_data = require("game/data/playercharacter_data") describe('itest_dsl', function () + describe('gameplay_value_data', function () + + describe('_init', function () + it('should create gameplay value data', function () + local data = gameplay_value_data("position", parsable_types.vector) + assert.is_not_nil(data) + assert.are_same({"position", parsable_types.vector}, {data.name, data.parsable_type}) + end) + end) + + end) + + describe('generate_function_table', function () + + it('should assert when the number of arguments is wrong', function () + local enum_types = {a = 10, b = 20} + local module = { + use_a = function() end, + use_b = function() end + } + local function_table = generate_function_table(module, enum_types, "use_") + assert.are_same({[10] = module.use_a, [20] = module.use_b}, function_table) + end) + + end) + + describe('parse_number', function () + + it('should assert when the number of arguments is wrong', function () + assert.has_error(function () + parse_number({"too", "many"}) + end, "parse_number: got 2 args, expected 1") + end) + + it('should return the single string argument as number', function () + assert.are_equal(5, parse_number({"5"})) + end) + + end) + + describe('parse_vector', function () + + it('should assert when the number of arguments is wrong', function () + assert.has_error(function () + parse_vector({"too few"}) + end, "parse_vector: got 1 args, expected 2") + end) + + it('should return the 2 coordinate string arguments as vector', function () + assert.are_equal(vector(2, -3.5), parse_vector({"2", "-3.5"})) + end) + + end) + + describe('parse_horizontal_dir', function () + + it('should assert when the number of arguments is wrong', function () + assert.has_error(function () + parse_horizontal_dir({"too", "many"}) + end, "parse_horizontal_dir: got 2 args, expected 1") + end) + + it('should return the single argument as horizontal direction', function () + assert.are_equal(horizontal_dirs.right, parse_horizontal_dir({"right"})) + end) + + end) + + describe('parse_expect', function () + + it('should assert when the number of arguments is wrong', function () + assert.has_error(function () + parse_expect({"too few"}) + end, "parse_expect: got 1 args, expected at least 2") + end) + + it('should return the gameplay value type and the expected value, itself recursively parsed', function () + assert.are_same({gp_value_types.pc_bottom_pos, vector(1, 3)}, + {parse_expect({"pc_bottom_pos", "1", "3"})}) + end) + + end) + + describe('execute_', function () + + before_each(function () + -- some executions require the player character + stage.state.player_char = player_char() + end) + + after_each(function () + -- clean up dummy player character + stage.state:init() + end) + + describe('execute_warp', function () + + setup(function () + spy.on(player_char, "warp_bottom_to") + end) + + teardown(function () + player_char.warp_bottom_to:revert() + end) + + it('should call warp_bottom_to on the current player character', function () + execute_warp({vector(1, 3)}) + + assert.spy(player_char.warp_bottom_to).was_called(1) + assert.spy(player_char.warp_bottom_to).was_called_with(match.ref(stage.state.player_char), vector(1, 3)) + end) + + end) + + describe('execute_move', function () + + it('should call warp_bottom_to on the current player character', function () + execute_move({horizontal_dirs.right}) + assert.are_equal(vector(1, 0), stage.state.player_char.move_intention) + end) + + end) + + end) + + describe('eval_', function () + + before_each(function () + -- some evaluators require the player character + stage.state.player_char = player_char() + end) + + after_each(function () + -- clean up dummy player character + stage.state:init() + end) + + describe('eval_pc_bottom_pos', function () + + it('should call warp_bottom_to on the current player character', function () + stage.state.player_char:set_bottom_center(vector(12, 47)) + assert.are_equal(vector(12, 47), eval_pc_bottom_pos()) + end) + + end) + + describe('eval_pc_velocity', function () + + it('should call warp_bottom_to on the current player character', function () + stage.state.player_char.velocity = vector(1, -4) + assert.are_equal(vector(1, -4), eval_pc_velocity()) + end) + + end) + + end) + + describe('command', function () describe('_init', function () it('should create a new dsl itest', function () - local cmd = command(itest_dsl_command_types.move, {horizontal_dirs.left}) + local cmd = command(command_types.move, {horizontal_dirs.left}) assert.is_not_nil(cmd) - assert.are_same({itest_dsl_command_types.move, {horizontal_dirs.left}}, {cmd.type, cmd.args}) + assert.are_same({command_types.move, {horizontal_dirs.left}}, {cmd.type, cmd.args}) end) end) @@ -32,9 +195,9 @@ describe('itest_dsl', function () describe('_init', function () it('should create a new dsl itest', function () - local exp = expectation(itest_dsl_gp_value_types.pc_bottom_pos, 24) + local exp = expectation(gp_value_types.pc_bottom_pos, 24) assert.is_not_nil(exp) - assert.are_same({itest_dsl_gp_value_types.pc_bottom_pos, 24}, {exp.gp_value_type, exp.expected_value}) + assert.are_same({gp_value_types.pc_bottom_pos, 24}, {exp.gp_value_type, exp.expected_value}) end) end) @@ -134,8 +297,8 @@ describe('itest_dsl', function () end) stub(itest_dsl_parser, "parse_action_sequence", function (lines, next_line_index) return { - command(itest_dsl_command_types[lines[next_line_index]], { vector(1, 2) } ), - command(itest_dsl_command_types[lines[next_line_index+1]], {itest_dsl_gp_value_types.pc_bottom_pos, vector(3, 4)}) + command(command_types[lines[next_line_index]], { vector(1, 2) } ), + command(command_types[lines[next_line_index+1]], {gp_value_types.pc_bottom_pos, vector(3, 4)}) } end) end) @@ -170,8 +333,8 @@ expect {64, 0} }), { - command(itest_dsl_command_types.warp, { vector(1, 2) } ), - command(itest_dsl_command_types.expect, {itest_dsl_gp_value_types.pc_bottom_pos, vector(3, 4)}) + command(command_types.warp, { vector(1, 2) } ), + command(command_types.expect, {gp_value_types.pc_bottom_pos, vector(3, 4)}) } }, { @@ -361,9 +524,9 @@ expect end) - describe('parse_action_sequence', function () + describe('#solo parse_action_sequence', function () - it('should return ', function () + it('should return a sequence of commands read in lines, starting at next_line_index', function () local dsli_lines = { "???", "???", @@ -379,16 +542,30 @@ expect local commands = itest_dsl_parser.parse_action_sequence(dsli_lines, 5) assert.are_same( { - command(itest_dsl_command_types.warp, { vector(12, 45) } ), - command(itest_dsl_command_types.wait, { 1 } ), - command(itest_dsl_command_types.move, { horizontal_dirs.left } ), - command(itest_dsl_command_types.wait, { 2 } ), - command(itest_dsl_command_types.expect, {itest_dsl_gp_value_types.pc_bottom_pos, vector(10, 45)}), - command(itest_dsl_command_types.expect, {itest_dsl_gp_value_types.pc_velocity, vector(2, -3.5)}), + command(command_types.warp, { vector(12, 45) } ), + command(command_types.wait, { 1 } ), + command(command_types.move, { horizontal_dirs.left } ), + command(command_types.wait, { 2 } ), + command(command_types.expect, {gp_value_types.pc_bottom_pos, vector(10, 45)}), + command(command_types.expect, {gp_value_types.pc_velocity, vector(2, -3.5)}), }, commands) end) + it('should assert if an unknown command is found', function () + local dsli_lines = { + "???", + "???", + "???", + "", + "unknown ? ?", + } + assert.has_error(function () + itest_dsl_parser.parse_action_sequence(dsli_lines, 5) + end, + "no command type named 'unknown'") + end) + end) describe('create_itest', function () @@ -399,13 +576,13 @@ expect dsli.stage_name = "test1" dsli.tilemap = nil dsli.commands = { - command(itest_dsl_command_types.warp, { vector(12, 45) } ), - command(itest_dsl_command_types.wait, { 10 } ), - command(itest_dsl_command_types.wait, { 1 } ), - command(itest_dsl_command_types.move, { horizontal_dirs.left } ), - command(itest_dsl_command_types.wait, { 2 } ), - command(itest_dsl_command_types.expect, {itest_dsl_gp_value_types.pc_bottom_pos, vector(10, 45)}), - command(itest_dsl_command_types.expect, {itest_dsl_gp_value_types.pc_velocity, vector(2, -3.5)}), + command(command_types.warp, { vector(12, 45) } ), + command(command_types.wait, { 10 } ), + command(command_types.wait, { 1 } ), + command(command_types.move, { horizontal_dirs.left } ), + command(command_types.wait, { 2 } ), + command(command_types.expect, {gp_value_types.pc_bottom_pos, vector(10, 45)}), + command(command_types.expect, {gp_value_types.pc_velocity, vector(2, -3.5)}), } local test = itest_dsl_parser.create_itest("test 1", dsli) @@ -532,37 +709,49 @@ expect end) - describe('_evaluate', function () + describe('_act', function () - it('should assert if an unknown gameplay value type is passed', function () - assert.has_error(function () - itest_dsl_parser._evaluate(-1) - end, "unknown gameplay value: -1") + local function f() end + + before_each(function () + itest_dsl_parser._itest = integration_test("test 1", {}) + end) + + after_each(function () + itest_manager:init() + end) + + it('should add an action after an existing time trigger, and clear the last time trigger', function () + itest_dsl_parser._last_time_trigger = time_trigger(3, true) + itest_dsl_parser:_act(f) + assert.are_equal(1, #itest_dsl_parser._itest.action_sequence) + local action = itest_dsl_parser._itest.action_sequence[1] + assert.are_same({time_trigger(3, true), f, + nil}, + {action.trigger, action.callback, + itest_dsl_parser._last_time_trigger}) end) end) - describe('_add_final_expectation', function () + describe('_wait', function () before_each(function () itest_dsl_parser._itest = integration_test('test', {}) end) - it('should add to the final expectation an expectation with gameplay value type and expected value', function () - itest_dsl_parser:_add_final_expectation(itest_dsl_gp_value_types.pc_bottom_pos, vector(27, 30)) - assert.are_equal(1, #itest_dsl_parser._final_expectations) - assert.are_equal(expectation(itest_dsl_gp_value_types.pc_bottom_pos, vector(27, 30)), itest_dsl_parser._final_expectations[1]) + it('should set the current time_trigger of the parser to one with the passed interval, in frames', function () + itest_dsl_parser:_wait(12) + assert.are_equal(time_trigger(12, true), itest_dsl_parser._last_time_trigger) end) - it('should add to the final expectation an expectation with gameplay value type and expected value', function () - itest_dsl_parser._final_expectations = { - expectation(itest_dsl_gp_value_types.pc_bottom_pos, vector(27, 30)) - } - - itest_dsl_parser:_add_final_expectation(itest_dsl_gp_value_types.pc_velocity, vector(-5, 3)) - - assert.are_equal(2, #itest_dsl_parser._final_expectations) - assert.are_equal(expectation(itest_dsl_gp_value_types.pc_velocity, vector(-5, 3)), itest_dsl_parser._final_expectations[2]) + it('should add a dummy action with any previous time trigger, then set the last time trigger to the new one', function () + itest_dsl_parser._last_time_trigger = time_trigger(4, true) + itest_dsl_parser:_wait(8) + assert.are_equal(1, #itest_dsl_parser._itest.action_sequence) + local action = itest_dsl_parser._itest.action_sequence[1] + assert.are_same({time_trigger(4, true), nil}, {action.trigger, action.callback}) + assert.are_equal(time_trigger(8, true), itest_dsl_parser._last_time_trigger) end) end) @@ -570,18 +759,18 @@ expect describe('_define_final_assertion', function () setup(function () - -- mock _evaluate (we won't care about the 1st argument thx to this) - stub(itest_dsl_parser, "_evaluate", function (gp_value_type) - if gp_value_type == itest_dsl_gp_value_types.pc_bottom_pos then - return vector(27, 30) - else - return vector(-3, 2.5) - end - end) + -- mock evaluators + itest_dsl.evaluators[gp_value_types.pc_bottom_pos] = function () + return vector(27, 30) + end + itest_dsl.evaluators[gp_value_types.pc_velocity] = function () + return vector(-3, 2.5) + end end) teardown(function () - itest_dsl_parser._evaluate:revert() + -- reset evaluators + itest_dsl.evaluators = generate_function_table(itest_dsl, gp_value_types, "eval_") end) before_each(function () @@ -590,8 +779,8 @@ expect it('should set the final assertion as returning true, message when the gameplay value is expected', function () itest_dsl_parser._final_expectations = { - expectation(itest_dsl_gp_value_types.pc_bottom_pos, vector(27, 30)), - expectation(itest_dsl_gp_value_types.pc_velocity, vector(-3, 2.5)) + expectation(gp_value_types.pc_bottom_pos, vector(27, 30)), + expectation(gp_value_types.pc_velocity, vector(-3, 2.5)) } itest_dsl_parser:_define_final_assertion() assert.are_same({true, ""}, {itest_dsl_parser._itest.final_assertion()}) @@ -599,8 +788,8 @@ expect it('should set the final assertion as returning false, message when the gameplay value is not expected', function () itest_dsl_parser._final_expectations = { - expectation(itest_dsl_gp_value_types.pc_bottom_pos, vector(27, 30)), -- ok - expectation(itest_dsl_gp_value_types.pc_velocity, vector(-3, 7.5)) -- different from actual + expectation(gp_value_types.pc_bottom_pos, vector(27, 30)), -- ok + expectation(gp_value_types.pc_velocity, vector(-3, 7.5)) -- different from actual } itest_dsl_parser:_define_final_assertion() local expected_message = "Passed gameplay value 'player character velocity':\nvector(-3, 2.5)\nExpected:\nvector(-3, 7.5)\n" @@ -616,26 +805,6 @@ expect end) - describe('_evaluate', function () - - -- add gameplay value types tests here - - it('should return the player character bottom position for ', function () - -- simulate stage state on_enter by just creating pc - stage.state.player_char = player_char() - stage.state.player_char:spawn_bottom_at(vector(2, 8)) - - assert.are_equal(vector(2, 8), itest_dsl_parser._evaluate(itest_dsl_gp_value_types.pc_bottom_pos)) - end) - - it('should assert if an unknown gameplay value type is passed', function () - assert.has_error(function () - itest_dsl_parser._evaluate(-1) - end, "unknown gameplay value: -1") - end) - - end) - end) end) From a3bbb696cb3c2de231d06e179faacdc7149cc9a1 Mon Sep 17 00:00:00 2001 From: hsandt Date: Sun, 6 Jan 2019 02:08:07 +0000 Subject: [PATCH 020/140] [TEST] DSL: Added eval_ground_spd and remaining expects in itestplayercharacter --- src/engine/test/itest_dsl.lua | 10 ++++++++-- src/game/itests/itestplayercharacter.lua | 12 +++--------- tests/utestitest_dsl.lua | 25 ++++++++++++++++-------- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/src/engine/test/itest_dsl.lua b/src/engine/test/itest_dsl.lua index e97f11ed..ab0cb9e5 100644 --- a/src/engine/test/itest_dsl.lua +++ b/src/engine/test/itest_dsl.lua @@ -172,6 +172,10 @@ function itest_dsl.eval_pc_velocity() return stage.state.player_char.velocity end +function itest_dsl.eval_pc_ground_spd() + return stage.state.player_char.ground_speed +end + -- table of functions used to evaluate and returns the gameplay value in current game state evaluators = generate_function_table(itest_dsl, gp_value_types, "eval_") itest_dsl.evaluators = evaluators @@ -482,13 +486,15 @@ function itest_dsl_parser:_define_final_assertion() -- check each expectation one by one for exp in all(final_expectations_proxy) do - local gp_value = evaluators[exp.gp_value_type]() + local evaluator = evaluators[exp.gp_value_type] + assert(evaluator, "evaluators["..exp.gp_value_type.."] (for '"..gp_value_type_strings[exp.gp_value_type].."') is not defined") + local gp_value = evaluator() if gp_value ~= exp.expected_value then success = false local gp_value_data = gp_value_data_t[exp.gp_value_type] assert(gp_value_data, "gp_value_data_t["..exp.gp_value_type.."] is not defined") local gp_value_name = gp_value_data.name - local message = "Passed gameplay value '"..gp_value_name.."':\n".. + local message = "\nPassed gameplay value '"..gp_value_name.."':\n".. gp_value.."\n".. "Expected:\n".. exp.expected_value diff --git a/src/game/itests/itestplayercharacter.lua b/src/game/itests/itestplayercharacter.lua index d2973f72..19c445ee 100644 --- a/src/game/itests/itestplayercharacter.lua +++ b/src/game/itests/itestplayercharacter.lua @@ -28,20 +28,14 @@ itest_dsl_parser.register('#solo platformer accel right flat', [[ warp 4 8 move right wait 30 -expect pc_bottom_pos 14.8984375 8. +expect pc_bottom_pos 14.8984375 8 +expect pc_ground_spd 0.703125 +expect pc_velocity 0.703125 0 ]] ) --[[ original -itest.setup = function () - -- stage.state.player_char.motion_mode = motion_modes.platformer -end - -itest.teardown = function () - clear_map() - -- teardown_map_data() -end -- check that player char has moved to the right and is still on the ground itest.final_assertion = function () diff --git a/tests/utestitest_dsl.lua b/tests/utestitest_dsl.lua index f07f27cb..fe84d340 100644 --- a/tests/utestitest_dsl.lua +++ b/tests/utestitest_dsl.lua @@ -5,7 +5,7 @@ local itest_dsl = require("engine/test/itest_dsl") local gameplay_value_data, generate_function_table = get_members(itest_dsl, "gameplay_value_data", "generate_function_table") local parse_number, parse_vector, parse_horizontal_dir, parse_expect = get_members(itest_dsl, "parse_number", "parse_vector", "parse_horizontal_dir", "parse_expect") local execute_warp, execute_move, execute_wait = get_members(itest_dsl, "execute_warp", "execute_move", "execute_wait") -local eval_pc_bottom_pos, eval_pc_velocity = get_members(itest_dsl, "eval_pc_bottom_pos", "eval_pc_velocity") +local eval_pc_bottom_pos, eval_pc_velocity, eval_pc_ground_spd = get_members(itest_dsl, "eval_pc_bottom_pos", "eval_pc_velocity", "eval_pc_ground_spd") local command, expectation = get_members(itest_dsl, "command", "expectation") local dsl_itest, itest_dsl_parser = get_members(itest_dsl, "dsl_itest", "itest_dsl_parser") local integrationtest = require("engine/test/integrationtest") @@ -137,7 +137,7 @@ describe('itest_dsl', function () describe('execute_move', function () - it('should call warp_bottom_to on the current player character', function () + it('should set the move intention of the current player character to the directional unit vector matching his horizontal direction', function () execute_move({horizontal_dirs.right}) assert.are_equal(vector(1, 0), stage.state.player_char.move_intention) end) @@ -160,7 +160,7 @@ describe('itest_dsl', function () describe('eval_pc_bottom_pos', function () - it('should call warp_bottom_to on the current player character', function () + it('should return the bottom position of the current player character', function () stage.state.player_char:set_bottom_center(vector(12, 47)) assert.are_equal(vector(12, 47), eval_pc_bottom_pos()) end) @@ -169,13 +169,22 @@ describe('itest_dsl', function () describe('eval_pc_velocity', function () - it('should call warp_bottom_to on the current player character', function () + it('should return the velocity the current player character', function () stage.state.player_char.velocity = vector(1, -4) assert.are_equal(vector(1, -4), eval_pc_velocity()) end) end) + describe('eval_pc_ground_spd', function () + + it('should return the ground speed current player character', function () + stage.state.player_char.ground_speed = 3.5 + assert.are_equal(3.5, eval_pc_ground_spd()) + end) + + end) + end) @@ -524,7 +533,7 @@ expect end) - describe('#solo parse_action_sequence', function () + describe('parse_action_sequence', function () it('should return a sequence of commands read in lines, starting at next_line_index', function () local dsli_lines = { @@ -632,8 +641,8 @@ expect assert.are_equal(vector(-1, 0), stage.state.player_char.move_intention) -- we have not passed time so the character cannot have reached expected position - local expected_message = "Passed gameplay value 'player character bottom position':\nvector(12, 45)\nExpected:\nvector(10, 45)\n".. - "Passed gameplay value 'player character velocity':\nvector(0, 0)\nExpected:\nvector(2, -3.5)\n" + local expected_message = "\nPassed gameplay value 'player character bottom position':\nvector(12, 45)\nExpected:\nvector(10, 45)\n".. + "\nPassed gameplay value 'player character velocity':\nvector(0, 0)\nExpected:\nvector(2, -3.5)\n" assert.are_same({false, expected_message}, {test.final_assertion()}) -- but if we cheat and warp him on the spot, final assertion will work @@ -792,7 +801,7 @@ expect expectation(gp_value_types.pc_velocity, vector(-3, 7.5)) -- different from actual } itest_dsl_parser:_define_final_assertion() - local expected_message = "Passed gameplay value 'player character velocity':\nvector(-3, 2.5)\nExpected:\nvector(-3, 7.5)\n" + local expected_message = "\nPassed gameplay value 'player character velocity':\nvector(-3, 2.5)\nExpected:\nvector(-3, 7.5)\n" assert.are_same({false, expected_message}, {itest_dsl_parser._itest.final_assertion()}) end) From c915722fadf5303718e856d68ab0f169304bfed4 Mon Sep 17 00:00:00 2001 From: hsandt Date: Sun, 6 Jan 2019 02:23:16 +0000 Subject: [PATCH 021/140] [TEST] DSL: Added parse and eval motion_state --- prebuild/replace_strings.py | 1 + src/engine/test/itest_dsl.lua | 24 ++++++++++++--- src/game/itests/itestplayercharacter.lua | 37 +++--------------------- tests/utestitest_dsl.lua | 27 +++++++++++++++-- 4 files changed, 50 insertions(+), 39 deletions(-) diff --git a/prebuild/replace_strings.py b/prebuild/replace_strings.py index 3a33950f..b8db231a 100755 --- a/prebuild/replace_strings.py +++ b/prebuild/replace_strings.py @@ -126,6 +126,7 @@ 'number': 1, 'vector': 2, 'horizontal_dir': 11, + 'motion_state': 12, 'expect': 21, }, diff --git a/src/engine/test/itest_dsl.lua b/src/engine/test/itest_dsl.lua index ab0cb9e5..c985a181 100644 --- a/src/engine/test/itest_dsl.lua +++ b/src/engine/test/itest_dsl.lua @@ -52,6 +52,7 @@ parsable_types = { number = 1, vector = 2, horizontal_dir = 11, + motion_state = 12, expect = 21, -- meta-type meaning we must check the 1st arg (gp_value_type) to know what the rest should be } @@ -83,9 +84,10 @@ command_arg_types = { -- type of gameplay values available for expectations gp_value_types = { - pc_bottom_pos = 1, -- bottom position of player character - pc_velocity = 11, -- velocity of player character - pc_ground_spd = 12, -- ground speed of player character + pc_bottom_pos = 1, -- bottom position of player character + pc_velocity = 11, -- velocity of player character + pc_ground_spd = 12, -- ground speed of player character + pc_motion_state = 21, -- motion state of player character } --#if assert @@ -97,6 +99,7 @@ local gp_value_data_t = { [gp_value_types.pc_bottom_pos] = gameplay_value_data("player character bottom position", parsable_types.vector), [gp_value_types.pc_velocity] = gameplay_value_data("player character velocity", parsable_types.vector), [gp_value_types.pc_ground_spd] = gameplay_value_data("player character ground speed", parsable_types.number), + [gp_value_types.pc_motion_state] = gameplay_value_data("player character motion state", parsable_types.motion_state), } @@ -114,7 +117,16 @@ end function itest_dsl.parse_horizontal_dir(arg_strings) assert(#arg_strings == 1, "parse_horizontal_dir: got "..#arg_strings.." args, expected 1") - return horizontal_dirs[arg_strings[1]] + local horizontal_dir = horizontal_dirs[arg_strings[1]] + assert(horizontal_dir, "horizontal_dirs["..arg_strings[1].."] is not defined") + return horizontal_dir +end + +function itest_dsl.parse_motion_state(arg_strings) + assert(#arg_strings == 1, "parse_motion_state: got "..#arg_strings.." args, expected 1") + local motion_state = motion_states[arg_strings[1]] + assert(motion_state, "motion_states["..arg_strings[1].."] is not defined") + return motion_states[arg_strings[1]] end -- convert string args to vector @@ -176,6 +188,10 @@ function itest_dsl.eval_pc_ground_spd() return stage.state.player_char.ground_speed end +function itest_dsl.eval_pc_motion_state() + return stage.state.player_char.motion_state +end + -- table of functions used to evaluate and returns the gameplay value in current game state evaluators = generate_function_table(itest_dsl, gp_value_types, "eval_") itest_dsl.evaluators = evaluators diff --git a/src/game/itests/itestplayercharacter.lua b/src/game/itests/itestplayercharacter.lua index 19c445ee..c2892803 100644 --- a/src/game/itests/itestplayercharacter.lua +++ b/src/game/itests/itestplayercharacter.lua @@ -29,44 +29,15 @@ warp 4 8 move right wait 30 expect pc_bottom_pos 14.8984375 8 +expect pc_motion_state grounded expect pc_ground_spd 0.703125 expect pc_velocity 0.703125 0 ]] ) - ---[[ original - --- check that player char has moved to the right and is still on the ground -itest.final_assertion = function () - -- local is_motion_state_expected, motion_state_message = motion_states.grounded == stage.state.player_char.motion_state, "Expected motion state 'grounded', got "..stage.state.player_char.motion_state - -- no almost eq - -- no bottom center - local is_position_expected, position_message = almost_eq_with_message(vector(14.8984375, 80.), stage.state.player_char:get_bottom_center(), 1/256) - -- local is_ground_speed_expected, ground_speed_message = almost_eq_with_message(0.703125, stage.state.player_char.ground_speed, 1/256) - -- local is_velocity_expected, velocity_message = almost_eq_with_message(vector(0.703125, 0), stage.state.player_char.velocity, 1/256) - - -- local final_message = "" - - -- local success = is_position_expected and is_ground_speed_expected and is_velocity_expected and is_motion_state_expected - -- if not success then - -- if not is_motion_state_expected then - -- final_message = final_message..motion_state_message.."\n" - -- end - -- if not is_position_expected then - -- final_message = final_message..position_message.."\n" - -- end - -- if not is_ground_speed_expected then - -- final_message = final_message..ground_speed_message.."\n" - -- end - -- if not is_velocity_expected then - -- final_message = final_message..velocity_message.."\n" - -- end - - -- end -end ---]] - +-- todo: +-- eq (add = symbol before expected value) +-- almost_eq (add ~ symbol before expected value, default to threshold: 1/256) --[[ itest = integration_test('debug move right', {stage.state.type}) diff --git a/tests/utestitest_dsl.lua b/tests/utestitest_dsl.lua index fe84d340..eb8d6094 100644 --- a/tests/utestitest_dsl.lua +++ b/tests/utestitest_dsl.lua @@ -3,9 +3,9 @@ require("engine/core/helper") require("engine/core/math") local itest_dsl = require("engine/test/itest_dsl") local gameplay_value_data, generate_function_table = get_members(itest_dsl, "gameplay_value_data", "generate_function_table") -local parse_number, parse_vector, parse_horizontal_dir, parse_expect = get_members(itest_dsl, "parse_number", "parse_vector", "parse_horizontal_dir", "parse_expect") +local parse_number, parse_vector, parse_horizontal_dir, parse_motion_state, parse_expect = get_members(itest_dsl, "parse_number", "parse_vector", "parse_horizontal_dir", "parse_motion_state", "parse_expect") local execute_warp, execute_move, execute_wait = get_members(itest_dsl, "execute_warp", "execute_move", "execute_wait") -local eval_pc_bottom_pos, eval_pc_velocity, eval_pc_ground_spd = get_members(itest_dsl, "eval_pc_bottom_pos", "eval_pc_velocity", "eval_pc_ground_spd") +local eval_pc_bottom_pos, eval_pc_velocity, eval_pc_ground_spd, eval_pc_motion_state = get_members(itest_dsl, "eval_pc_bottom_pos", "eval_pc_velocity", "eval_pc_ground_spd", "eval_pc_motion_state") local command, expectation = get_members(itest_dsl, "command", "expectation") local dsl_itest, itest_dsl_parser = get_members(itest_dsl, "dsl_itest", "itest_dsl_parser") local integrationtest = require("engine/test/integrationtest") @@ -89,6 +89,20 @@ describe('itest_dsl', function () end) + describe('parse_motion_state', function () + + it('should assert when the number of arguments is wrong', function () + assert.has_error(function () + parse_motion_state({"too", "many"}) + end, "parse_motion_state: got 2 args, expected 1") + end) + + it('should return the single argument as motion state', function () + assert.are_equal(motion_states.airborne, parse_motion_state({"airborne"})) + end) + + end) + describe('parse_expect', function () it('should assert when the number of arguments is wrong', function () @@ -185,6 +199,15 @@ describe('itest_dsl', function () end) + describe('eval_pc_motion_state', function () + + it('should return the ground speed current player character', function () + stage.state.player_char.motion_state = motion_states.airborne + assert.are_equal(motion_states.airborne, eval_pc_motion_state()) + end) + + end) + end) From 6067f9a69fc0e045832ac74f99115441ba7c5e34 Mon Sep 17 00:00:00 2001 From: hsandt Date: Tue, 8 Jan 2019 09:27:11 +0000 Subject: [PATCH 022/140] [HELPER] Added enum function --- src/engine/core/helper.lua | 14 ++++++++++++++ tests/utesthelper.lua | 15 ++++++++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/engine/core/helper.lua b/src/engine/core/helper.lua index 64be5998..eb97a8ac 100644 --- a/src/engine/core/helper.lua +++ b/src/engine/core/helper.lua @@ -1,6 +1,20 @@ require("engine/application/constants") +-- create an enum from variadic arguments +function enum(...) + local variant_names = {...} + local t = {} + local i = 1 + + for variant_name in all(variant_names) do + t[variant_name] = i + i = i + 1 + end + + return t +end + -- implementation of "map" in other languages (but "map" means something else in pico8) function transform(t, func) local transformed_t = {} diff --git a/tests/utesthelper.lua b/tests/utesthelper.lua index c1dd20f7..2bd9effa 100644 --- a/tests/utesthelper.lua +++ b/tests/utesthelper.lua @@ -2,13 +2,14 @@ require("bustedhelper") helper = require("engine/core/helper") math = require("engine/core/math") -- just to test stringify and are_same -describe('transform', function () - it('should return a table where an operation has been applied to each element of the original table', function () - local t = {-1, 2, 3} - local function double(x) - return 2 * x - end - assert.are_same({-2, 4, 6}, transform(t, double)) +describe('enum', function () + it('should return a table containing enum variants with names passed as variadic parameters, values starting from 1', function () + assert.are_same({ + left = 1, + right = 2, + up = 3, + down = 4 + }, enum("left", "right", "up", "down")) end) end) From 8b270e8689d16ddfb1a9e734ef959b7216f27efc Mon Sep 17 00:00:00 2001 From: hsandt Date: Mon, 14 Jan 2019 23:21:54 +0000 Subject: [PATCH 023/140] [HELPER] enum now takes 1 table parameter --- src/engine/core/helper.lua | 5 ++--- tests/utesthelper.lua | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/engine/core/helper.lua b/src/engine/core/helper.lua index eb97a8ac..e6d140bd 100644 --- a/src/engine/core/helper.lua +++ b/src/engine/core/helper.lua @@ -1,9 +1,8 @@ require("engine/application/constants") --- create an enum from variadic arguments -function enum(...) - local variant_names = {...} +-- create an enum from a sequence of variant names +function enum(variant_names) local t = {} local i = 1 diff --git a/tests/utesthelper.lua b/tests/utesthelper.lua index 2bd9effa..4a3481a7 100644 --- a/tests/utesthelper.lua +++ b/tests/utesthelper.lua @@ -3,13 +3,13 @@ helper = require("engine/core/helper") math = require("engine/core/math") -- just to test stringify and are_same describe('enum', function () - it('should return a table containing enum variants with names passed as variadic parameters, values starting from 1', function () + it('should return a table containing enum variants with the names passed as a sequence, values starting from 1', function () assert.are_same({ left = 1, right = 2, up = 3, down = 4 - }, enum("left", "right", "up", "down")) + }, enum {"left", "right", "up", "down"}) end) end) From edc20d47933191784b02e96ed774f2937b09ed0f Mon Sep 17 00:00:00 2001 From: hsandt Date: Mon, 14 Jan 2019 23:23:00 +0000 Subject: [PATCH 024/140] [TEST] DSL: enums are now defined with enum function, with contiguous indices --- prebuild/replace_strings.py | 17 ++++++++-------- src/engine/test/itest_dsl.lua | 37 +++++++++++++++++++---------------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/prebuild/replace_strings.py b/prebuild/replace_strings.py index b8db231a..35e6b461 100755 --- a/prebuild/replace_strings.py +++ b/prebuild/replace_strings.py @@ -125,22 +125,23 @@ 'parsable_types': { 'number': 1, 'vector': 2, - 'horizontal_dir': 11, - 'motion_state': 12, - 'expect': 21, + 'horizontal_dir': 3, + 'motion_state': 4, + 'expect': 5, }, 'command_types': { 'warp': 1, 'move': 2, - 'wait': 11, - 'expect': 21, + 'wait': 3, + 'expect': 4, }, 'gp_value_types': { - 'pc_bottom_pos': 1, - 'pc_velocity': 11, - 'pc_ground_spd': 12, + 'pc_bottom_pos': 1, + 'pc_velocity': 2, + 'pc_ground_spd': 3, + 'pc_motion_state': 4, }, } diff --git a/src/engine/test/itest_dsl.lua b/src/engine/test/itest_dsl.lua index c985a181..8abe881e 100644 --- a/src/engine/test/itest_dsl.lua +++ b/src/engine/test/itest_dsl.lua @@ -48,12 +48,12 @@ itest_dsl.generate_function_table = generate_function_table --#endif -- type of variables that can be parsed -parsable_types = { - number = 1, - vector = 2, - horizontal_dir = 11, - motion_state = 12, - expect = 21, -- meta-type meaning we must check the 1st arg (gp_value_type) to know what the rest should be +parsable_types = enum { + "number", + "vector", + "horizontal_dir", + "motion_state", + "expect", -- meta-type meaning we must check the 1st arg (gp_value_type) to know what the rest should be } --#if assert @@ -62,11 +62,12 @@ parsable_type_strings = invert_table(parsable_types) -- type of commands available -command_types = { - warp = 1, -- warp player character bottom args: {bottom_position: vector} - move = 2, -- set sticky pc move intention args: {move_dir: horizontal_dirs} - wait = 11, -- wait some frames args: {frames: int} - expect = 21, -- expect a gameplay value args: {gp_value_type: gp_value_types, expected_args...: matching gp value parsable type} +command_types = enum { + "warp", -- warp player character bottom args: {bottom_position: vector} + "move", -- set sticky pc move intention args: {move_dir: horizontal_dirs} + -- todo: stop, jump, crouch, spin_dash + "wait", -- wait some frames args: {frames: int} + "expect", -- expect a gameplay value args: {gp_value_type: gp_value_types, expected_args...: matching gp value parsable type} } --#if assert @@ -83,11 +84,11 @@ command_arg_types = { -- type of gameplay values available for expectations -gp_value_types = { - pc_bottom_pos = 1, -- bottom position of player character - pc_velocity = 11, -- velocity of player character - pc_ground_spd = 12, -- ground speed of player character - pc_motion_state = 21, -- motion state of player character +gp_value_types = enum { + "pc_bottom_pos", -- bottom position of player character + "pc_velocity", -- velocity of player character + "pc_ground_spd", -- ground speed of player character + "pc_motion_state", -- motion state of player character } --#if assert @@ -99,7 +100,7 @@ local gp_value_data_t = { [gp_value_types.pc_bottom_pos] = gameplay_value_data("player character bottom position", parsable_types.vector), [gp_value_types.pc_velocity] = gameplay_value_data("player character velocity", parsable_types.vector), [gp_value_types.pc_ground_spd] = gameplay_value_data("player character ground speed", parsable_types.number), - [gp_value_types.pc_motion_state] = gameplay_value_data("player character motion state", parsable_types.motion_state), + [gp_value_types.pc_motion_state] = gameplay_value_data("player character motion state", parsable_types.motion_state), } @@ -142,8 +143,10 @@ function itest_dsl.parse_expect(arg_strings) end -- determine the type of value reference tested for comparison (e.g. pc position) local gp_value_type = gp_value_types[gp_value_type_str] + assert(gp_value_type, "gp_value_types['"..gp_value_type_str.."'] is not defined") -- parse the value components to semantical type (e.g. vector) local gp_value_data = gp_value_data_t[gp_value_type] + assert(gp_value_data, "gp_value_data_t["..gp_value_type.."] (for '"..gp_value_type_str.."') is not defined") local expected_value_parser = value_parsers[gp_value_data.parsable_type] assert(expected_value_parser, "no value parser defined for gp value type '"..parsable_type_strings[gp_value_data.parsable_type].."'") local expected_value = expected_value_parser(expected_value_comps) From e9672f2a2c260b6db7c1b865f1155e99db9b47b1 Mon Sep 17 00:00:00 2001 From: hsandt Date: Mon, 14 Jan 2019 23:45:38 +0000 Subject: [PATCH 025/140] [BUILD] itest config now keeps assert but strips visual_logger --- prebuild/preprocess.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/prebuild/preprocess.py b/prebuild/preprocess.py index e2a3c884..fcdfdeaf 100644 --- a/prebuild/preprocess.py +++ b/prebuild/preprocess.py @@ -16,10 +16,17 @@ # For non-pico8 builds, we use --#ifn pico8 to indicate we won't have preprocessing, # but for busted unit tests we prefer using --#if utest (which is never defined) to make clear that # the code is only needed for a purpose of redundancy and unit test harnessing in general. + +# assert +# If 'assert' is defined here, it should *not* be stripped in the next table. +# The 'assert' symbol if only here to only to strip multi-line asserts or blocks of functions only used inside assert calls. + +# visual_logger +# 'visual_logger' requires 'log', the dependeency is not automatically solved. defined_symbols_table = { 'debug': ['pico8', 'assert', 'log', 'visual_logger', 'tuner', 'profiler', 'mouse', 'cheat'], 'assert': ['pico8', 'assert', 'log', 'visual_logger'], - 'itest': ['pico8', 'log', 'visual_logger', 'test'], + 'itest': ['pico8', 'assert', 'log', 'test'], 'itest_light': ['pico8', 'log', 'test'], 'profiler': ['pico8', 'log', 'visual_logger', 'profiler'], 'visual_log': ['pico8', 'log', 'visual_logger'], @@ -37,7 +44,7 @@ stripped_functions_table = { 'debug': [], 'assert': [], - 'itest': ['assert'], + 'itest': [], 'itest_light': ['assert'], 'profiler': ['assert'], 'visual_log': ['assert'], From 2fedad80a117a02b476308ed97da4ef047506c79 Mon Sep 17 00:00:00 2001 From: hsandt Date: Mon, 14 Jan 2019 23:46:48 +0000 Subject: [PATCH 026/140] [ITEST] DSL: added missing require playercharacter for motion_states --- src/engine/test/itest_dsl.lua | 4 +++- tests/utestitest_dsl.lua | 18 ++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/engine/test/itest_dsl.lua b/src/engine/test/itest_dsl.lua index 8abe881e..88389b45 100644 --- a/src/engine/test/itest_dsl.lua +++ b/src/engine/test/itest_dsl.lua @@ -1,6 +1,7 @@ require("engine/core/helper") local integrationtest = require("engine/test/integrationtest") -local itest_manager, integration_test = integrationtest.itest_manager, integrationtest.integration_test +local itest_manager, integration_test = get_members(integrationtest, + "itest_manager", "integration_test") local tile_data = require("game/data/tile_data") local tilemap = require("engine/data/tilemap") @@ -8,6 +9,7 @@ local tilemap = require("engine/data/tilemap") -- dsl interpretation requirements local flow = require("engine/application/flow") local stage = require("game/ingame/stage") -- required +local player_char = require("game/ingame/playercharacter") local pc_data = require("game/data/playercharacter_data") diff --git a/tests/utestitest_dsl.lua b/tests/utestitest_dsl.lua index eb8d6094..4734edbb 100644 --- a/tests/utestitest_dsl.lua +++ b/tests/utestitest_dsl.lua @@ -2,14 +2,20 @@ require("bustedhelper") require("engine/core/helper") require("engine/core/math") local itest_dsl = require("engine/test/itest_dsl") -local gameplay_value_data, generate_function_table = get_members(itest_dsl, "gameplay_value_data", "generate_function_table") +local gameplay_value_data, generate_function_table = get_members(itest_dsl, + "gameplay_value_data", "generate_function_table") local parse_number, parse_vector, parse_horizontal_dir, parse_motion_state, parse_expect = get_members(itest_dsl, "parse_number", "parse_vector", "parse_horizontal_dir", "parse_motion_state", "parse_expect") -local execute_warp, execute_move, execute_wait = get_members(itest_dsl, "execute_warp", "execute_move", "execute_wait") -local eval_pc_bottom_pos, eval_pc_velocity, eval_pc_ground_spd, eval_pc_motion_state = get_members(itest_dsl, "eval_pc_bottom_pos", "eval_pc_velocity", "eval_pc_ground_spd", "eval_pc_motion_state") -local command, expectation = get_members(itest_dsl, "command", "expectation") -local dsl_itest, itest_dsl_parser = get_members(itest_dsl, "dsl_itest", "itest_dsl_parser") +local execute_warp, execute_move, execute_wait = get_members(itest_dsl, + "execute_warp", "execute_move", "execute_wait") +local eval_pc_bottom_pos, eval_pc_velocity, eval_pc_ground_spd, eval_pc_motion_state = get_members(itest_dsl, + "eval_pc_bottom_pos", "eval_pc_velocity", "eval_pc_ground_spd", "eval_pc_motion_state") +local command, expectation = get_members(itest_dsl, + "command", "expectation") +local dsl_itest, itest_dsl_parser = get_members(itest_dsl, + "dsl_itest", "itest_dsl_parser") local integrationtest = require("engine/test/integrationtest") -local itest_manager, time_trigger, integration_test = get_members(integrationtest, "itest_manager", "time_trigger", "integration_test") +local itest_manager, time_trigger, integration_test = get_members(integrationtest, + "itest_manager", "time_trigger", "integration_test") local flow = require("engine/application/flow") local gameapp = require("game/application/gameapp") local gamestate = require("game/application/gamestate") From ecf481100595239c5ab3f82a4a2bafc9fbb13669 Mon Sep 17 00:00:00 2001 From: hsandt Date: Mon, 14 Jan 2019 23:56:38 +0000 Subject: [PATCH 027/140] [ITEST] Player Character: don't strip motion_states enum from pico8 build to allow itest_dsl dynamic access --- src/game/ingame/playercharacter.lua | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/game/ingame/playercharacter.lua b/src/game/ingame/playercharacter.lua index b6a3864a..c9adee0f 100644 --- a/src/game/ingame/playercharacter.lua +++ b/src/game/ingame/playercharacter.lua @@ -23,14 +23,21 @@ motion_modes = { debug = 2 -- debug "fly" mode } +--#endif + +-- motion_states is accessed dynamically via variant name in itest_dsl +-- so we don't strip it away from pico8 builds +-- it is only used for expectations, though, so it could be #if test only, +-- but the dsl may be used for attract mode later (dsl) so unless we distinguish +-- parsable types like motion_states that are only used for expectations +-- as opposed to actions, we should keep this in the release build + -- enum for character motion state in platformer mode motion_states = { grounded = 1, -- character is on the ground airborne = 2 -- character is in the air } ---#endif - local player_char = new_class() -- parameters From 63eab2e1f3a963883d0c720012e21712c9ea1bd6 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 18 Jan 2019 19:45:08 +0000 Subject: [PATCH 028/140] [ITEST] DSL: Added set_motion_mode command Added 'cheat' to itest build config to test debug motion mode --- prebuild/preprocess.py | 2 +- prebuild/replace_strings.py | 14 +-- src/engine/test/itest_dsl.lua | 56 +++++++++-- src/game/ingame/playercharacter.lua | 18 ++-- src/game/itests/itestplayercharacter.lua | 30 ++++-- tests/utestitest_dsl.lua | 122 ++++++++++++++--------- 6 files changed, 163 insertions(+), 79 deletions(-) diff --git a/prebuild/preprocess.py b/prebuild/preprocess.py index fcdfdeaf..b476d3fa 100644 --- a/prebuild/preprocess.py +++ b/prebuild/preprocess.py @@ -26,7 +26,7 @@ defined_symbols_table = { 'debug': ['pico8', 'assert', 'log', 'visual_logger', 'tuner', 'profiler', 'mouse', 'cheat'], 'assert': ['pico8', 'assert', 'log', 'visual_logger'], - 'itest': ['pico8', 'assert', 'log', 'test'], + 'itest': ['pico8', 'assert', 'log', 'test', 'cheat'], 'itest_light': ['pico8', 'log', 'test'], 'profiler': ['pico8', 'log', 'visual_logger', 'profiler'], 'visual_log': ['pico8', 'log', 'visual_logger'], diff --git a/prebuild/replace_strings.py b/prebuild/replace_strings.py index 35e6b461..089d3900 100755 --- a/prebuild/replace_strings.py +++ b/prebuild/replace_strings.py @@ -126,15 +126,17 @@ 'number': 1, 'vector': 2, 'horizontal_dir': 3, - 'motion_state': 4, - 'expect': 5, + 'motion_mode': 4, + 'motion_state': 5, + 'expect': 6, }, 'command_types': { - 'warp': 1, - 'move': 2, - 'wait': 3, - 'expect': 4, + 'warp': 1, + 'set_motion_mode': 2, + 'move': 3, + 'wait': 4, + 'expect': 5, }, 'gp_value_types': { diff --git a/src/engine/test/itest_dsl.lua b/src/engine/test/itest_dsl.lua index 88389b45..ec96652f 100644 --- a/src/engine/test/itest_dsl.lua +++ b/src/engine/test/itest_dsl.lua @@ -1,3 +1,31 @@ +--[[ itest domain-specific language definition and parser + +usage example: + +``` +itest_dsl_parser.register('debug move right', [[ +@stage # +... +### + +warp 4 8 +move right +wait 30 +expect pc_bottom_pos 14. 8. +``` + +list of commands: + +warp x y warp player character bottom to (x, y) +set_motion_mode mode set motion mode (do it before warping to avoid + unwanted position adjustment on arrival) +move dir set sticky pc move intention toward [dir] +wait n wait [n] frames +expect gp_value_type expect a gameplay value to be equal to (...) + (value params...) + +--]] + require("engine/core/helper") local integrationtest = require("engine/test/integrationtest") local itest_manager, integration_test = get_members(integrationtest, @@ -54,6 +82,7 @@ parsable_types = enum { "number", "vector", "horizontal_dir", + "motion_mode", "motion_state", "expect", -- meta-type meaning we must check the 1st arg (gp_value_type) to know what the rest should be } @@ -65,10 +94,11 @@ parsable_type_strings = invert_table(parsable_types) -- type of commands available command_types = enum { - "warp", -- warp player character bottom args: {bottom_position: vector} - "move", -- set sticky pc move intention args: {move_dir: horizontal_dirs} + "warp", -- warp player character bottom args: {bottom_position: vector} + "set_motion_mode", -- set motion mode args: {motion_mode_str: motion_modes key} + "move", -- set sticky pc move intention args: {move_dir_str: horizontal_dirs key} -- todo: stop, jump, crouch, spin_dash - "wait", -- wait some frames args: {frames: int} + "wait", -- wait some frames args: {frames: int} "expect", -- expect a gameplay value args: {gp_value_type: gp_value_types, expected_args...: matching gp value parsable type} } @@ -78,10 +108,11 @@ command_type_strings = invert_table(command_types) -- argument types expected after those commands command_arg_types = { - [command_types.warp] = parsable_types.vector, - [command_types.move] = parsable_types.horizontal_dir, - [command_types.wait] = parsable_types.number, - [command_types.expect] = parsable_types.expect, + [command_types.warp] = parsable_types.vector, + [command_types.set_motion_mode] = parsable_types.motion_mode, + [command_types.move] = parsable_types.horizontal_dir, + [command_types.wait] = parsable_types.number, + [command_types.expect] = parsable_types.expect, } @@ -125,6 +156,13 @@ function itest_dsl.parse_horizontal_dir(arg_strings) return horizontal_dir end +function itest_dsl.parse_motion_mode(arg_strings) + assert(#arg_strings == 1, "parse_motion_mode: got "..#arg_strings.." args, expected 1") + local motion_mode = motion_modes[arg_strings[1]] + assert(motion_mode, "motion_modes["..arg_strings[1].."] is not defined") + return motion_mode +end + function itest_dsl.parse_motion_state(arg_strings) assert(#arg_strings == 1, "parse_motion_state: got "..#arg_strings.." args, expected 1") local motion_state = motion_states[arg_strings[1]] @@ -168,6 +206,10 @@ function itest_dsl.execute_warp(args) stage.state.player_char:warp_bottom_to(args[1]) end +function itest_dsl.execute_set_motion_mode(args) + stage.state.player_char.motion_mode = args[1] +end + function itest_dsl.execute_move(args) stage.state.player_char.move_intention = horizontal_dir_vectors[args[1]] end diff --git a/src/game/ingame/playercharacter.lua b/src/game/ingame/playercharacter.lua index c9adee0f..302ac8ae 100644 --- a/src/game/ingame/playercharacter.lua +++ b/src/game/ingame/playercharacter.lua @@ -17,21 +17,21 @@ control_modes = { puppet = 3 -- itest script controls character } +--#endif + +-- motion_modes and motion_states are accessed dynamically via variant name in itest_dsl +-- so we don't strip them away from pico8 builds +-- it is only used for debug and expectations, though, so it could be #if cheat/test only, +-- but the dsl may be used for attract mode later (dsl) so unless we distinguish +-- parsable types like motion_states that are only used for expectations (and cheat actions) +-- as opposed to actions, we should keep this in the release build + -- enum for character motion mode motion_modes = { platformer = 1, -- normal in-game debug = 2 -- debug "fly" mode } ---#endif - --- motion_states is accessed dynamically via variant name in itest_dsl --- so we don't strip it away from pico8 builds --- it is only used for expectations, though, so it could be #if test only, --- but the dsl may be used for attract mode later (dsl) so unless we distinguish --- parsable types like motion_states that are only used for expectations --- as opposed to actions, we should keep this in the release build - -- enum for character motion state in platformer mode motion_states = { grounded = 1, -- character is on the ground diff --git a/src/game/itests/itestplayercharacter.lua b/src/game/itests/itestplayercharacter.lua index c2892803..9a202e6a 100644 --- a/src/game/itests/itestplayercharacter.lua +++ b/src/game/itests/itestplayercharacter.lua @@ -2,7 +2,8 @@ local integrationtest = require("engine/test/integrationtest") local itest_dsl = require("engine/test/itest_dsl") local itest_dsl_parser = itest_dsl.itest_dsl_parser -local itest_manager, integration_test, time_trigger = integrationtest.itest_manager, integrationtest.integration_test, integrationtest.time_trigger +local itest_manager, integration_test, time_trigger = get_members(integrationtest, + "itest_manager", "integration_test", "time_trigger") local input = require("engine/input/input") local flow = require("engine/application/flow") local stage = require("game/ingame/stage") -- required @@ -16,11 +17,9 @@ local itest --- dsl training +-- dsl definition --- pico8 doesn't like [[]] and will replace lines after the 3rd with --- empty lines... need "text \n".. or "text \ to continue to next line -itest_dsl_parser.register('#solo platformer accel right flat', [[ +itest_dsl_parser.register('platformer accel right flat', [[ @stage # ... ### @@ -28,18 +27,31 @@ itest_dsl_parser.register('#solo platformer accel right flat', [[ warp 4 8 move right wait 30 + expect pc_bottom_pos 14.8984375 8 expect pc_motion_state grounded expect pc_ground_spd 0.703125 expect pc_velocity 0.703125 0 -]] -) +]]) -- todo: -- eq (add = symbol before expected value) -- almost_eq (add ~ symbol before expected value, default to threshold: 1/256) +itest_dsl_parser.register('#solo debug move right', [[ +@stage # +.# + +set_motion_mode debug +warp 0 8 +move right +wait 60 + +expect pc_bottom_pos 56.7185 8 +]]) + --[[ + itest = integration_test('debug move right', {stage.state.type}) itest_manager:register(itest) @@ -1014,6 +1026,8 @@ end --]] +--[[ Really comment this block out for now, as it makes too many chars + --[[#pico8 -- human test for pico8 only to check rendering -- bugfix history: @@ -1043,3 +1057,5 @@ itest:add_action(time_trigger(1.), function () end) -- no final assertion, let the user check if result is correct or not (note it will display success whatever) -- #pico8]] + +--]] diff --git a/tests/utestitest_dsl.lua b/tests/utestitest_dsl.lua index 4734edbb..30c4bd6b 100644 --- a/tests/utestitest_dsl.lua +++ b/tests/utestitest_dsl.lua @@ -4,9 +4,6 @@ require("engine/core/math") local itest_dsl = require("engine/test/itest_dsl") local gameplay_value_data, generate_function_table = get_members(itest_dsl, "gameplay_value_data", "generate_function_table") -local parse_number, parse_vector, parse_horizontal_dir, parse_motion_state, parse_expect = get_members(itest_dsl, "parse_number", "parse_vector", "parse_horizontal_dir", "parse_motion_state", "parse_expect") -local execute_warp, execute_move, execute_wait = get_members(itest_dsl, - "execute_warp", "execute_move", "execute_wait") local eval_pc_bottom_pos, eval_pc_velocity, eval_pc_ground_spd, eval_pc_motion_state = get_members(itest_dsl, "eval_pc_bottom_pos", "eval_pc_velocity", "eval_pc_ground_spd", "eval_pc_motion_state") local command, expectation = get_members(itest_dsl, @@ -53,73 +50,91 @@ describe('itest_dsl', function () end) - describe('parse_number', function () + describe('parse_', function () - it('should assert when the number of arguments is wrong', function () - assert.has_error(function () - parse_number({"too", "many"}) - end, "parse_number: got 2 args, expected 1") - end) + describe('parse_number', function () + + it('should assert when the number of arguments is wrong', function () + assert.has_error(function () + itest_dsl.parse_number({"too", "many"}) + end, "parse_number: got 2 args, expected 1") + end) + + it('should return the single string argument as number', function () + assert.are_equal(5, itest_dsl.parse_number({"5"})) + end) - it('should return the single string argument as number', function () - assert.are_equal(5, parse_number({"5"})) end) - end) + describe('parse_vector', function () - describe('parse_vector', function () + it('should assert when the number of arguments is wrong', function () + assert.has_error(function () + itest_dsl.parse_vector({"too few"}) + end, "parse_vector: got 1 args, expected 2") + end) - it('should assert when the number of arguments is wrong', function () - assert.has_error(function () - parse_vector({"too few"}) - end, "parse_vector: got 1 args, expected 2") - end) + it('should return the 2 coordinate string arguments as vector', function () + assert.are_equal(vector(2, -3.5), itest_dsl.parse_vector({"2", "-3.5"})) + end) - it('should return the 2 coordinate string arguments as vector', function () - assert.are_equal(vector(2, -3.5), parse_vector({"2", "-3.5"})) end) - end) + describe('parse_horizontal_dir', function () + + it('should assert when the number of arguments is wrong', function () + assert.has_error(function () + itest_dsl.parse_horizontal_dir({"too", "many"}) + end, "parse_horizontal_dir: got 2 args, expected 1") + end) - describe('parse_horizontal_dir', function () + it('should return the single argument as horizontal direction', function () + assert.are_equal(horizontal_dirs.right, itest_dsl.parse_horizontal_dir({"right"})) + end) - it('should assert when the number of arguments is wrong', function () - assert.has_error(function () - parse_horizontal_dir({"too", "many"}) - end, "parse_horizontal_dir: got 2 args, expected 1") end) - it('should return the single argument as horizontal direction', function () - assert.are_equal(horizontal_dirs.right, parse_horizontal_dir({"right"})) - end) + describe('parse_motion_mode', function () - end) + it('should assert when the number of arguments is wrong', function () + assert.has_error(function () + itest_dsl.parse_motion_mode({"too", "many"}) + end, "parse_motion_mode: got 2 args, expected 1") + end) - describe('parse_motion_state', function () + it('should return the single argument as motion mode', function () + assert.are_equal(motion_modes.debug, itest_dsl.parse_motion_mode({"debug"})) + end) - it('should assert when the number of arguments is wrong', function () - assert.has_error(function () - parse_motion_state({"too", "many"}) - end, "parse_motion_state: got 2 args, expected 1") end) - it('should return the single argument as motion state', function () - assert.are_equal(motion_states.airborne, parse_motion_state({"airborne"})) - end) + describe('parse_motion_state', function () - end) + it('should assert when the number of arguments is wrong', function () + assert.has_error(function () + itest_dsl.parse_motion_state({"too", "many"}) + end, "parse_motion_state: got 2 args, expected 1") + end) - describe('parse_expect', function () + it('should return the single argument as motion state', function () + assert.are_equal(motion_states.airborne, itest_dsl.parse_motion_state({"airborne"})) + end) - it('should assert when the number of arguments is wrong', function () - assert.has_error(function () - parse_expect({"too few"}) - end, "parse_expect: got 1 args, expected at least 2") end) - it('should return the gameplay value type and the expected value, itself recursively parsed', function () - assert.are_same({gp_value_types.pc_bottom_pos, vector(1, 3)}, - {parse_expect({"pc_bottom_pos", "1", "3"})}) + describe('parse_expect', function () + + it('should assert when the number of arguments is wrong', function () + assert.has_error(function () + itest_dsl.parse_expect({"too few"}) + end, "parse_expect: got 1 args, expected at least 2") + end) + + it('should return the gameplay value type and the expected value, itself recursively parsed', function () + assert.are_same({gp_value_types.pc_bottom_pos, vector(1, 3)}, + {itest_dsl.parse_expect({"pc_bottom_pos", "1", "3"})}) + end) + end) end) @@ -147,7 +162,7 @@ describe('itest_dsl', function () end) it('should call warp_bottom_to on the current player character', function () - execute_warp({vector(1, 3)}) + itest_dsl.execute_warp({vector(1, 3)}) assert.spy(player_char.warp_bottom_to).was_called(1) assert.spy(player_char.warp_bottom_to).was_called_with(match.ref(stage.state.player_char), vector(1, 3)) @@ -155,10 +170,19 @@ describe('itest_dsl', function () end) + describe('execute_set_motion_mode', function () + + it('should set the motion mode', function () + itest_dsl.execute_set_motion_mode({motion_modes.debug}) + assert.are_equal(motion_modes.debug, stage.state.player_char.motion_mode) + end) + + end) + describe('execute_move', function () it('should set the move intention of the current player character to the directional unit vector matching his horizontal direction', function () - execute_move({horizontal_dirs.right}) + itest_dsl.execute_move({horizontal_dirs.right}) assert.are_equal(vector(1, 0), stage.state.player_char.move_intention) end) From b1e896b77818b2eee3f2e36f6c1c779ac75a2bb9 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 18 Jan 2019 21:17:28 +0000 Subject: [PATCH 029/140] [ASSERTIONS] Added eq_with_message used in itest_dsl to prepare almost_eq with busted --- src/engine/test/assertions.lua | 19 +++++++++++++++---- src/engine/test/itest_dsl.lua | 12 +++++------- tests/utestassertions.lua | 29 ++++++++++++++++++++--------- tests/utestitest_dsl.lua | 9 ++++++--- 4 files changed, 46 insertions(+), 23 deletions(-) diff --git a/src/engine/test/assertions.lua b/src/engine/test/assertions.lua index 8d3ab4fd..5dc25ba6 100644 --- a/src/engine/test/assertions.lua +++ b/src/engine/test/assertions.lua @@ -17,19 +17,30 @@ function contains_with_message(sequence, passed) local result = contains(sequence, passed) if result then -- passed is not contained, return false with does_not_contain message (will appear when using assert.is_false(contains_with_message())) - return true, "Expected object not to be one of the entries of the sequence.\nPassed in:\n"..nice_dump(passed).."\nSequence:\n"..nice_dump(sequence).."\n--- Ignore below ---" + return true, "Expected object not to be one of the entries of the sequence.\nPassed in:\n"..nice_dump(passed).."\nSequence:\n"..nice_dump(sequence) else - return false, "Expected object to be one of the entries of the sequence.\nPassed in:\n"..nice_dump(passed).."\nSequence:\n"..nice_dump(sequence).."\n--- Ignore below ---" + return false, "Expected object to be one of the entries of the sequence.\nPassed in:\n"..nice_dump(passed).."\nSequence:\n"..nice_dump(sequence) end end +-- imitation of busted equality check with message used in assert.are_equal +-- it returns a "inequality expected" message if expected == passed so we can use it to assert inequality as well +function eq_with_message(expected, passed) + if expected == passed then + return true, "Expected objects to not be equal.\nPassed in:\n"..nice_dump(passed).."\nDid not expect:\n"..nice_dump(expected) + else + return false, "Expected objects to be equal.\nPassed in:\n"..nice_dump(passed).."\nExpected:\n"..nice_dump(expected) + end +end + +-- same, but with almost equality function almost_eq_with_message(expected, passed, eps) eps = eps or 0.01 local result = almost_eq(expected, passed, eps) if result then - return true, "Expected objects not to be almost equal with eps: "..eps..".\nPassed in:\n"..nice_dump(passed).."\nExpected:\n"..nice_dump(expected).."\n--- Ignore below ---" + return true, "Expected objects to not be almost equal with eps: "..eps..".\nPassed in:\n"..nice_dump(passed).."\nDid not expect:\n"..nice_dump(expected) else - return false, "Expected objects to be almost equal with eps: "..eps..".\nPassed in:\n"..nice_dump(passed).."\nExpected:\n"..nice_dump(expected).."\n--- Ignore below ---" + return false, "Expected objects to be almost equal with eps: "..eps..".\nPassed in:\n"..nice_dump(passed).."\nExpected:\n"..nice_dump(expected) end end diff --git a/src/engine/test/itest_dsl.lua b/src/engine/test/itest_dsl.lua index ec96652f..fd98b006 100644 --- a/src/engine/test/itest_dsl.lua +++ b/src/engine/test/itest_dsl.lua @@ -27,6 +27,7 @@ expect gp_value_type expect a gameplay value to be equal to (...) --]] require("engine/core/helper") +require("engine/test/assertions") local integrationtest = require("engine/test/integrationtest") local itest_manager, integration_test = get_members(integrationtest, "itest_manager", "integration_test") @@ -552,16 +553,14 @@ function itest_dsl_parser:_define_final_assertion() local evaluator = evaluators[exp.gp_value_type] assert(evaluator, "evaluators["..exp.gp_value_type.."] (for '"..gp_value_type_strings[exp.gp_value_type].."') is not defined") local gp_value = evaluator() - if gp_value ~= exp.expected_value then + local value_success, value_eq_message = eq_with_message(exp.expected_value, gp_value) + if not value_success then success = false local gp_value_data = gp_value_data_t[exp.gp_value_type] assert(gp_value_data, "gp_value_data_t["..exp.gp_value_type.."] is not defined") local gp_value_name = gp_value_data.name - local message = "\nPassed gameplay value '"..gp_value_name.."':\n".. - gp_value.."\n".. - "Expected:\n".. - exp.expected_value - full_message = full_message..message.."\n" + local value_message = "\nFor gameplay value '"..gp_value_name.."':\n"..value_eq_message + full_message = full_message..value_message.."\n" end end @@ -569,5 +568,4 @@ function itest_dsl_parser:_define_final_assertion() end end - return itest_dsl diff --git a/tests/utestassertions.lua b/tests/utestassertions.lua index 09ad4b6e..cb74fec5 100644 --- a/tests/utestassertions.lua +++ b/tests/utestassertions.lua @@ -17,26 +17,37 @@ describe('assertions', function () describe('contains_with_message', function () it('should return (true, "Expected...") when the searched value is contained in the table', function () - local expected_message = "Expected object not to be one of the entries of the sequence.\nPassed in:\n2\nSequence:\n{[1] = 1, [2] = 2, [3] = 3}\n--- Ignore below ---" + local expected_message = "Expected object not to be one of the entries of the sequence.\nPassed in:\n2\nSequence:\n{[1] = 1, [2] = 2, [3] = 3}" assert.are_same({true, expected_message}, {contains_with_message({1, 2, 3}, 2)}) - local expected_message2 = "Expected object not to be one of the entries of the sequence.\nPassed in:\nvector(2, 4)\nSequence:\n".."{[1] = \"string\", [2] = vector(2, 4)}".."\n--- Ignore below ---" + local expected_message2 = "Expected object not to be one of the entries of the sequence.\nPassed in:\nvector(2, 4)\nSequence:\n".."{[1] = \"string\", [2] = vector(2, 4)}".."" assert.are_same({true, expected_message2}, {contains_with_message({"string", vector(2, 4)}, vector(2, 4))}) end) it('should return (false, "Expected...") when the searched value is not contained in the table', function () - local expected_message = "Expected object to be one of the entries of the sequence.\nPassed in:\n0\nSequence:\n{[1] = 1, [2] = 2, [3] = 3}\n--- Ignore below ---" + local expected_message = "Expected object to be one of the entries of the sequence.\nPassed in:\n0\nSequence:\n{[1] = 1, [2] = 2, [3] = 3}" assert.are_same({false, expected_message}, {contains_with_message({1, 2, 3}, 0)}) - local expected_message2 = "Expected object to be one of the entries of the sequence.\nPassed in:\nvector(2, 4)\nSequence:\n".."{[1] = \"string\", [2] = vector(2, 5)}".."\n--- Ignore below ---" + local expected_message2 = "Expected object to be one of the entries of the sequence.\nPassed in:\nvector(2, 4)\nSequence:\n".."{[1] = \"string\", [2] = vector(2, 5)}".."" assert.are_same({false, expected_message2}, {contains_with_message({"string", vector(2, 5)}, vector(2, 4))}) end) end) + describe('eq_with_message', function () + it('should return (true, "Expected...") when the passed value is equal to the expected one', function () + local expected_message = "Expected objects to not be equal.\nPassed in:\n2.4\nDid not expect:\n2.4" + assert.are_same({true, expected_message}, {eq_with_message(2.4, 2.4)}) + end) + it('should return (false, "Expected...") when the passed value is not equal to the expected one', function () + local expected_message = "Expected objects to be equal.\nPassed in:\n2.3\nExpected:\n2.4" + assert.are_same({false, expected_message}, {eq_with_message(2.4, 2.3)}) + end) + end) + describe('almost_eq_with_message', function () - it('should return (true, "") when the searched value is contained in the table', function () - local expected_message = "Expected objects not to be almost equal with eps: 0.01.\nPassed in:\n2.39\nExpected:\n2.4\n--- Ignore below ---" - assert.are_same({true, expected_message}, {almost_eq_with_message(2.4, 2.39)}) + it('should return (true, "Expected...") when the passed value is almost equal to the expected one', function () + local expected_message = "Expected objects to not be almost equal with eps: 0.01.\nPassed in:\n2.39\nDid not expect:\n2.4" + assert.are_same({true, expected_message}, {almost_eq_with_message(2.4, 2.39, 0.01)}) end) - it('should return (false, "Expected...") when the searched value is not contained in the table', function () - local expected_message = "Expected objects to be almost equal with eps: 0.001.\nPassed in:\n2.39\nExpected:\n2.4\n--- Ignore below ---" + it('should return (false, "Expected...") when the passed value is not almost equal to the expected one', function () + local expected_message = "Expected objects to be almost equal with eps: 0.001.\nPassed in:\n2.39\nExpected:\n2.4" assert.are_same({false, expected_message}, {almost_eq_with_message(2.4, 2.39, 0.001)}) end) end) diff --git a/tests/utestitest_dsl.lua b/tests/utestitest_dsl.lua index 30c4bd6b..46fe53cb 100644 --- a/tests/utestitest_dsl.lua +++ b/tests/utestitest_dsl.lua @@ -694,8 +694,10 @@ expect assert.are_equal(vector(-1, 0), stage.state.player_char.move_intention) -- we have not passed time so the character cannot have reached expected position - local expected_message = "\nPassed gameplay value 'player character bottom position':\nvector(12, 45)\nExpected:\nvector(10, 45)\n".. - "\nPassed gameplay value 'player character velocity':\nvector(0, 0)\nExpected:\nvector(2, -3.5)\n" + local expected_message = "\nFor gameplay value 'player character bottom position':\nExpected objects to be equal.\n".. + "Passed in:\nvector(12, 45)\nExpected:\nvector(10, 45)\n".. + "\nFor gameplay value 'player character velocity':\nExpected objects to be equal.\n".. + "Passed in:\nvector(0, 0)\nExpected:\nvector(2, -3.5)\n" assert.are_same({false, expected_message}, {test.final_assertion()}) -- but if we cheat and warp him on the spot, final assertion will work @@ -854,7 +856,8 @@ expect expectation(gp_value_types.pc_velocity, vector(-3, 7.5)) -- different from actual } itest_dsl_parser:_define_final_assertion() - local expected_message = "\nPassed gameplay value 'player character velocity':\nvector(-3, 2.5)\nExpected:\nvector(-3, 7.5)\n" + local expected_message = "\nFor gameplay value 'player character velocity':\nExpected objects to be equal.\n".. + "Passed in:\nvector(-3, 2.5)\nExpected:\nvector(-3, 7.5)\n" assert.are_same({false, expected_message}, {itest_dsl_parser._itest.final_assertion()}) end) From b1bfe7275806dc4dd6dd820880bceda2abb79b89 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 18 Jan 2019 21:29:50 +0000 Subject: [PATCH 030/140] [PREBUILD] Preprocess: warn if #pico8 block is not closed --- prebuild/preprocess.py | 6 ++++-- prebuild/test_preprocess.py | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/prebuild/preprocess.py b/prebuild/preprocess.py index b476d3fa..5b27e065 100644 --- a/prebuild/preprocess.py +++ b/prebuild/preprocess.py @@ -26,8 +26,8 @@ defined_symbols_table = { 'debug': ['pico8', 'assert', 'log', 'visual_logger', 'tuner', 'profiler', 'mouse', 'cheat'], 'assert': ['pico8', 'assert', 'log', 'visual_logger'], - 'itest': ['pico8', 'assert', 'log', 'test', 'cheat'], - 'itest_light': ['pico8', 'log', 'test'], + 'itest': ['pico8', 'assert', 'log', 'itest', 'cheat'], + 'itest_light': ['pico8', 'log', 'itest'], 'profiler': ['pico8', 'log', 'visual_logger', 'profiler'], 'visual_log': ['pico8', 'log', 'visual_logger'], 'pico8_utest': ['pico8', 'assert', 'log'], @@ -218,6 +218,8 @@ def preprocess_lines(lines, config): if if_block_modes_stack: logging.warning('file ended inside an --#if block. Make sure the block is closed by an --#endif directive') + if inside_pico8_block: + logging.warning('file ended inside a --[[#pico8 block. Make sure the block is closed by a --#pico8]] directive') return preprocessed_lines diff --git a/prebuild/test_preprocess.py b/prebuild/test_preprocess.py index 8aa38024..98b62150 100644 --- a/prebuild/test_preprocess.py +++ b/prebuild/test_preprocess.py @@ -392,6 +392,21 @@ def test_preprocess_lines_accepted_ifn_inside_pico8_block(self): ] self.assertEqual(preprocess.preprocess_lines(test_lines, 'release'), expected_processed_lines) + def test_preprocess_lines_missing_end_pico8_ignored(self): + test_lines = [ + 'print("start")\n', + '--[[#pico8 pico8 start\n', + 'real pico8 code\n', + 'print("end")\n', + ] + expected_processed_lines = [ + 'print("start")\n', + 'real pico8 code\n', + 'print("end")\n', + ] + # this will also trigger a warning, but we don't test it + self.assertEqual(preprocess.preprocess_lines(test_lines, 'release'), expected_processed_lines) + def test_preprocess_lines_with_unknown_config(self): test_lines = [] self.assertRaises(ValueError, preprocess.preprocess_lines, test_lines, 'unknown') From 26798d5df8e70dd0a09c970a5d2b0318baff44d3 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 18 Jan 2019 21:44:17 +0000 Subject: [PATCH 031/140] [PREBUILD] Preprocess: removed space before #pico8 that triggered unclosed block warning --- src/game/data/tile_data.lua | 2 +- src/game/itests/itestplayercharacter.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/game/data/tile_data.lua b/src/game/data/tile_data.lua index edb2403e..98d61a3b 100644 --- a/src/game/data/tile_data.lua +++ b/src/game/data/tile_data.lua @@ -31,5 +31,5 @@ function clear_map() --[[#pico8 -- clear map data memset(0x2000, 0, 0x1000) --- #pico8]] +--#pico8]] end diff --git a/src/game/itests/itestplayercharacter.lua b/src/game/itests/itestplayercharacter.lua index 9a202e6a..88e35338 100644 --- a/src/game/itests/itestplayercharacter.lua +++ b/src/game/itests/itestplayercharacter.lua @@ -1056,6 +1056,6 @@ end itest:add_action(time_trigger(1.), function () end) -- no final assertion, let the user check if result is correct or not (note it will display success whatever) --- #pico8]] +--#pico8]] --]] From 4d46a9338df8cde6ce0ad572ca575350579fa5af Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 18 Jan 2019 22:28:33 +0000 Subject: [PATCH 032/140] [ITEST] DSL: Test approximate equal for expected values in busted Fixed PICO-8 variant of itest playercharacter debug motion with exact final position --- src/engine/application/flow.lua | 2 +- src/engine/test/itest_dsl.lua | 12 +++++++ src/game/itests/itestplayercharacter.lua | 46 ++++-------------------- tests/pico8api.lua | 2 ++ tests/utestitest_dsl.lua | 7 ++-- 5 files changed, 25 insertions(+), 44 deletions(-) diff --git a/src/engine/application/flow.lua b/src/engine/application/flow.lua index 94cad3ea..74250e61 100644 --- a/src/engine/application/flow.lua +++ b/src/engine/application/flow.lua @@ -64,7 +64,7 @@ function flow:_change_state(new_gamestate) self.next_state = nil -- clear any gamestate query end ---#if test +--#if itest -- check if a new gamestate was queried, and enter it if so (convenient for itests) function flow:change_gamestate_by_type(gamestate_type) assert(self.gamestates[gamestate_type] ~= nil, "flow:change_gamestate_by_type: gamestate type '"..gamestate_type.."' has not been added to the flow gamestates") diff --git a/src/engine/test/itest_dsl.lua b/src/engine/test/itest_dsl.lua index fd98b006..da951525 100644 --- a/src/engine/test/itest_dsl.lua +++ b/src/engine/test/itest_dsl.lua @@ -553,7 +553,19 @@ function itest_dsl_parser:_define_final_assertion() local evaluator = evaluators[exp.gp_value_type] assert(evaluator, "evaluators["..exp.gp_value_type.."] (for '"..gp_value_type_strings[exp.gp_value_type].."') is not defined") local gp_value = evaluator() +--[[#pico8 + -- in pico8, we use fixed point precision, which is what we expect as final values + -- however, precomputing 16.16 fixed precision values by hand is very hard, + -- so I may end up using the same approx as with busted below local value_success, value_eq_message = eq_with_message(exp.expected_value, gp_value) +--#pico8]] +--#if utest + -- with busted, we use float point precision, which gives us slightly different values + -- unfortunately, the error accumulates over time, and position integrates from speed from accel, + -- so depending on the simulation time and the gameplay value type, the error threshold will vary + -- to be safe, we use 1/64 (0.015) although 1/256 is often enough) + local value_success, value_eq_message = almost_eq_with_message(exp.expected_value, gp_value, 1/64) +--#endif if not value_success then success = false local gp_value_data = gp_value_data_t[exp.gp_value_type] diff --git a/src/game/itests/itestplayercharacter.lua b/src/game/itests/itestplayercharacter.lua index 88e35338..126ae0f2 100644 --- a/src/game/itests/itestplayercharacter.lua +++ b/src/game/itests/itestplayercharacter.lua @@ -34,11 +34,7 @@ expect pc_ground_spd 0.703125 expect pc_velocity 0.703125 0 ]]) --- todo: --- eq (add = symbol before expected value) --- almost_eq (add ~ symbol before expected value, default to threshold: 1/256) - -itest_dsl_parser.register('#solo debug move right', [[ +itest_dsl_parser.register('debug move right', [[ @stage # .# @@ -47,44 +43,14 @@ warp 0 8 move right wait 60 -expect pc_bottom_pos 56.7185 8 +expect pc_bottom_pos 0x0038.b7f1 8 ]]) ---[[ - -itest = integration_test('debug move right', {stage.state.type}) -itest_manager:register(itest) - -itest.setup = function () - setup_map_data() - - -- just add a tile in the way to make sure debug motion ignores collisions - mset(1, 10, 64) - - flow:change_gamestate_by_type(stage.state.type) - stage.state.player_char.position = vector(0., 80.) - stage.state.player_char.control_mode = control_modes.puppet - stage.state.player_char.motion_mode = motion_modes.debug - - -- player char starts moving to the right - stage.state.player_char.move_intention = vector(1., 0.) -end - -itest.teardown = function () - clear_map() - teardown_map_data() -end - --- stop after 1 second -itest:add_action(time_trigger(1.), function () end) - --- check that player char has moved a little to the right (integrate accel) -itest.final_assertion = function () - -- 56.7185 in PICO-8 fixed point precision - -- 56.7333 in Lua floating point precision - return almost_eq_with_message(vector(56.7185, 80.), stage.state.player_char.position, 0.015) -end +-- precision note on expected pc_bottom_pos: +-- 56.7185211181640625 (0x0038.b7f1) in PICO-8 fixed point precision +-- 56.7333 in Lua floating point precision +--[[ -- bugfix history: -- . test failed because initial character position was wrong in the test diff --git a/tests/pico8api.lua b/tests/pico8api.lua index caf4917a..53007092 100644 --- a/tests/pico8api.lua +++ b/tests/pico8api.lua @@ -128,6 +128,8 @@ function tostr(val, hex) return tostring(val) end elseif kind == "boolean" then + -- this is even more precise that pico8 tostr, that will skip the last decimals (e.g. 1e-4 in 1+1e-4), + -- even if fixed point precision didn't lose them. but it's fine since it's mostly useful to debug failing tests return tostring(val) else return "[" .. kind .. "]" diff --git a/tests/utestitest_dsl.lua b/tests/utestitest_dsl.lua index 46fe53cb..096ccb30 100644 --- a/tests/utestitest_dsl.lua +++ b/tests/utestitest_dsl.lua @@ -694,9 +694,10 @@ expect assert.are_equal(vector(-1, 0), stage.state.player_char.move_intention) -- we have not passed time so the character cannot have reached expected position - local expected_message = "\nFor gameplay value 'player character bottom position':\nExpected objects to be equal.\n".. + -- note we are testing as busted, so we get the almost_eq messages + local expected_message = "\nFor gameplay value 'player character bottom position':\nExpected objects to be almost equal with eps: 0.015625.\n".. "Passed in:\nvector(12, 45)\nExpected:\nvector(10, 45)\n".. - "\nFor gameplay value 'player character velocity':\nExpected objects to be equal.\n".. + "\nFor gameplay value 'player character velocity':\nExpected objects to be almost equal with eps: 0.015625.\n".. "Passed in:\nvector(0, 0)\nExpected:\nvector(2, -3.5)\n" assert.are_same({false, expected_message}, {test.final_assertion()}) @@ -856,7 +857,7 @@ expect expectation(gp_value_types.pc_velocity, vector(-3, 7.5)) -- different from actual } itest_dsl_parser:_define_final_assertion() - local expected_message = "\nFor gameplay value 'player character velocity':\nExpected objects to be equal.\n".. + local expected_message = "\nFor gameplay value 'player character velocity':\nExpected objects to be almost equal with eps: 0.015625.\n".. "Passed in:\nvector(-3, 2.5)\nExpected:\nvector(-3, 7.5)\n" assert.are_same({false, expected_message}, {itest_dsl_parser._itest.final_assertion()}) end) From dc3e28cc9a3bd4dbfc4a1f77222dfb8b5589462b Mon Sep 17 00:00:00 2001 From: hsandt Date: Tue, 22 Jan 2019 00:22:36 +0000 Subject: [PATCH 033/140] [HELPER] Added string_toenum to avoid small error on negative numbers --- src/engine/core/helper.lua | 13 +++++++++++++ src/engine/test/itest_dsl.lua | 5 ++--- tests/pico8api.lua | 10 ++++++++++ tests/utesthelper.lua | 32 +++++++++++++++++++++++++++++++- tests/utestitest_dsl.lua | 2 +- tests/utestpico8api.lua | 28 ++++++++++++++++------------ 6 files changed, 73 insertions(+), 17 deletions(-) diff --git a/src/engine/core/helper.lua b/src/engine/core/helper.lua index e6d140bd..5bdbb88b 100644 --- a/src/engine/core/helper.lua +++ b/src/engine/core/helper.lua @@ -120,6 +120,19 @@ function invert_table(t) end --#endif +-- alternative to tonum that only works with strings (and numbers +-- thanks to sub converting them implicitly) +-- it fixes the 0x0000.0001 issue on negative number strings +function string_tonum(val) + -- inspired by cheepicus's workaround in + -- https://www.lexaloffle.com/bbs/?tid=3780 + if sub(val, 1, 1) == '-' then + return - tonum(sub(val, 2)) + else + return tonum(val) + end +end + --#if log function stringify(value) diff --git a/src/engine/test/itest_dsl.lua b/src/engine/test/itest_dsl.lua index da951525..5bc4d7ba 100644 --- a/src/engine/test/itest_dsl.lua +++ b/src/engine/test/itest_dsl.lua @@ -142,12 +142,12 @@ local gp_value_data_t = { function itest_dsl.parse_number(arg_strings) assert(#arg_strings == 1, "parse_number: got "..#arg_strings.." args, expected 1") - return tonum(arg_strings[1]) + return string_tonum(arg_strings[1]) end function itest_dsl.parse_vector(arg_strings) assert(#arg_strings == 2, "parse_vector: got "..#arg_strings.." args, expected 2") - return vector(tonum(arg_strings[1]), tonum(arg_strings[2])) + return vector(string_tonum(arg_strings[1]), string_tonum(arg_strings[2])) end function itest_dsl.parse_horizontal_dir(arg_strings) @@ -191,7 +191,6 @@ function itest_dsl.parse_expect(arg_strings) local expected_value_parser = value_parsers[gp_value_data.parsable_type] assert(expected_value_parser, "no value parser defined for gp value type '"..parsable_type_strings[gp_value_data.parsable_type].."'") local expected_value = expected_value_parser(expected_value_comps) - return gp_value_type, expected_value end diff --git a/tests/pico8api.lua b/tests/pico8api.lua index 53007092..95d0085c 100644 --- a/tests/pico8api.lua +++ b/tests/pico8api.lua @@ -110,10 +110,20 @@ end function cursor(x, y) end +-- convert string to number, preserve number +-- return nil if it fails to parse (unlike pico8 which returns custom value [no value]) +-- tonumber is even a little better than pico8 tonum which fails on negative number strings: +-- e.g. "-0.1875" -> 0xffff.d001 instead of 0xffff.d000 +-- https://www.lexaloffle.com/bbs/?tid=3780 +-- for this reason, we recommend not using tonum in actual pico8 code +-- and to use helper's tonum_precise instead function tonum(val) return tonumber(val) -- not a direct assignment to prevent usage of the radix argument end +-- http://pico-8.wikia.com/wiki/Tostr +-- slight difference with pico8: when passing the result of a function +-- that returns nothing, we return "[nil]" instead of "[no value]" function tostr(val, hex) local kind=type(val) if kind == "string" then diff --git a/tests/utesthelper.lua b/tests/utesthelper.lua index 4a3481a7..0f2da753 100644 --- a/tests/utesthelper.lua +++ b/tests/utesthelper.lua @@ -181,6 +181,37 @@ describe('invert_table', function () end) end) +describe('string_tonum', function () + it('"100" => 100', function () + assert.are_equal(100, string_tonum("100")) + end) + -- unlike tonum, this one works for both pico8 and native Lua + it('"-25.25" => -25.25', function () + assert.are_equal(-25.25, string_tonum("-25.25")) + end) + it('304.25 => 304.25', function () + assert.are_equal(304.25, string_tonum(304.25)) + end) + it('-25.25 => -25.25', function () + assert.are_equal(-25.25, string_tonum(-25.25)) + end) + it('nil => error', function () + assert.has_error(function () + sub(nil, 1, 1) + end) + end) + it('true => error', function () + assert.has_error(function () + sub(nil, 1, 1) + end) + end) + it('{} => error', function () + assert.has_error(function () + sub({}, 1, 1) + end) + end) +end) + describe('stringify', function () it('nil => "[nil]"', function () assert.are_equal("[nil]", stringify(nil)) @@ -211,7 +242,6 @@ describe('stringify', function () end assert.are_equal("[function]", stringify(f)) end) - end) describe('joinstr_table', function () diff --git a/tests/utestitest_dsl.lua b/tests/utestitest_dsl.lua index 096ccb30..f30c69fe 100644 --- a/tests/utestitest_dsl.lua +++ b/tests/utestitest_dsl.lua @@ -348,7 +348,7 @@ describe('itest_dsl', function () setup(function () stub(itest_dsl_parser, "parse_gamestate_definition", function (lines) - local tile_id = tonum(lines[3]) + local tile_id = string_tonum(lines[3]) return lines[1], lines[2], tilemap({ diff --git a/tests/utestpico8api.lua b/tests/utestpico8api.lua index 3390bcaa..44679c64 100644 --- a/tests/utestpico8api.lua +++ b/tests/utestpico8api.lua @@ -88,30 +88,34 @@ describe('pico8api', function () describe('tonum', function () it('should return the number corresponding to a number', function () - assert.are_equal(5, tonum(5)) + assert.are_equal(-25.34, tonum(-25.34)) end) - it('should return the number corresponding to a string', function () - assert.are_equal(5, tonum("5")) + it('should return the positive number corresponding to a string', function () + assert.are_equal(25, tonum("25")) end) - end) - - describe('tonum', function () - - it('should return the number corresponding to a number', function () - assert.are_equal(5, tonum(5)) + it('should return the negative number corresponding to a string (not fractional power of 2)', function () + assert.are_equal(-25.34, tonum("-25.34")) end) - it('should return the number corresponding to a string', function () - assert.are_equal(5, tonum("5")) + -- this one is for native Lua only: PICO-8 itself doesn't pass it + -- because tonum fails on negative number strings of 0x0000.0001! + it('should return the negative number corresponding to a string (fractional power of 2)', function () + assert.are_equal(-25.25, tonum("-25.25")) end) end) describe('tostr', function () it('nil => "[nil]"', function () - assert.are_equal("[nil]", tostr(nil)) + assert.are_equal("[nil]", tostr(nil)) -- or tostr() + end) + -- this one works for native Lua only; it differs from pico8 + -- which would return "[no value]", indicating a special value + it('empty function return => "[nil]"', function () + function f() end + assert.are_equal("[nil]", tostr(f())) end) it('"string" => "string"', function () assert.are_equal("string", tostr("string")) From dc4acf37899b80c2298ca04a2f1e534e827d20e0 Mon Sep 17 00:00:00 2001 From: hsandt Date: Tue, 22 Jan 2019 00:35:54 +0000 Subject: [PATCH 034/140] [ITEST] DSL: Converted decel and land itests to DSL --- src/game/itests/itestplayercharacter.lua | 212 +++++------------------ 1 file changed, 45 insertions(+), 167 deletions(-) diff --git a/src/game/itests/itestplayercharacter.lua b/src/game/itests/itestplayercharacter.lua index 126ae0f2..2158672f 100644 --- a/src/game/itests/itestplayercharacter.lua +++ b/src/game/itests/itestplayercharacter.lua @@ -16,9 +16,29 @@ local tile_test_data = require("game/test_data/tile_test_data") local itest +-- debug motion --- dsl definition +itest_dsl_parser.register('debug move right', [[ +@stage # +.# + +set_motion_mode debug +warp 0 8 +move right +wait 60 + +expect pc_bottom_pos 0x0038.b7f1 8 +]]) +-- precision note on expected pc_bottom_pos: +-- 56.7185211181640625 (0x0038.b7f1) in PICO-8 fixed point precision +-- 56.7333 in Lua floating point precision + + +-- ground motion + +-- bugfix history: +-- . test was wrong, initialize in setup, not at time trigger 0 itest_dsl_parser.register('platformer accel right flat', [[ @stage # ... @@ -34,187 +54,45 @@ expect pc_ground_spd 0.703125 expect pc_velocity 0.703125 0 ]]) -itest_dsl_parser.register('debug move right', [[ +itest_dsl_parser.register('#solo platformer decel right flat', [[ @stage # -.# +... +### -set_motion_mode debug -warp 0 8 +warp 4 8 move right -wait 60 +wait 30 +move left +wait 10 -expect pc_bottom_pos 0x0038.b7f1 8 +expect pc_bottom_pos 14.7109375 8 +expect pc_motion_state grounded +expect pc_ground_spd -0.1875 +expect pc_velocity -0.1875 0 ]]) --- precision note on expected pc_bottom_pos: --- 56.7185211181640625 (0x0038.b7f1) in PICO-8 fixed point precision --- 56.7333 in Lua floating point precision ---[[ +-- air motion -- bugfix history: -- . test failed because initial character position was wrong in the test -- * test failed in pico8 only because in _compute_signed_distance_to_closest_ground, --- I was setting min_signed_distance = 32768 = -32767 -itest = integration_test('platformer land vertical', {stage.state.type}) -itest_manager:register(itest) - -itest.setup = function () - setup_map_data() - - -- add tile where the character will land - mset(0, 10, 64) - - flow:change_gamestate_by_type(stage.state.type) - - -- respawn character in the air (important to always start with airborne state) - stage.state.player_char:spawn_at(vector(4., 48.)) - stage.state.player_char.control_mode = control_modes.puppet - stage.state.player_char.motion_mode = motion_modes.platformer -end - -itest.teardown = function () - clear_map() - teardown_map_data() -end - --- wait 1 second and stop -itest:add_action(time_trigger(1.), function () end) - --- check that player char has landed and snapped to the ground -itest.final_assertion = function () - return almost_eq_with_message(vector(4., 80.), stage.state.player_char:get_bottom_center(), 1/256) -end - - --- bugfix history: . test was wrong, initialize in setup, not at time trigger 0 -itest = integration_test('platformer accel right flat', {stage.state.type}) -itest_manager:register(itest) - -itest.setup = function () - setup_map_data() - - mset(0, 10, 64) - mset(1, 10, 64) - mset(2, 10, 64) - - flow:change_gamestate_by_type(stage.state.type) - - -- respawn character on the ground (important to always start with grounded state) - stage.state.player_char:spawn_at(vector(4., 80. - pc_data.center_height_standing)) -- set bottom y at 80 - stage.state.player_char.control_mode = control_modes.puppet - stage.state.player_char.motion_mode = motion_modes.platformer - - -- start moving to the right from frame 0 by setting intention in setup - stage.state.player_char.move_intention = vector(1, 0) -end - -itest.teardown = function () - clear_map() - teardown_map_data() -end - --- wait 30 frames and stop -itest:add_action(time_trigger(0.5), function () end) - --- check that player char has moved to the right and is still on the ground -itest.final_assertion = function () - local is_motion_state_expected, motion_state_message = motion_states.grounded == stage.state.player_char.motion_state, "Expected motion state 'grounded', got "..stage.state.player_char.motion_state - -- to compute position x from x0 after n frames at accel a from speed s0: x = x0 + n*s0 + n(n+1)/2*a - local is_position_expected, position_message = almost_eq_with_message(vector(14.8984375, 80.), stage.state.player_char:get_bottom_center(), 1/256) - -- to compute speed s from s0 after n frames at accel a: x = s0 + n*a - local is_ground_speed_expected, ground_speed_message = almost_eq_with_message(0.703125, stage.state.player_char.ground_speed, 1/256) - local is_velocity_expected, velocity_message = almost_eq_with_message(vector(0.703125, 0), stage.state.player_char.velocity, 1/256) - - local final_message = "" - - local success = is_position_expected and is_ground_speed_expected and is_velocity_expected and is_motion_state_expected - if not success then - if not is_motion_state_expected then - final_message = final_message..motion_state_message.."\n" - end - if not is_position_expected then - final_message = final_message..position_message.."\n" - end - if not is_ground_speed_expected then - final_message = final_message..ground_speed_message.."\n" - end - if not is_velocity_expected then - final_message = final_message..velocity_message.."\n" - end - - end - - return success, final_message -end - - -itest = integration_test('platformer decel right flat', {stage.state.type}) -itest_manager:register(itest) - -itest.setup = function () - setup_map_data() - - -- add tiles where the character will move - mset(0, 10, 64) - mset(1, 10, 64) - mset(2, 10, 64) - - flow:change_gamestate_by_type(stage.state.type) - - -- respawn character on the ground (important to always start with grounded state) - stage.state.player_char:spawn_at(vector(4., 80. - pc_data.center_height_standing)) -- set bottom y at 80 - stage.state.player_char.control_mode = control_modes.puppet - stage.state.player_char.motion_mode = motion_modes.platformer - - -- start moving to the right from frame 0 by setting intention in setup - stage.state.player_char.move_intention = vector(1, 0) -end - -itest.teardown = function () - clear_map() - teardown_map_data() -end - --- at frame 30, decelerate (brake) -itest:add_action(time_trigger(0.5), function () - stage.state.player_char.move_intention = vector(-1, 0) -end) - --- wait 10 frames and stop -itest:add_action(time_trigger(10, true), function () end) - --- check that player char has moved to the right and is still on the ground -itest.final_assertion = function () - local is_motion_state_expected, motion_state_message = motion_states.grounded == stage.state.player_char.motion_state, "Expected motion state 'grounded', got "..stage.state.player_char.motion_state - -- to compute position, apply deceleration to the current speed and sum to the last position at frame 30. don't forget to clamp speed to - max speed when changing sign over max speed, - -- before continuing to increase speed with - max accel each step after that - local is_position_expected, position_message = almost_eq_with_message(vector(14.7109375, 80.), stage.state.player_char:get_bottom_center(), 1/256) - local is_ground_speed_expected, ground_speed_message = almost_eq_with_message(-0.1875, stage.state.player_char.ground_speed, 1/256) - local is_velocity_expected, velocity_message = almost_eq_with_message(vector(-0.1875, 0), stage.state.player_char.velocity, 1/256) - - local final_message = "" +-- I was setting min_signed_distance = 32768 = -32767 +itest_dsl_parser.register('platformer land vertical', [[ +@stage # +. +. +. +# - local success = is_position_expected and is_ground_speed_expected and is_velocity_expected and is_motion_state_expected - if not success then - if not is_motion_state_expected then - final_message = final_message..motion_state_message.."\n" - end - if not is_position_expected then - final_message = final_message..position_message.."\n" - end - if not is_ground_speed_expected then - final_message = final_message..ground_speed_message.."\n" - end - if not is_velocity_expected then - final_message = final_message..velocity_message.."\n" - end +warp 4 0 +wait 21 - end +expect pc_bottom_pos 4 24 +]]) - return success, final_message -end +--[[ itest = integration_test('platformer friction right flat', {stage.state.type}) itest_manager:register(itest) From 2dbf48251dcd2abeb3a2473464f1277485da6697 Mon Sep 17 00:00:00 2001 From: hsandt Date: Thu, 24 Jan 2019 23:32:59 +0000 Subject: [PATCH 035/140] [README] Move "pico8 in path" instruction above --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0db66060..20b07574 100644 --- a/README.md +++ b/README.md @@ -56,13 +56,15 @@ It will install `luamin` (along with `luaparse`), which is used in `npm/luamin_f ### Build and run +First, make sure the `pico8` executable is in your path. + The most straightforward way to build and run the game on Unix platforms is: * `cd path/to/sonic-pico8-repo` * `./build.sh main release` * `./run.sh main release` -or, if `pico8` is not in your path: +Instead of the last instruction, you can also enter directly: * `pico8 -run build/sonic-pico8_v${BUILD_VERSION}_release.p8` From 429e6a048b802fb354edb75fa086740687f7610c Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 25 Jan 2019 00:09:47 +0000 Subject: [PATCH 036/140] [ITEST] DSL: Added parse_none and execute_stop --- prebuild/replace_strings.py | 18 ++++++++++-------- src/engine/test/itest_dsl.lua | 24 ++++++++++++++++++------ tests/utestitest_dsl.lua | 23 +++++++++++++++++++++++ 3 files changed, 51 insertions(+), 14 deletions(-) diff --git a/prebuild/replace_strings.py b/prebuild/replace_strings.py index 089d3900..2443ce72 100755 --- a/prebuild/replace_strings.py +++ b/prebuild/replace_strings.py @@ -123,20 +123,22 @@ # itest_dsl 'parsable_types': { - 'number': 1, - 'vector': 2, - 'horizontal_dir': 3, - 'motion_mode': 4, - 'motion_state': 5, - 'expect': 6, + 'none': 1, + 'number': 2, + 'vector': 3, + 'horizontal_dir': 4, + 'motion_mode': 5, + 'motion_state': 6, + 'expect': 7, }, 'command_types': { 'warp': 1, 'set_motion_mode': 2, 'move': 3, - 'wait': 4, - 'expect': 5, + 'stop': 4, + 'wait': 5, + 'expect': 6, }, 'gp_value_types': { diff --git a/src/engine/test/itest_dsl.lua b/src/engine/test/itest_dsl.lua index 5bc4d7ba..1cee4893 100644 --- a/src/engine/test/itest_dsl.lua +++ b/src/engine/test/itest_dsl.lua @@ -80,6 +80,7 @@ itest_dsl.generate_function_table = generate_function_table -- type of variables that can be parsed parsable_types = enum { + "none", "number", "vector", "horizontal_dir", @@ -95,12 +96,13 @@ parsable_type_strings = invert_table(parsable_types) -- type of commands available command_types = enum { - "warp", -- warp player character bottom args: {bottom_position: vector} - "set_motion_mode", -- set motion mode args: {motion_mode_str: motion_modes key} - "move", -- set sticky pc move intention args: {move_dir_str: horizontal_dirs key} - -- todo: stop, jump, crouch, spin_dash - "wait", -- wait some frames args: {frames: int} - "expect", -- expect a gameplay value args: {gp_value_type: gp_value_types, expected_args...: matching gp value parsable type} + "warp", -- warp player character bottom args: {bottom_position: vector} + "set_motion_mode", -- set motion mode args: {motion_mode_str: motion_modes key} + "move", -- set sticky pc move intention args: {move_dir_str: horizontal_dirs key} + "stop", -- stop moving horizontally args: {} + -- todo: jump, crouch, spin_dash + "wait", -- wait some frames args: {frames: int} + "expect", -- expect a gameplay value args: {gp_value_type: gp_value_types, expected_args...: matching gp value parsable type} } --#if assert @@ -112,6 +114,7 @@ command_arg_types = { [command_types.warp] = parsable_types.vector, [command_types.set_motion_mode] = parsable_types.motion_mode, [command_types.move] = parsable_types.horizontal_dir, + [command_types.stop] = parsable_types.none, [command_types.wait] = parsable_types.number, [command_types.expect] = parsable_types.expect, } @@ -140,6 +143,11 @@ local gp_value_data_t = { -- parsing functions +function itest_dsl.parse_none(arg_strings) + assert(#arg_strings == 0, "parse_none: got "..#arg_strings.." args, expected 0") + return nil +end + function itest_dsl.parse_number(arg_strings) assert(#arg_strings == 1, "parse_number: got "..#arg_strings.." args, expected 1") return string_tonum(arg_strings[1]) @@ -214,6 +222,10 @@ function itest_dsl.execute_move(args) stage.state.player_char.move_intention = horizontal_dir_vectors[args[1]] end +function itest_dsl.execute_stop(args) + stage.state.player_char.move_intention = vector.zero() +end + -- wait and expect are not timed actions and will be handled as special cases -- table of functions to call when applying a command with args, indexed by command type diff --git a/tests/utestitest_dsl.lua b/tests/utestitest_dsl.lua index f30c69fe..7ef980fe 100644 --- a/tests/utestitest_dsl.lua +++ b/tests/utestitest_dsl.lua @@ -52,6 +52,20 @@ describe('itest_dsl', function () describe('parse_', function () + describe('parse_none', function () + + it('should assert when the number of arguments is wrong', function () + assert.has_error(function () + itest_dsl.parse_none({"too many"}) + end, "parse_none: got 1 args, expected 0") + end) + + it('should return nil', function () + assert.is_nil(itest_dsl.parse_none({})) + end) + + end) + describe('parse_number', function () it('should assert when the number of arguments is wrong', function () @@ -188,6 +202,15 @@ describe('itest_dsl', function () end) + describe('execute_stop', function () + + it('should set the move intention of the current player character to vector zero', function () + itest_dsl.execute_stop({}) + assert.are_equal(vector.zero(), stage.state.player_char.move_intention) + end) + + end) + end) describe('eval_', function () From 4e8ce62d9eb1213774e7055c0886acb5e71da506 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 25 Jan 2019 00:11:15 +0000 Subject: [PATCH 037/140] [ITEST] Converted friction itest to DSL --- src/game/itests/itestplayercharacter.lua | 109 +++++++---------------- 1 file changed, 34 insertions(+), 75 deletions(-) diff --git a/src/game/itests/itestplayercharacter.lua b/src/game/itests/itestplayercharacter.lua index 2158672f..966ba285 100644 --- a/src/game/itests/itestplayercharacter.lua +++ b/src/game/itests/itestplayercharacter.lua @@ -18,7 +18,8 @@ local itest -- debug motion -itest_dsl_parser.register('debug move right', [[ +itest_dsl_parser.register( + 'debug move right', [[ @stage # .# @@ -39,7 +40,8 @@ expect pc_bottom_pos 0x0038.b7f1 8 -- bugfix history: -- . test was wrong, initialize in setup, not at time trigger 0 -itest_dsl_parser.register('platformer accel right flat', [[ +itest_dsl_parser.register( + 'platformer accel right flat', [[ @stage # ... ### @@ -54,7 +56,8 @@ expect pc_ground_spd 0.703125 expect pc_velocity 0.703125 0 ]]) -itest_dsl_parser.register('#solo platformer decel right flat', [[ +itest_dsl_parser.register( + 'platformer decel right flat', [[ @stage # ... ### @@ -71,6 +74,32 @@ expect pc_ground_spd -0.1875 expect pc_velocity -0.1875 0 ]]) +itest_dsl_parser.register( + 'platformer friction right flat', [[ +@stage # +.... +#### + +warp 4 8 +move right +wait 30 +stop +wait 30 + +expect pc_bottom_pos 25.09375 8 +expect pc_motion_state grounded +expect pc_ground_spd 0 +expect pc_velocity 0 0 +]]) + +-- calculation notes: +-- to compute position, use the fact that friction == accel, so our speed describes a pyramid over where each value is mirrored +-- around the middle, where the max is, except the max speed itself (0.703125) which is only reached a single frame +-- so we can 2x the accumulated distance computed in the first test (only accel over 30 frames), +-- then subtract the unique max value, and add the initial position x +-- expected position: vector(4 + 2 * 10.8984375 - 0.703125, 80.) = vector(25.09375, 80) +-- otherwise, character has stopped so expected speed is 0 + -- air motion @@ -78,7 +107,8 @@ expect pc_velocity -0.1875 0 -- . test failed because initial character position was wrong in the test -- * test failed in pico8 only because in _compute_signed_distance_to_closest_ground, -- I was setting min_signed_distance = 32768 = -32767 -itest_dsl_parser.register('platformer land vertical', [[ +itest_dsl_parser.register( + 'platformer land vertical', [[ @stage # . . @@ -92,77 +122,6 @@ expect pc_bottom_pos 4 24 ]]) ---[[ - -itest = integration_test('platformer friction right flat', {stage.state.type}) -itest_manager:register(itest) - -itest.setup = function () - setup_map_data() - - -- add tiles where the character will move - mset(0, 10, 64) - mset(1, 10, 64) - mset(2, 10, 64) - mset(3, 10, 64) - - flow:change_gamestate_by_type(stage.state.type) - - -- respawn character on the ground (important to always start with grounded state) - stage.state.player_char:spawn_at(vector(4., 80. - pc_data.center_height_standing)) -- set bottom y at 80 - stage.state.player_char.control_mode = control_modes.puppet - stage.state.player_char.motion_mode = motion_modes.platformer - - -- start moving to the right from frame 0 by setting intention in setup - stage.state.player_char.move_intention = vector(1, 0) -end - -itest.teardown = function () - clear_map() - teardown_map_data() -end - --- at frame 30, slow down with friction -itest:add_action(time_trigger(0.5), function () - stage.state.player_char.move_intention = vector.zero() -end) - --- wait 30 frames and stop -itest:add_action(time_trigger(0.5), function () end) - --- check that player char has moved to the right and is still on the ground -itest.final_assertion = function () - local is_motion_state_expected, motion_state_message = motion_states.grounded == stage.state.player_char.motion_state, "Expected motion state 'grounded', got "..stage.state.player_char.motion_state - -- to compute position, use the fact that friction == accel, so our speed describes a pyramid over time with a non-mirrored, unique max at 0.703125, - -- so we can 2x the accumulated distance computed in the first test (only accel over 30 frames), then subtract the non-doubled max value, and add the initial position x - local is_position_expected, position_message = almost_eq_with_message(vector(4 + 2 * 10.8984375 - 0.703125, 80.), stage.state.player_char:get_bottom_center(), 1/256) - -- to compute speed s from s0 after n frames at accel a: x = s0 + n*a - local is_ground_speed_expected, ground_speed_message = almost_eq_with_message(0, stage.state.player_char.ground_speed, 1/256) - local is_velocity_expected, velocity_message = almost_eq_with_message(vector(0, 0), stage.state.player_char.velocity, 1/256) - - local final_message = "" - - local success = is_position_expected and is_ground_speed_expected and is_velocity_expected and is_motion_state_expected - if not success then - if not is_motion_state_expected then - final_message = final_message..motion_state_message.."\n" - end - if not is_position_expected then - final_message = final_message..position_message.."\n" - end - if not is_ground_speed_expected then - final_message = final_message..ground_speed_message.."\n" - end - if not is_velocity_expected then - final_message = final_message..velocity_message.."\n" - end - - end - - return success, final_message -end ---]] - -- bugfix history: -- . forgot to add a solid ground below the slope to confirm ground -- ! identified bug in _compute_ground_motion_result where slope angle was set on extra step, From dd33b80adb8396b9c386f5ca6e8eb5f9eba9d649 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 8 Feb 2019 21:47:27 +0000 Subject: [PATCH 038/140] [ITEST] DSL: Added gp value: pc_slope --- src/engine/test/itest_dsl.lua | 6 ++++++ src/game/data/tile_data.lua | 1 + tests/utestitest_dsl.lua | 13 +++++++++++-- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/engine/test/itest_dsl.lua b/src/engine/test/itest_dsl.lua index 1cee4893..7aebfbc4 100644 --- a/src/engine/test/itest_dsl.lua +++ b/src/engine/test/itest_dsl.lua @@ -126,6 +126,7 @@ gp_value_types = enum { "pc_velocity", -- velocity of player character "pc_ground_spd", -- ground speed of player character "pc_motion_state", -- motion state of player character + "pc_slope", -- current slope on which player character is grounded } --#if assert @@ -138,6 +139,7 @@ local gp_value_data_t = { [gp_value_types.pc_velocity] = gameplay_value_data("player character velocity", parsable_types.vector), [gp_value_types.pc_ground_spd] = gameplay_value_data("player character ground speed", parsable_types.number), [gp_value_types.pc_motion_state] = gameplay_value_data("player character motion state", parsable_types.motion_state), + [gp_value_types.pc_slope] = gameplay_value_data("player character slope", parsable_types.number), } @@ -251,6 +253,10 @@ function itest_dsl.eval_pc_motion_state() return stage.state.player_char.motion_state end +function itest_dsl.eval_pc_slope() + return stage.state.player_char.slope_angle +end + -- table of functions used to evaluate and returns the gameplay value in current game state evaluators = generate_function_table(itest_dsl, gp_value_types, "eval_") itest_dsl.evaluators = evaluators diff --git a/src/game/data/tile_data.lua b/src/game/data/tile_data.lua index 98d61a3b..2c4ee32a 100644 --- a/src/game/data/tile_data.lua +++ b/src/game/data/tile_data.lua @@ -5,6 +5,7 @@ local tile_test_data = require("game/test_data/tile_test_data") tile_symbol_to_ids = { ['.'] = 0, -- empty ['#'] = 64, -- full tile + ['/'] = 65, -- ascending slope 45 } -- for itests that need map setup, we exceptionally not teardown diff --git a/tests/utestitest_dsl.lua b/tests/utestitest_dsl.lua index 7ef980fe..3cd43ee4 100644 --- a/tests/utestitest_dsl.lua +++ b/tests/utestitest_dsl.lua @@ -4,8 +4,8 @@ require("engine/core/math") local itest_dsl = require("engine/test/itest_dsl") local gameplay_value_data, generate_function_table = get_members(itest_dsl, "gameplay_value_data", "generate_function_table") -local eval_pc_bottom_pos, eval_pc_velocity, eval_pc_ground_spd, eval_pc_motion_state = get_members(itest_dsl, - "eval_pc_bottom_pos", "eval_pc_velocity", "eval_pc_ground_spd", "eval_pc_motion_state") +local eval_pc_bottom_pos, eval_pc_velocity, eval_pc_ground_spd, eval_pc_motion_state, eval_pc_slope = get_members(itest_dsl, + "eval_pc_bottom_pos", "eval_pc_velocity", "eval_pc_ground_spd", "eval_pc_motion_state", "eval_pc_slope") local command, expectation = get_members(itest_dsl, "command", "expectation") local dsl_itest, itest_dsl_parser = get_members(itest_dsl, @@ -261,6 +261,15 @@ describe('itest_dsl', function () end) + describe('eval_pc_slope', function () + + it('should return the ground speed current player character', function () + stage.state.player_char.slope_angle = -0.125 + assert.are_equal(-0.125, eval_pc_slope()) + end) + + end) + end) From dceaba2bd3b00eb7f91891d9b501c12533ca2525 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 8 Feb 2019 21:48:08 +0000 Subject: [PATCH 039/140] [ITEST] Converted ascending slope 45 itest to DSL --- src/game/itests/itestplayercharacter.lua | 100 ++++++----------------- 1 file changed, 27 insertions(+), 73 deletions(-) diff --git a/src/game/itests/itestplayercharacter.lua b/src/game/itests/itestplayercharacter.lua index 966ba285..6e601543 100644 --- a/src/game/itests/itestplayercharacter.lua +++ b/src/game/itests/itestplayercharacter.lua @@ -127,85 +127,39 @@ expect pc_bottom_pos 4 24 -- ! identified bug in _compute_ground_motion_result where slope angle was set on extra step, -- despite being only a subpixel extra move -- . was expecting positive speed but slope was ascending -itest = integration_test('platformer ascending slope right', {stage.state.type}) -itest_manager:register(itest) - -itest.setup = function () - setup_map_data() - - -- add tiles where the character will move - mset(0, 10, 64) -- flat ground - mset(1, 9, 65) -- ascending slope 45, one level up - - flow:change_gamestate_by_type(stage.state.type) +itest_dsl_parser.register( + 'platformer ascending slope right', [[ +@stage # +.. +./ +#. - -- respawn character on the ground (important to always start with grounded state) - stage.state.player_char:spawn_at(vector(4., 80. - pc_data.center_height_standing)) -- set bottom y at 80 - stage.state.player_char.control_mode = control_modes.puppet - stage.state.player_char.motion_mode = motion_modes.platformer +warp 4 16 +move right +wait 15 - -- start moving to the right from frame 0 by setting intention in setup - stage.state.player_char.move_intention = vector(1, 0) -end +expect pc_bottom_pos 6.519668501758 15 +expect pc_motion_state grounded +expect pc_slope -0.125 +expect pc_ground_spd 0.26318359375 +expect pc_velocity 0.1860961140625 -0.1860961140625 +]]) -itest.teardown = function () - clear_map() - teardown_map_data() -end +-- pc_slope -45/360 = -1/8 = -0.125 --- wait 30 frames and stop --- ground_accel_frame2 = 0.0234375 --- at frame 1: bottom pos (4 + ground_accel_frame2, 80), velocity (ground_accel_frame2, 0), ground_speed (ground_accel_frame2) --- at frame n before slope: bpos (4 + n(n+1)/2*ground_accel_frame2, 80), velocity (n*ground_accel_frame2, 0) +-- calculation notes: +-- at frame 1: bottom pos (4 + ground_accel_frame2, 16), velocity (ground_accel_frame2, 0), ground_speed (ground_accel_frame2) +-- at frame n before slope: bpos (4 + n(n+1)/2*ground_accel_frame2, 16), velocity (n*ground_accel_frame2, 0) -- character makes first step on slope when right sensor reaches position x = 8 (column 0 height of tile 65 is 1) -- i.e. center reaches 8 - ground_sensor_extent_x = 5.5 --- at frame 1: bpos (4.0234375, 80), velocity (0.0234375, 0), ground_speed(0.0234375) --- at frame 9: bpos (5.0546875, 80), velocity (0.2109375, 0), ground_speed(0.2109375) --- at frame 10: bpos (5.2890625, 80), velocity (0.234375, 0), ground_speed(0.234375) --- at frame 11: bpos (5.546875, 80), velocity (0.2578125, 0), ground_speed(0.2578125) --- at frame 12: bpos (5.828125, 80), velocity (0.28125, 0), ground_speed(0.28125) --- at frame 13: bpos (6.1328125, 79), velocity (0.3046875, 0), ground_speed(0.3046875), first step on slope and at higher level than flat ground, acknowledge slope as current ground --- at frame 14: bpos (6.333572387695, 79), velocity (0.2007598876953125, -0.2007598876953125), ground_speed(0.283935546875), because slope was current ground at frame start, slope factor was applied with 0.0625*sin(45) = -0.044189453125 (in PICO-8 16.16 fixed point precision) --- at frame 15: bpos (6.519668501758, 79), velocity (0.1860961140625, -0.1860961140625), ground_speed(0.26318359375), still under slope factor effect and velocity following slope tangent --- note that speed decrease on slope is not implemented yet (via cosine but also gravity), so this test will have to change when it is --- however, the result should stay true for a very low slope (a wave where registered slope is 0) -itest:add_action(time_trigger(15, true), function () end) - --- check that player char has moved to the right and is still on the ground -itest.final_assertion = function () - local is_motion_state_expected, motion_state_message = motion_states.grounded == stage.state.player_char.motion_state, "Expected motion state 'grounded', got "..stage.state.player_char.motion_state - -- to compute position, use the fact that friction == accel, so our speed describes a pyramid over time with a non-mirrored, unique max at 0.703125, - -- so we can 2x the accumulated distance computed in the first test (only accel over 30 frames), then subtract the non-doubled max value, and add the initial position x - local is_position_expected, position_message = almost_eq_with_message(vector(6.519668501758, 79), stage.state.player_char:get_bottom_center(), 1/256) - local is_slope_expected, slope_message = almost_eq_with_message(-45/360, stage.state.player_char.slope_angle, 1/256) - -- to compute speed s from s0 after n frames at accel a: x = s0 + n*a - local is_ground_speed_expected, ground_speed_message = almost_eq_with_message(0.26318359375, stage.state.player_char.ground_speed, 1/256) - local is_velocity_expected, velocity_message = almost_eq_with_message(vector(0.1860961140625, -0.1860961140625), stage.state.player_char.velocity, 1/256) - - local final_message = "" - - local success = is_position_expected and is_ground_speed_expected and is_velocity_expected and is_motion_state_expected - if not success then - if not is_motion_state_expected then - final_message = final_message.."motion_state: \n"..motion_state_message.."\n" - end - if not is_position_expected then - final_message = final_message.."position_message: \n"..position_message.."\n" - end - if not is_slope_expected then - final_message = final_message.."slope_message: \n"..slope_message.."\n" - end - if not is_ground_speed_expected then - final_message = final_message.."ground_speed_message: \n"..ground_speed_message.."\n" - end - if not is_velocity_expected then - final_message = final_message.."velocity_message: \n"..velocity_message.."\n" - end - - end - - return success, final_message -end +-- at frame 1: bpos (4.0234375, 16), velocity (0.0234375, 0), ground_speed(0.0234375) +-- at frame 9: bpos (5.0546875, 16), velocity (0.2109375, 0), ground_speed(0.2109375) +-- at frame 10: bpos (5.2890625, 16), velocity (0.234375, 0), ground_speed(0.234375) +-- at frame 11: bpos (5.546875, 16), velocity (0.2578125, 0), ground_speed(0.2578125) +-- at frame 12: bpos (5.828125, 16), velocity (0.28125, 0), ground_speed(0.28125) +-- at frame 13: bpos (6.1328125, 15), velocity (0.3046875, 0), ground_speed(0.3046875), first step on slope and at higher level than flat ground, acknowledge slope as current ground +-- at frame 14: bpos (6.333572387695, 15), velocity (0.2007598876953125, -0.2007598876953125), ground_speed(0.283935546875), because slope was current ground at frame start, slope factor was applied with 0.0625*sin(45) = -0.044189453125 (in PICO-8 16.16 fixed point precision) +-- at frame 15: bpos (6.519668501758, 15), velocity (0.1860961140625, -0.1860961140625), ground_speed(0.26318359375), still under slope factor effect and velocity following slope tangent --[[ -- bugfix history: ! identified bug in _update_platformer_motion where absence of elseif From 43382acc4d3fd8f44d3bad62ecd2969adef6a1f7 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 8 Feb 2019 22:53:47 +0000 Subject: [PATCH 040/140] [ITEST] Chain actions immediately if the next action has waiting time of 0 --- src/engine/test/integrationtest.lua | 8 +- tests/utestintegrationtest.lua | 124 ++++++++++++++++++++++++++-- 2 files changed, 126 insertions(+), 6 deletions(-) diff --git a/src/engine/test/integrationtest.lua b/src/engine/test/integrationtest.lua index 28389a80..0bee63f3 100644 --- a/src/engine/test/integrationtest.lua +++ b/src/engine/test/integrationtest.lua @@ -255,6 +255,9 @@ end function integration_test_runner:_check_next_action() assert(self._next_action_index <= #self.current_test.action_sequence, "self._next_action_index ("..self._next_action_index..") is out of bounds for self.current_test.action_sequence (size "..#self.current_test.action_sequence..")") + -- test: chain actions with no intervals between them + local should_trigger_next_action + repeat -- check if next action should be applied local next_action = self.current_test.action_sequence[self._next_action_index] local should_trigger_next_action = next_action.trigger:_check(self.current_frame - self._last_trigger_frame) @@ -265,8 +268,11 @@ function integration_test_runner:_check_next_action() end self._last_trigger_frame = self.current_frame self._next_action_index = self._next_action_index + 1 - self:_check_end() + if self:_check_end() then + break + end end + until not should_trigger_next_action end function integration_test_runner:_check_end() diff --git a/tests/utestintegrationtest.lua b/tests/utestintegrationtest.lua index 7544afc9..f4e8c63f 100644 --- a/tests/utestintegrationtest.lua +++ b/tests/utestintegrationtest.lua @@ -657,20 +657,22 @@ describe('integration_test_runner', function () end) - describe('_check_next_action (with single action)', function () + describe('_check_next_action', function () describe('(with dummy action after 1s)', function () local action_callback = spy.new(function () end) + local action_callback2 = spy.new(function () end) before_each(function () - test:add_action(time_trigger(1.0), action_callback, '_check_next_action_test_action') + test:add_action(time_trigger(1.0), action_callback, 'action_callback') -- don't stub a function if the return value matters, as in start spy.on(integration_test_runner, "_check_end") end) teardown(function () action_callback:revert() + action_callback2:revert() integration_test_runner._check_end:revert() end) @@ -680,6 +682,7 @@ describe('integration_test_runner', function () after_each(function () action_callback:clear() + action_callback2:clear() integration_test_runner._check_end:clear() end) @@ -695,7 +698,6 @@ describe('integration_test_runner', function () -- time trigger uses relative frames, so compare the difference since last trigger to 60 integration_test_runner.current_frame = 158 integration_test_runner._last_trigger_frame = 100 - test:add_action(time_trigger(1.0), action_callback, '_check_next_action_test_action') end) it('should not call the action nor advance the time/index', function () @@ -715,7 +717,6 @@ describe('integration_test_runner', function () -- time trigger uses relative frames, so compare the difference since last trigger to 60 integration_test_runner.current_frame = 160 integration_test_runner._last_trigger_frame = 100 - test:add_action(time_trigger(1.0), action_callback, '_check_next_action_test_action') end) it('should call the action and advance the timeindex', function () @@ -749,6 +750,119 @@ describe('integration_test_runner', function () end) + describe('(with 2nd dummy action immediately after the other)', function () + + describe('(when next action index is 1/1)', function () + + before_each(function () + integration_test_runner._next_action_index = 1 + end) + + describe('(when next action time trigger is not reached yet)', function () + + before_each(function () + -- time trigger uses relative frames, so compare the difference since last trigger to 60 + test:add_action(time_trigger(0.0), action_callback2, 'action_callback2') + integration_test_runner.current_frame = 158 + integration_test_runner._last_trigger_frame = 100 + end) + + it('should not call any actions nor advance the time/index', function () + integration_test_runner._check_end:clear() -- was called on start in before_each + integration_test_runner:_check_next_action() + assert.spy(action_callback).was_not_called() + assert.spy(action_callback2).was_not_called() + assert.are_equal(100, integration_test_runner._last_trigger_frame) + assert.are_equal(1, integration_test_runner._next_action_index) + assert.spy(integration_test_runner._check_end).was_not_called() + end) + + end) + + describe('(when next action time trigger is reached)', function () + + before_each(function () + -- time trigger uses relative frames, so compare the difference since last trigger to 60 + test:add_action(time_trigger(0.0), action_callback2, 'action_callback2') + integration_test_runner.current_frame = 160 + integration_test_runner._last_trigger_frame = 100 + end) + + it('should call both actions and advance the timeindex by 2', function () + integration_test_runner._check_end:clear() -- was called on start in before_each + integration_test_runner:_check_next_action() + assert.spy(action_callback).was_called(1) + assert.spy(action_callback).was_called_with() + assert.spy(action_callback2).was_called(1) -- thx to action chaining when next action time is 0 + assert.spy(action_callback2).was_called_with() + assert.are_equal(160, integration_test_runner._last_trigger_frame) + assert.are_equal(3, integration_test_runner._next_action_index) -- after action 2 + assert.spy(integration_test_runner._check_end).was_called(2) -- checked after each action + assert.spy(integration_test_runner._check_end).was_called_with(match.ref(integration_test_runner)) + end) + + end) + + end) + + end) + + describe('(with 2nd dummy action some frames after the other)', function () + + describe('(when next action index is 1/1)', function () + + before_each(function () + integration_test_runner._next_action_index = 1 + end) + + describe('(when next action time trigger is not reached yet)', function () + + before_each(function () + -- time trigger uses relative frames, so compare the difference since last trigger to 60 + test:add_action(time_trigger(0.2), action_callback2, 'action_callback2') + integration_test_runner.current_frame = 158 + integration_test_runner._last_trigger_frame = 100 + end) + + it('should not call any actions nor advance the time/index', function () + integration_test_runner._check_end:clear() -- was called on start in before_each + integration_test_runner:_check_next_action() + assert.spy(action_callback).was_not_called() + assert.spy(action_callback2).was_not_called() + assert.are_equal(100, integration_test_runner._last_trigger_frame) + assert.are_equal(1, integration_test_runner._next_action_index) + assert.spy(integration_test_runner._check_end).was_not_called() + end) + + end) + + describe('(when next action time trigger is reached)', function () + + before_each(function () + -- time trigger uses relative frames, so compare the difference since last trigger to 60 + test:add_action(time_trigger(0.2), action_callback2, 'action_callback2') + integration_test_runner.current_frame = 160 + integration_test_runner._last_trigger_frame = 100 + end) + + it('should call only the first action and advance the timeindex', function () + integration_test_runner._check_end:clear() -- was called on start in before_each + integration_test_runner:_check_next_action() + assert.spy(action_callback).was_called(1) + assert.spy(action_callback).was_called_with() + assert.spy(action_callback2).was_not_called() -- at least 1 frame before action2, no action chaining + assert.are_equal(160, integration_test_runner._last_trigger_frame) + assert.are_equal(2, integration_test_runner._next_action_index) + assert.spy(integration_test_runner._check_end).was_called(1) + assert.spy(integration_test_runner._check_end).was_called_with(match.ref(integration_test_runner)) + end) + + end) + + end) + + end) + end) describe('(with empty action)', function () @@ -758,7 +872,7 @@ describe('integration_test_runner', function () test:add_action(time_trigger(1, true), nil, 'empty action') end) - it('should assert', function () + it('should recognize next empty action and do nothing', function () integration_test_runner:start(test) integration_test_runner.current_frame = 2 -- to trigger action to do at end of frame 1 From 2e1be1c3965b3109e699861b9d4e96e1bfc29583 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 8 Feb 2019 22:55:54 +0000 Subject: [PATCH 041/140] [ITEST] Converted "platformer fall cliff" to DSL Tweaked waiting frames to match new flooring pixel position x behavior Chained actions allow to warp and start moving from frame 1 (previous itests still worked because everything was offset by 1 frame, including expectations) --- src/game/itests/itestplayercharacter.lua | 127 +++++++++-------------- 1 file changed, 48 insertions(+), 79 deletions(-) diff --git a/src/game/itests/itestplayercharacter.lua b/src/game/itests/itestplayercharacter.lua index 6e601543..f5d95965 100644 --- a/src/game/itests/itestplayercharacter.lua +++ b/src/game/itests/itestplayercharacter.lua @@ -101,27 +101,6 @@ expect pc_velocity 0 0 -- otherwise, character has stopped so expected speed is 0 --- air motion - --- bugfix history: --- . test failed because initial character position was wrong in the test --- * test failed in pico8 only because in _compute_signed_distance_to_closest_ground, --- I was setting min_signed_distance = 32768 = -32767 -itest_dsl_parser.register( - 'platformer land vertical', [[ -@stage # -. -. -. -# - -warp 4 0 -wait 21 - -expect pc_bottom_pos 4 24 -]]) - - -- bugfix history: -- . forgot to add a solid ground below the slope to confirm ground -- ! identified bug in _compute_ground_motion_result where slope angle was set on extra step, @@ -161,75 +140,65 @@ expect pc_velocity 0.1860961140625 -0.1860961140625 -- at frame 14: bpos (6.333572387695, 15), velocity (0.2007598876953125, -0.2007598876953125), ground_speed(0.283935546875), because slope was current ground at frame start, slope factor was applied with 0.0625*sin(45) = -0.044189453125 (in PICO-8 16.16 fixed point precision) -- at frame 15: bpos (6.519668501758, 15), velocity (0.1860961140625, -0.1860961140625), ground_speed(0.26318359375), still under slope factor effect and velocity following slope tangent ---[[ --- bugfix history: ! identified bug in _update_platformer_motion where absence of elseif --- allowed to enter both grounded and airborne update, causing 2x update when leaving the cliff -itest = integration_test('platformer fall cliff', {stage.state.type}) -itest_manager:register(itest) -itest.setup = function () - setup_map_data() - - -- add tiles where the character will move - mset(0, 10, 64) - mset(1, 10, 64) - - flow:change_gamestate_by_type(stage.state.type) - - -- respawn character on the ground (important to always start with grounded state) - stage.state.player_char:spawn_at(vector(4., 80. - pc_data.center_height_standing)) -- set bottom y at 80 - stage.state.player_char.control_mode = control_modes.puppet - stage.state.player_char.motion_mode = motion_modes.platformer - - -- start moving to the right from frame 0 by setting intention in setup - stage.state.player_char.move_intention = vector(1, 0) -end +-- air motion -itest.teardown = function () - clear_map() - teardown_map_data() -end +-- bugfix history: +-- . test failed because initial character position was wrong in the test +-- * test failed in pico8 only because in _compute_signed_distance_to_closest_ground, +-- I was setting min_signed_distance = 32768 = -32767 +itest_dsl_parser.register( + 'platformer land vertical', [[ +@stage # +. +. +. +# --- at frame 34: pos (17.9453125, 74), velocity (0.796875, 0), grounded --- at frame 35: pos (18.765625, 74), velocity (0.8203125, 0), airborne -> stop accel -itest:add_action(time_trigger(35, true), function () - stage.state.player_char.move_intention = vector.zero() -end) +warp 4 0 +wait 21 --- wait 25 frames and stop --- at frame 60: pos (39.2734375, 74 + 35.546875), velocity (0.8203125, 2.734375), airborne -itest:add_action(time_trigger(25, true), function () end) +expect pc_bottom_pos 4 24 +expect pc_motion_state grounded +expect pc_ground_spd 0 +expect pc_velocity 0 0 +]]) --- check that player char has moved to the right and fell -itest.final_assertion = function () - local is_motion_state_expected, motion_state_message = motion_states.airborne == stage.state.player_char.motion_state, "Expected motion state 'airborne', got "..stage.state.player_char.motion_state - local is_position_expected, position_message = almost_eq_with_message(vector(39.2734375, 80. + 35.546875), stage.state.player_char:get_bottom_center(), 1/256) - local is_ground_speed_expected, ground_speed_message = almost_eq_with_message(0, stage.state.player_char.ground_speed, 1/256) - local is_velocity_expected, velocity_message = almost_eq_with_message(vector(0.8203125, 2.734375), stage.state.player_char.velocity, 1/256) - local final_message = "" +-- bugfix history: +-- ! identified bug in _update_platformer_motion where absence of elseif +-- allowed to enter both grounded and airborne update, causing 2x update when leaving the cliff +-- * revealed that new system always flooring pixel position x caused leaving cliff +-- frame later, adding a grounded frame with friction +itest_dsl_parser.register( + 'platformer fall cliff', [[ +@stage # +.. +## - local success = is_position_expected and is_ground_speed_expected and is_velocity_expected and is_motion_state_expected - if not success then - if not is_motion_state_expected then - final_message = final_message..motion_state_message.."\n" - end - if not is_position_expected then - final_message = final_message..position_message.."\n" - end - if not is_ground_speed_expected then - final_message = final_message..ground_speed_message.."\n" - end - if not is_velocity_expected then - final_message = final_message..velocity_message.."\n" - end +warp 4 8 +move right +wait 36 +stop +wait 24 - end +expect pc_bottom_pos 39.859375 40.8125 +expect pc_motion_state airborne +expect pc_ground_spd 0 +expect pc_velocity 0.84375 2.625 +]]) - return success, final_message -end +-- calculation notes: +-- at frame 1: pos (17.9453125, 8), velocity (0.796875, 0), grounded +-- at frame 34: pos (17.9453125, 8), velocity (0.796875, 0), grounded +-- at frame 35: pos (18.765625, 8), velocity (0.8203125, 0), grounded (do not apply ground sensor extent: -2.5 directly, floor to full px first) +-- at frame 36: pos (19.609375, 8), velocity (0.84375, 0), airborne (flr_x=19) -> stop accel +-- wait 24 frames and stop +-- gravity during 24 frames: accel = 0.109375 * (24 * 25 / 2), velocity = 0.109375 * 24 = 2.625 +-- at frame 60: pos (39.859375, 8 + 32.8125), velocity (0.84375, 2.625), airborne +--[[ itest = integration_test('platformer hop flat', {stage.state.type}) itest_manager:register(itest) From 30b1d516e4dab47628b2ee632768d75cfcc178c9 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 8 Feb 2019 23:32:14 +0000 Subject: [PATCH 042/140] [TEST] Support numbers with more than 4 hex figures after point in tostr --- tests/pico8api.lua | 5 ++++- tests/utestpico8api.lua | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/pico8api.lua b/tests/pico8api.lua index 95d0085c..86dcecab 100644 --- a/tests/pico8api.lua +++ b/tests/pico8api.lua @@ -130,7 +130,10 @@ function tostr(val, hex) return val elseif kind == "number" then if hex then - val=val*0x10000 + -- in floating-point precision Lua, val may have more that 4 hex figures + -- after the hexadecimal point + val=flr(val*0x10000) + local test = val & 0xFFFF0000 local part1=(val & 0xFFFF0000) >> 16 -- fixed from original api.lua local part2=val & 0xFFFF return string.format("0x%04x.%04x", part1, part2) diff --git a/tests/utestpico8api.lua b/tests/utestpico8api.lua index 44679c64..08e2b272 100644 --- a/tests/utestpico8api.lua +++ b/tests/utestpico8api.lua @@ -135,6 +135,15 @@ describe('pico8api', function () it('0x58cb.fd85 => "0x58cb.fd85" (hex)', function () assert.are_equal("0x58cb.fd85", tostr(0x58cb.fd85, true)) end) + -- this one is only useful to test robustness with native Lua: + -- in PICO-8, floats have 16:16 fixed point precision, + -- so they can never get more than 4 hex figures after the point + -- with busted, we need to cut the extra hex figures to avoid + -- error "number (local 'val') has no integer representation" + -- when applying binary operations + it('0x58cb.fd8524 => "0x58cb.fd85" (hex)', function () + assert.are_equal("0x58cb.fd85", tostr(0x58cb.fd8524, true)) + end) it('{} => "[table]" (_tostring not implemented)', function () assert.are_equal("[table]", tostr({})) end) From b889b7adaf9f86d819b62b8a78854f0a61724301 Mon Sep 17 00:00:00 2001 From: hsandt Date: Sat, 9 Feb 2019 00:06:53 +0000 Subject: [PATCH 043/140] [ITEST] Fixed PICO-8 perfect precision for 'platformer ascending slope right' --- src/game/ingame/playercharacter.lua | 4 ++++ src/game/itests/itestplayercharacter.lua | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/game/ingame/playercharacter.lua b/src/game/ingame/playercharacter.lua index 302ac8ae..44573cf9 100644 --- a/src/game/ingame/playercharacter.lua +++ b/src/game/ingame/playercharacter.lua @@ -389,7 +389,11 @@ function player_char:_update_platformer_motion_grounded() end log("self.position: "..self.position, "trace") + log("self.position.x (hex): "..tostr(self.position.x, true), "trace") + log("self.position.y (hex): "..tostr(self.position.y, true), "trace") log("self.velocity: "..self.velocity, "trace") + log("self.velocity.x (hex): "..tostr(self.velocity.x, true), "trace") + log("self.velocity.y (hex): "..tostr(self.velocity.y, true), "trace") log("self.ground_speed: "..self.ground_speed, "trace") end diff --git a/src/game/itests/itestplayercharacter.lua b/src/game/itests/itestplayercharacter.lua index f5d95965..8a30a53d 100644 --- a/src/game/itests/itestplayercharacter.lua +++ b/src/game/itests/itestplayercharacter.lua @@ -33,7 +33,7 @@ expect pc_bottom_pos 0x0038.b7f1 8 -- precision note on expected pc_bottom_pos: -- 56.7185211181640625 (0x0038.b7f1) in PICO-8 fixed point precision --- 56.7333 in Lua floating point precision +-- 56.733333333333 in Lua floating point precision -- ground motion @@ -117,13 +117,23 @@ warp 4 16 move right wait 15 -expect pc_bottom_pos 6.519668501758 15 +expect pc_bottom_pos 0x0006.8509 15 expect pc_motion_state grounded expect pc_slope -0.125 expect pc_ground_spd 0.26318359375 -expect pc_velocity 0.1860961140625 -0.1860961140625 +expect pc_velocity 0x0000.2fa4 -0x0000.2fa5 ]]) +-- precision note on expected pc_bottom_pos: +-- 6.5196685791015625, 15 (0x0006.8509, 0x000f.0000) in PICO-8 fixed point precision +-- 6.5196741403377, 15 in Lua floating point precision + +-- precision note on expected pc_velocity: +-- 0.18609619140625, −0.186111450195 (0x0000.2fa4, 0xffff.d05b = -1 + 0x0000.d05b = -0x0000.2fa5) in PICO-8 fixed point precision +-- (we cannot use 0xffff. which would be interpreted as 65535; also note that vx != -vy due to cos imprecision of 0x0001 I guess) +-- 0.1860922277609, -0.1860922277609 in Lua floating point precision + + -- pc_slope -45/360 = -1/8 = -0.125 -- calculation notes: From caf12a76d38fe1cbc9ee7f19e5e5763254814aa7 Mon Sep 17 00:00:00 2001 From: hsandt Date: Sat, 9 Feb 2019 00:07:15 +0000 Subject: [PATCH 044/140] [HELPER] Added utests for hexadecimals in string_tonum --- tests/pico8api.lua | 2 +- tests/utesthelper.lua | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/pico8api.lua b/tests/pico8api.lua index 86dcecab..18018528 100644 --- a/tests/pico8api.lua +++ b/tests/pico8api.lua @@ -116,7 +116,7 @@ end -- e.g. "-0.1875" -> 0xffff.d001 instead of 0xffff.d000 -- https://www.lexaloffle.com/bbs/?tid=3780 -- for this reason, we recommend not using tonum in actual pico8 code --- and to use helper's tonum_precise instead +-- and to use helper's string_tonum instead function tonum(val) return tonumber(val) -- not a direct assignment to prevent usage of the radix argument end diff --git a/tests/utesthelper.lua b/tests/utesthelper.lua index 0f2da753..0787b18e 100644 --- a/tests/utesthelper.lua +++ b/tests/utesthelper.lua @@ -195,6 +195,12 @@ describe('string_tonum', function () it('-25.25 => -25.25', function () assert.are_equal(-25.25, string_tonum(-25.25)) end) + it('0x0000.2fa4 => 0x0000.2fa4', function () + assert.are_equal(0x0000.2fa4, string_tonum("0x0000.2fa4")) + end) + it('-0x0000.2fa4 => -0x0000.2fa4', function () + assert.are_equal(-0x0000.2fa4, string_tonum("-0x0000.2fa4")) + end) it('nil => error', function () assert.has_error(function () sub(nil, 1, 1) From d5a590d0f2bebc492980a46695e9806ab45ff7e2 Mon Sep 17 00:00:00 2001 From: hsandt Date: Thu, 21 Feb 2019 21:14:45 +0000 Subject: [PATCH 045/140] [MOTION] Do not apply gravity on hop/interrupt jump frame --- prebuild/replace_strings.py | 6 +- src/engine/test/itest_dsl.lua | 15 +++- src/game/ingame/playercharacter.lua | 35 +++++----- src/game/itests/itestplayercharacter.lua | 89 +++++++----------------- tests/utestitest_dsl.lua | 21 ++++++ tests/utestplayercharacter.lua | 68 +++++++++--------- 6 files changed, 113 insertions(+), 121 deletions(-) diff --git a/prebuild/replace_strings.py b/prebuild/replace_strings.py index 2443ce72..b1fa3d69 100755 --- a/prebuild/replace_strings.py +++ b/prebuild/replace_strings.py @@ -137,8 +137,10 @@ 'set_motion_mode': 2, 'move': 3, 'stop': 4, - 'wait': 5, - 'expect': 6, + 'jump': 5, + 'stop_jump': 6, + 'wait': 7, + 'expect': 8, }, 'gp_value_types': { diff --git a/src/engine/test/itest_dsl.lua b/src/engine/test/itest_dsl.lua index 7aebfbc4..481c1d74 100644 --- a/src/engine/test/itest_dsl.lua +++ b/src/engine/test/itest_dsl.lua @@ -100,7 +100,9 @@ command_types = enum { "set_motion_mode", -- set motion mode args: {motion_mode_str: motion_modes key} "move", -- set sticky pc move intention args: {move_dir_str: horizontal_dirs key} "stop", -- stop moving horizontally args: {} - -- todo: jump, crouch, spin_dash + "jump", -- start and hold jump args: {} + "stop_jump", -- stop any jump intention args: {} + -- todo: crouch, spin_dash "wait", -- wait some frames args: {frames: int} "expect", -- expect a gameplay value args: {gp_value_type: gp_value_types, expected_args...: matching gp value parsable type} } @@ -115,6 +117,8 @@ command_arg_types = { [command_types.set_motion_mode] = parsable_types.motion_mode, [command_types.move] = parsable_types.horizontal_dir, [command_types.stop] = parsable_types.none, + [command_types.jump] = parsable_types.none, + [command_types.stop_jump] = parsable_types.none, [command_types.wait] = parsable_types.number, [command_types.expect] = parsable_types.expect, } @@ -228,6 +232,15 @@ function itest_dsl.execute_stop(args) stage.state.player_char.move_intention = vector.zero() end +function itest_dsl.execute_jump(args) + stage.state.player_char.jump_intention = true -- will be consumed + stage.state.player_char.hold_jump_intention = true +end + +function itest_dsl.execute_stop_jump(args) + stage.state.player_char.hold_jump_intention = false +end + -- wait and expect are not timed actions and will be handled as special cases -- table of functions to call when applying a command with args, indexed by command type diff --git a/src/game/ingame/playercharacter.lua b/src/game/ingame/playercharacter.lua index 44573cf9..bd30a186 100644 --- a/src/game/ingame/playercharacter.lua +++ b/src/game/ingame/playercharacter.lua @@ -381,6 +381,9 @@ function player_char:_update_platformer_motion_grounded() self.position = ground_motion_result.position self.slope_angle = ground_motion_result.slope_angle + -- todo: reset jump intention on fall... we don't want character to cancel a natural fall by releasing jump button + -- (does not happen because of negative jump speed interrupt threshold, but could happen + -- once inertia is added by running off an ascending cliff) if ground_motion_result.is_falling then self:_enter_motion_state(motion_states.airborne) else @@ -680,21 +683,12 @@ function player_char:_check_jump() if self.should_jump then self.should_jump = false - -- compute initial jump speed based on whether player is still holding jump button - local initial_jump_speed - if self.hold_jump_intention then - -- variable jump - initial_jump_speed = pc_data.initial_var_jump_speed_frame - else - -- hop - initial_jump_speed = pc_data.jump_interrupt_speed_frame - -- mark jump as interrupted so we don't check it again - -- (optional, since we will never be able to interrupt such a small jump anyway) - self.has_interrupted_jump = true - end - - -- only support flat ground for now - self.velocity.y = self.velocity.y - initial_jump_speed + -- apply initial jump speed for variable jump + -- note: if the player is doing a hop, the vertical speed will be reset + -- to the interrupt speed during the same frame in _update_platformer_motion_airborne + -- (we don't do it here so we centralize the check and don't apply gravity during such a frame) + -- limitation: only support flat ground for now + self.velocity.y = self.velocity.y - pc_data.initial_var_jump_speed_frame self:_enter_motion_state(motion_states.airborne) return true end @@ -703,13 +697,13 @@ end -- update motion following platformer airborne motion rules function player_char:_update_platformer_motion_airborne() - -- check if player is continuing or interrupting jump *before* applying gravity - -- since our pc_data.jump_interrupt_speed_frame is defined to be applied before gravity - self:_check_hold_jump() - -- apply gravity to current speed y self.velocity.y = self.velocity.y + pc_data.gravity_frame2 + -- check if player is continuing or interrupting jump *after* applying gravity + -- this means gravity will *not* be applied during the hop/interrupt jump frame + self:_check_hold_jump() + -- apply x acceleration via intention (if not 0) self.velocity.x = self.velocity.x + self.move_intention.x * pc_data.air_accel_x_frame2 @@ -721,6 +715,9 @@ function player_char:_update_platformer_motion_airborne() if has_landed then self:_enter_motion_state(motion_states.grounded) end + + log("self.position: "..self.position, "trace") + log("self.velocity: "..self.velocity, "trace") end -- check if character wants to interrupt jump by not holding anymore, diff --git a/src/game/itests/itestplayercharacter.lua b/src/game/itests/itestplayercharacter.lua index 8a30a53d..1e205204 100644 --- a/src/game/itests/itestplayercharacter.lua +++ b/src/game/itests/itestplayercharacter.lua @@ -133,7 +133,6 @@ expect pc_velocity 0x0000.2fa4 -0x0000.2fa5 -- (we cannot use 0xffff. which would be interpreted as 65535; also note that vx != -vy due to cos imprecision of 0x0001 I guess) -- 0.1860922277609, -0.1860922277609 in Lua floating point precision - -- pc_slope -45/360 = -1/8 = -0.125 -- calculation notes: @@ -207,77 +206,39 @@ expect pc_velocity 0.84375 2.625 -- gravity during 24 frames: accel = 0.109375 * (24 * 25 / 2), velocity = 0.109375 * 24 = 2.625 -- at frame 60: pos (39.859375, 8 + 32.8125), velocity (0.84375, 2.625), airborne +itest_dsl_parser.register( + 'platformer hop flat', [[ +@stage # +. +# ---[[ -itest = integration_test('platformer hop flat', {stage.state.type}) -itest_manager:register(itest) - -itest.setup = function () - setup_map_data() - - -- add tiles where the character will move - mset(0, 10, 64) - - flow:change_gamestate_by_type(stage.state.type) - - -- respawn character on the ground (important to always start with grounded state) - stage.state.player_char:spawn_at(vector(4., 80. - pc_data.center_height_standing)) -- set bottom y at 80 - stage.state.player_char.control_mode = control_modes.puppet - stage.state.player_char.motion_mode = motion_modes.platformer +warp 4 8 +jump +stop_jump +wait 20 - -- start jump - stage.state.player_char.jump_intention = true -- will be consumed - -- don't set hold_jump_intention at all to get a hop - -- (you can also set it on setup and reset it at end of frame 1) -end +expect pc_bottom_pos 4 -11.296875 +expect pc_motion_state airborne +expect pc_ground_spd 0 +expect pc_velocity 0 -0.03125 +]]) -itest.teardown = function () - clear_map() - teardown_map_data() -end +-- calculation notes -- wait for apogee (frame 20) and stop --- at frame 1: bpos (4, 80), velocity (0, 0), grounded (waits 1 frame before confirming hop/jump) --- at frame 2: bpos (4, 80 - 2), velocity (0, -2), airborne (hop confirmed) --- at frame 3: bpos (4, 80 - 3.890625), velocity (0, -1.890625), airborne (hop confirmed) --- at frame 19: pos (4, 80 - 19.265625), velocity (0, -0.140625), airborne -> before apogee --- at frame 20: pos (4, 80 - 19.296875), velocity (0, -0.03125), airborne -> reached apogee --- at frame 21: pos (4, 80 - 19.21875), velocity (0, 0.078125), airborne -> starts going down --- at frame 38: pos (4, 80 - 1.15625), velocity (0, 1.9375), airborne -> about to land --- at frame 39: pos (4, 80), velocity (0, 0), grounded -> has landed -itest:add_action(time_trigger(20, true), function () -end) - --- check that player char has moved to the right and fell -itest.final_assertion = function () - local is_motion_state_expected, motion_state_message = motion_states.airborne == stage.state.player_char.motion_state, "Expected motion state 'airborne', got "..stage.state.player_char.motion_state - local is_position_expected, position_message = almost_eq_with_message(vector(4, 80. - 19.296875), stage.state.player_char:get_bottom_center(), 1/256) - local is_ground_speed_expected, ground_speed_message = almost_eq_with_message(0, stage.state.player_char.ground_speed, 1/256) - local is_velocity_expected, velocity_message = almost_eq_with_message(vector(0, -0.03125), stage.state.player_char.velocity, 1/256) - - local final_message = "" - - local success = is_position_expected and is_ground_speed_expected and is_velocity_expected and is_motion_state_expected - if not success then - if not is_motion_state_expected then - final_message = final_message..motion_state_message.."\n" - end - if not is_position_expected then - final_message = final_message..position_message.."\n" - end - if not is_ground_speed_expected then - final_message = final_message..ground_speed_message.."\n" - end - if not is_velocity_expected then - final_message = final_message..velocity_message.."\n" - end - - end +-- at frame 1: bpos (4, 8), velocity (0, 0), grounded (waits 1 frame before confirming hop/jump) +-- at frame 2: bpos (4, 8 - 2), velocity (0, -2), airborne (hop confirmed, no gravity applied this frame) +-- at frame 3: bpos (4, 8 - 3.890625), velocity (0, -1.890625), airborne +-- at frame 19: pos (4, 8 - 19.265625), velocity (0, -0.140625), airborne -> before apogee +-- at frame 20: pos (4, 8 - 19.296875), velocity (0, -0.03125), airborne -> reached apogee +-- at frame 21: pos (4, 8 - 19.21875), velocity (0, 0.078125), airborne -> starts going down +-- at frame 38: pos (4, 8 - 1.15625), velocity (0, 1.9375), airborne -> about to land +-- at frame 39: pos (4, 8), velocity (0, 0), grounded -> has landed - return success, final_message -end +-- => apogee at y = 8 - 19.296875 = -11.296875 +--[[ itest = integration_test('platformer jump f2 interrupt flat', {stage.state.type}) itest_manager:register(itest) diff --git a/tests/utestitest_dsl.lua b/tests/utestitest_dsl.lua index 3cd43ee4..4c7e2f9f 100644 --- a/tests/utestitest_dsl.lua +++ b/tests/utestitest_dsl.lua @@ -205,12 +205,33 @@ describe('itest_dsl', function () describe('execute_stop', function () it('should set the move intention of the current player character to vector zero', function () + stage.state.player_char.move_intention = vector(99, -99) itest_dsl.execute_stop({}) assert.are_equal(vector.zero(), stage.state.player_char.move_intention) end) end) + describe('execute_jump', function () + + it('should set the jump intention and hold jump intention to true', function () + itest_dsl.execute_jump({}) + assert.are_same({true, true}, + {stage.state.player_char.jump_intention, stage.state.player_char.hold_jump_intention}) + end) + + end) + + describe('execute_stop_jump', function () + + it('should set the hold jump intention to false', function () + stage.state.player_char.hold_jump_intention = true + itest_dsl.execute_stop_jump({}) + assert.is_false(stage.state.player_char.hold_jump_intention) + end) + + end) + end) describe('eval_', function () diff --git a/tests/utestplayercharacter.lua b/tests/utestplayercharacter.lua index 4f792d15..1acd775f 100644 --- a/tests/utestplayercharacter.lua +++ b/tests/utestplayercharacter.lua @@ -2584,21 +2584,11 @@ describe('player_char', function () assert.are_same({false, vector(4.1, -1), motion_states.grounded}, {result, pc.velocity, pc.motion_state}) end) - it('should consume should_jump, add initial hop velocity, update motion state and return false when should_jump is true and hold_jump_intention is false', function () + it('should consume should_jump, add initial var jump velocity, update motion state and return false when should_jump is true', function () pc.velocity = vector(4.1, -1) pc.should_jump = true local result = pc:_check_jump() - -- interface - assert.are_same({true, vector(4.1, -3), motion_states.airborne}, {result, pc.velocity, pc.motion_state}) - end) - - it('should consume should_jump, add initial var jump velocity, update motion state and return false when should_jump is true and hold_jump_intention is true', function () - pc.velocity = vector(4.1, -1) - pc.should_jump = true - pc.hold_jump_intention = true - local result = pc:_check_jump() - -- interface assert.are_same({true, vector(4.1, -4.25), motion_states.airborne}, {result, pc.velocity, pc.motion_state}) end) @@ -2607,27 +2597,38 @@ describe('player_char', function () describe('_update_platformer_motion_airborne', function () - local check_hold_jump_stub - local enter_motion_state_stub - setup(function () - check_hold_jump_stub = stub(player_char, "_check_hold_jump") - enter_motion_state_stub = stub(player_char, "_enter_motion_state") + spy.on(player_char, "_enter_motion_state") end) teardown(function () - check_hold_jump_stub:revert() - enter_motion_state_stub:revert() + player_char._enter_motion_state:revert() end) - after_each(function () - check_hold_jump_stub:clear() - enter_motion_state_stub:clear() + before_each(function () + -- optional, just to enter airborne state and be in a meaningful state + pc:_enter_motion_state(motion_states.airborne) + -- clear spy just after this instead of after_each to avoid messing the call count + player_char._enter_motion_state:clear() end) - it('. should apply gravity to speed y', function () + it('should apply gravity to speed y when not interrupting jump', function () + pc.velocity.y = -3 + pc.hold_jump_intention = true + + pc:_update_platformer_motion_airborne() + + assert.are_equal(-3 + pc_data.gravity_frame2, pc.velocity.y) + end) + + it('should set to speed y to interrupt speed (no gravity added) when interrupting jump', function () + pc.velocity.y = -3 -- must be < -pc_data.jump_interrupt_speed_frame (-2) + pc.hold_jump_intention = false + pc:_update_platformer_motion_airborne() - assert.are_equal(pc_data.gravity_frame2, pc.velocity.y) + + -- interface: we are assessing the effect of _check_hold_jump directly + assert.are_equal(-pc_data.jump_interrupt_speed_frame, pc.velocity.y) end) it('should apply accel x', function () @@ -2639,18 +2640,15 @@ describe('player_char', function () assert.are_equal(4 - pc_data.air_accel_x_frame2, pc.velocity.x) end) - it('. should update position with new speed y', function () + -- bugfix history: + -- . + it('should update position with new speed y', function () pc.position = vector(4, -4) - pc:_update_platformer_motion_airborne() - assert.are_equal(vector(4, -4 + pc_data.gravity_frame2), pc.position) - end) + pc.velocity.y = -3 + pc.hold_jump_intention = true - it('should call _check_hold_jump', function () pc:_update_platformer_motion_airborne() - - -- implementation - assert.spy(check_hold_jump_stub).was_called(1) - assert.spy(check_hold_jump_stub).was_called_with(match.ref(pc)) + assert.are_equal(vector(4, -4 - 3 + pc_data.gravity_frame2), pc.position) end) describe('(_check_escape_from_ground returns false, so has not landed)', function () @@ -2671,7 +2669,7 @@ describe('player_char', function () pc:_update_platformer_motion_airborne() -- implementation - assert.spy(enter_motion_state_stub).was_not_called() + assert.spy(pc._enter_motion_state).was_not_called() end) end) @@ -2694,8 +2692,8 @@ describe('player_char', function () pc:_update_platformer_motion_airborne() -- implementation - assert.spy(enter_motion_state_stub).was_called(1) - assert.spy(enter_motion_state_stub).was_called_with(match.ref(pc), motion_states.grounded) + assert.spy(pc._enter_motion_state).was_called(1) + assert.spy(pc._enter_motion_state).was_called_with(match.ref(pc), motion_states.grounded) end) end) From 28d601822477f2d7ab252b108816b203bcf3c555 Mon Sep 17 00:00:00 2001 From: hsandt Date: Mon, 25 Feb 2019 21:41:06 +0000 Subject: [PATCH 046/140] [HELPER] Improved string_tonum parsing failure checks --- src/engine/core/helper.lua | 8 ++++++-- src/engine/core/math.lua | 4 ++-- tests/utesthelper.lua | 41 ++++++++++++++++++++++++++++---------- 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/engine/core/helper.lua b/src/engine/core/helper.lua index 5bdbb88b..b3b344bf 100644 --- a/src/engine/core/helper.lua +++ b/src/engine/core/helper.lua @@ -127,9 +127,13 @@ function string_tonum(val) -- inspired by cheepicus's workaround in -- https://www.lexaloffle.com/bbs/?tid=3780 if sub(val, 1, 1) == '-' then - return - tonum(sub(val, 2)) + local abs_num = tonum(sub(val, 2)) + assert(abs_num, "could not parse absolute part of number: '-"..sub(val, 2).."'") + return - abs_num else - return tonum(val) + local num = tonum(val) + assert(num, "could not parse number: '"..val.."'") + return num end end diff --git a/src/engine/core/math.lua b/src/engine/core/math.lua index 95fabd3a..a99b76be 100644 --- a/src/engine/core/math.lua +++ b/src/engine/core/math.lua @@ -4,8 +4,8 @@ require("engine/core/class") function almost_eq(lhs, rhs, eps) eps = eps or 0.01 - assert(lhs) - assert(rhs) + assert(lhs, "lhs is nil") + assert(rhs, "rhs") if type(lhs) == "number" and type(rhs) == "number" then return abs(lhs - rhs) <= eps elseif lhs.almost_eq then diff --git a/tests/utesthelper.lua b/tests/utesthelper.lua index 0787b18e..cf39b192 100644 --- a/tests/utesthelper.lua +++ b/tests/utesthelper.lua @@ -189,32 +189,53 @@ describe('string_tonum', function () it('"-25.25" => -25.25', function () assert.are_equal(-25.25, string_tonum("-25.25")) end) - it('304.25 => 304.25', function () + it('"304.25" => 304.25', function () assert.are_equal(304.25, string_tonum(304.25)) end) - it('-25.25 => -25.25', function () + it('"-25.25" => -25.25', function () assert.are_equal(-25.25, string_tonum(-25.25)) end) - it('0x0000.2fa4 => 0x0000.2fa4', function () + it('"0x0000.2fa4" => 0x0000.2fa4', function () assert.are_equal(0x0000.2fa4, string_tonum("0x0000.2fa4")) end) - it('-0x0000.2fa4 => -0x0000.2fa4', function () + it('"-0x0000.2fa4" => -0x0000.2fa4', function () assert.are_equal(-0x0000.2fa4, string_tonum("-0x0000.2fa4")) end) + it('"-abc" => error (minus sign instead of hyphen-minus)', function () + assert.has_error(function () + string_tonum("-abc") + end, + "could not parse absolute part of number: '-abc'") + end) + it('"−5" => error (minus sign instead of hyphen-minus)', function () + assert.has_error(function () + string_tonum("−5") + end, + "could not parse number: '−5'") + end) + it('"abc" => error (minus sign instead of hyphen-minus)', function () + assert.has_error(function () + string_tonum("abc") + end, + "could not parse number: 'abc'") + end) it('nil => error', function () assert.has_error(function () - sub(nil, 1, 1) - end) + string_tonum(nil) + end, + "bad argument #1 to 'sub' (string expected, got nil)") end) it('true => error', function () assert.has_error(function () - sub(nil, 1, 1) - end) + string_tonum(true) + end, + "bad argument #1 to 'sub' (string expected, got boolean)") end) it('{} => error', function () assert.has_error(function () - sub({}, 1, 1) - end) + string_tonum({}) + end, + "bad argument #1 to 'sub' (string expected, got table)") end) end) From 3873d4b7089d06baab7fb5c5e961cfc4fe08d9c6 Mon Sep 17 00:00:00 2001 From: hsandt Date: Mon, 25 Feb 2019 22:49:27 +0000 Subject: [PATCH 047/140] [MOTION] Don't apply gravity on jump first frame, even if not hop --- src/game/ingame/playercharacter.lua | 13 ++- src/game/itests/itestplayercharacter.lua | 117 ++++++++++++----------- tests/utestplayercharacter.lua | 42 ++++++-- 3 files changed, 105 insertions(+), 67 deletions(-) diff --git a/src/game/ingame/playercharacter.lua b/src/game/ingame/playercharacter.lua index bd30a186..fa03d060 100644 --- a/src/game/ingame/playercharacter.lua +++ b/src/game/ingame/playercharacter.lua @@ -61,6 +61,7 @@ local player_char = new_class() -- jump_intention bool current intention to start jump (consumed on jump) -- hold_jump_intention bool current intention to hold jump (always true when jump_intention is true) -- should_jump bool should the character jump when next frame is entered? used to delay variable jump/hop by 1 frame +-- has_jumped_this_frame bool has the character started a jump/hop this frame? -- has_interrupted_jump bool has the character already interrupted his jump once? -- current_sprite string current sprite key in the spr_data function player_char:_init() @@ -89,6 +90,7 @@ function player_char:_setup() self.jump_intention = false self.hold_jump_intention = false self.should_jump = false + self.has_jumped_this_frame = false self.has_interrupted_jump = false self.current_sprite = "idle" @@ -340,6 +342,7 @@ function player_char:_enter_motion_state(next_motion_state) -- we have just reached the ground (and possibly escaped), -- reset values airborne vars self.velocity.y = 0 -- no velocity retain yet on y + self.has_jumped_this_frame = false -- optional since consumed immediately in _update_platformer_motion_airborne self.has_interrupted_jump = false self.current_sprite = "idle" end @@ -690,6 +693,7 @@ function player_char:_check_jump() -- limitation: only support flat ground for now self.velocity.y = self.velocity.y - pc_data.initial_var_jump_speed_frame self:_enter_motion_state(motion_states.airborne) + self.has_jumped_this_frame = true return true end return false @@ -697,8 +701,13 @@ end -- update motion following platformer airborne motion rules function player_char:_update_platformer_motion_airborne() - -- apply gravity to current speed y - self.velocity.y = self.velocity.y + pc_data.gravity_frame2 + if self.has_jumped_this_frame then + -- do not apply gravity on first frame of jump, and consume has_jumped_this_frame + self.has_jumped_this_frame = false + else + -- apply gravity to current speed y + self.velocity.y = self.velocity.y + pc_data.gravity_frame2 + end -- check if player is continuing or interrupting jump *after* applying gravity -- this means gravity will *not* be applied during the hop/interrupt jump frame diff --git a/src/game/itests/itestplayercharacter.lua b/src/game/itests/itestplayercharacter.lua index 1e205204..ca0599ca 100644 --- a/src/game/itests/itestplayercharacter.lua +++ b/src/game/itests/itestplayercharacter.lua @@ -238,79 +238,82 @@ expect pc_velocity 0 -0.03125 -- => apogee at y = 8 - 19.296875 = -11.296875 ---[[ -itest = integration_test('platformer jump f2 interrupt flat', {stage.state.type}) -itest_manager:register(itest) +itest_dsl_parser.register( + 'platformer jump start flat', [[ +@stage # +. +# -itest.setup = function () - setup_map_data() +warp 4 8 +jump +wait 2 - -- add tiles where the character will move - mset(0, 10, 64) +expect pc_bottom_pos 4 4.75 +expect pc_motion_state airborne +expect pc_ground_spd 0 +expect pc_velocity 0 -3.25 +]]) - flow:change_gamestate_by_type(stage.state.type) - -- respawn character on the ground (important to always start with grounded state) - stage.state.player_char:spawn_at(vector(4., 80. - pc_data.center_height_standing)) -- set bottom y at 80 - stage.state.player_char.control_mode = control_modes.puppet - stage.state.player_char.motion_mode = motion_modes.platformer +itest_dsl_parser.register( + 'platformer jump interrupt flat', [[ +@stage # +. +# - -- start jump - stage.state.player_char.jump_intention = true -- will be consumed - stage.state.player_char.hold_jump_intention = true -end +warp 4 8 +jump +wait 4 +stop_jump +wait 1 -itest.teardown = function () - clear_map() - teardown_map_data() -end +expect pc_bottom_pos 4 -3.421875 +expect pc_motion_state airborne +expect pc_ground_spd 0 +expect pc_velocity 0 -2 +]]) + +-- calculation notes -- interrupt variable jump at the end of frame 2 --- at frame 1: bpos (4, 80), velocity (0, 0), grounded (waits 1 frame before confirming hop/jump) --- at frame 2: bpos (4, 80 - 3.25), velocity (0, -3.25), airborne (jump confirmed) -itest:add_action(time_trigger(2, true), function () - stage.state.player_char.hold_jump_intention = false -end) +-- at frame 1: bpos (4, 8), velocity (0, 0), grounded (waits 1 frame before confirming hop/jump) +-- at frame 2: bpos (4, 8 - 3.25), velocity (0, -3.25), airborne (jump confirmed) +-- at frame 3: bpos (4, 8 - 6.390625), velocity (0, -3.140625), airborne +-- at frame 4: bpos (4, 8 - 9.421875), velocity (0, -3.03125), airborne +-- at frame 5: bpos (4, 8 - 11.421875), velocity (0, -2), airborne (interrupt jump, no extra gravity) --- wait for the apogee (frame 20) and stop --- at frame 3: bpos (4, 80 - 5.140625), velocity (0, -1.890625), airborne -> jump interrupted (gravity is applied *after* setting speed y to -2) --- at frame 19: bpos (4, 80 - 20.515625), velocity (0, -0.140625), airborne -> before apogee --- at frame 20: bpos (4, 80 - 20.546875), velocity (0, -0.03125), airborne -> reached apogee --- at frame 21: bpos (4, 80 - 20.46875), velocity (0, 0.078125), airborne -> starts going down --- at frame 39: bpos (4, 80 - 0.3594), velocity (0, 2.15625), airborne -> about to land --- at frame 40: bpos (4, 80), velocity (0, 0), grounded -> has landed -itest:add_action(time_trigger(18, true), function () end) --- check that player char has reached the apogee of the jump -itest.final_assertion = function () - local is_motion_state_expected, motion_state_message = motion_states.airborne == stage.state.player_char.motion_state, "Expected motion state 'airborne', got "..stage.state.player_char.motion_state - local is_position_expected, position_message = almost_eq_with_message(vector(4, 80 - 20.546875), stage.state.player_char:get_bottom_center(), 1/256) - local is_ground_speed_expected, ground_speed_message = almost_eq_with_message(0, stage.state.player_char.ground_speed, 1/256) - local is_velocity_expected, velocity_message = almost_eq_with_message(vector(0, -0.03125), stage.state.player_char.velocity, 1/256) +itest_dsl_parser.register( + '#solo platformer small jump flat', [[ +@stage # +. +# - local final_message = "" +warp 4 8 +jump +wait 4 +stop_jump +wait 6 - local success = is_position_expected and is_ground_speed_expected and is_velocity_expected and is_motion_state_expected - if not success then - if not is_motion_state_expected then - final_message = final_message..motion_state_message.."\n" - end - if not is_position_expected then - final_message = final_message..position_message.."\n" - end - if not is_ground_speed_expected then - final_message = final_message..ground_speed_message.."\n" - end - if not is_velocity_expected then - final_message = final_message..velocity_message.."\n" - end +expect pc_bottom_pos 4 -11.7813 +expect pc_motion_state airborne +expect pc_ground_spd 0 +expect pc_velocity 0 -1.453125 +]]) - end +-- calculation notes - return success, final_message -end +-- frames 1-5 is same as 'platformer jump interrupt flat' +-- wait 5 frames and stop +-- at frame 6: bpos (4, 8 - 13.3125), velocity (0, -1.890625), airborne +-- at frame 7: bpos (4, 8 - 15.09375), velocity (0, -1.78125), airborne +-- at frame 8: bpos (4, 8 - 16.765675), velocity (0, -1.671925), airborne +-- at frame 9: bpos (4, 8 - 18.328175), velocity (0, -1.5625), airborne +-- at frame 10: bpos (4, 8 - 19.7813), velocity (0, -1.453125), airborne + +--[[ itest = integration_test('platformer full jump flat', {stage.state.type}) itest_manager:register(itest) diff --git a/tests/utestplayercharacter.lua b/tests/utestplayercharacter.lua index 1acd775f..02bb0592 100644 --- a/tests/utestplayercharacter.lua +++ b/tests/utestplayercharacter.lua @@ -101,6 +101,7 @@ describe('player_char', function () false, false, false, + false, "idle" }, { @@ -113,6 +114,7 @@ describe('player_char', function () pc.jump_intention, pc.hold_jump_intention, pc.should_jump, + pc.has_jumped_this_frame, pc.has_interrupted_jump, pc.current_sprite } @@ -1206,11 +1208,13 @@ describe('player_char', function () motion_states.grounded, 0, false, + false, "idle" }, { pc.motion_state, pc.velocity.y, + pc.has_jumped_this_frame, pc.has_interrupted_jump, pc.current_sprite }) @@ -2576,21 +2580,21 @@ describe('player_char', function () describe('_check_jump', function () - it('should return false when should_jump is false', function () + it('should not set jump members and return false when should_jump is false', function () pc.velocity = vector(4.1, -1) local result = pc:_check_jump() -- interface - assert.are_same({false, vector(4.1, -1), motion_states.grounded}, {result, pc.velocity, pc.motion_state}) + assert.are_same({false, vector(4.1, -1), motion_states.grounded, false}, {result, pc.velocity, pc.motion_state, pc.has_jumped_this_frame}) end) - it('should consume should_jump, add initial var jump velocity, update motion state and return false when should_jump is true', function () + it('should consume should_jump, add initial var jump velocity, update motion state, set has_jumped_this_frame flag and return true when should_jump is true', function () pc.velocity = vector(4.1, -1) pc.should_jump = true local result = pc:_check_jump() -- interface - assert.are_same({true, vector(4.1, -4.25), motion_states.airborne}, {result, pc.velocity, pc.motion_state}) + assert.are_same({true, vector(4.1, -4.25), motion_states.airborne, true}, {result, pc.velocity, pc.motion_state, pc.has_jumped_this_frame}) end) end) @@ -2612,23 +2616,45 @@ describe('player_char', function () player_char._enter_motion_state:clear() end) - it('should apply gravity to speed y when not interrupting jump', function () + it('should preserve (supposedly initial hop) velocity y on first frame of hop and clear has_jumped_this_frame flag', function () + pc.velocity.y = -3 -- must be < -pc_data.jump_interrupt_speed_frame (-2) + pc.has_jumped_this_frame = true + pc.hold_jump_intention = false + + pc:_update_platformer_motion_airborne() + + assert.are_same({-pc_data.jump_interrupt_speed_frame, false}, {pc.velocity.y, pc.has_jumped_this_frame}) + end) + + it('should preserve (supposedly initial jump) velocity y on first frame of jump (not hop) and clear has_jumped_this_frame flag', function () pc.velocity.y = -3 + pc.has_jumped_this_frame = true + pc.hold_jump_intention = true + + pc:_update_platformer_motion_airborne() + + assert.are_same({-3, false}, {pc.velocity.y, pc.has_jumped_this_frame}) + end) + + it('should apply gravity to velocity y when not on first frame of jump and not interrupting jump', function () + pc.velocity.y = -1 + pc.has_jumped_this_frame = false pc.hold_jump_intention = true pc:_update_platformer_motion_airborne() - assert.are_equal(-3 + pc_data.gravity_frame2, pc.velocity.y) + assert.are_same({-1 + pc_data.gravity_frame2, false}, {pc.velocity.y, pc.has_jumped_this_frame}) end) - it('should set to speed y to interrupt speed (no gravity added) when interrupting jump', function () + it('should set to speed y to interrupt speed (no gravity added) when interrupting actual jump', function () pc.velocity.y = -3 -- must be < -pc_data.jump_interrupt_speed_frame (-2) + pc.has_jumped_this_frame = false pc.hold_jump_intention = false pc:_update_platformer_motion_airborne() -- interface: we are assessing the effect of _check_hold_jump directly - assert.are_equal(-pc_data.jump_interrupt_speed_frame, pc.velocity.y) + assert.are_same({-pc_data.jump_interrupt_speed_frame, false}, {pc.velocity.y, pc.has_jumped_this_frame}) end) it('should apply accel x', function () From 384ced5242a7844ee148b90c13afb9555c7f69fd Mon Sep 17 00:00:00 2001 From: hsandt Date: Mon, 25 Feb 2019 22:55:22 +0000 Subject: [PATCH 048/140] [TEST] DSL: Converted 'platformer full jump flat' to DSL --- src/game/itests/itestplayercharacter.lua | 70 +++++------------------- 1 file changed, 15 insertions(+), 55 deletions(-) diff --git a/src/game/itests/itestplayercharacter.lua b/src/game/itests/itestplayercharacter.lua index ca0599ca..affc5b93 100644 --- a/src/game/itests/itestplayercharacter.lua +++ b/src/game/itests/itestplayercharacter.lua @@ -284,7 +284,7 @@ expect pc_velocity 0 -2 itest_dsl_parser.register( - '#solo platformer small jump flat', [[ + 'platformer small jump flat', [[ @stage # . # @@ -313,32 +313,23 @@ expect pc_velocity 0 -1.453125 -- at frame 10: bpos (4, 8 - 19.7813), velocity (0, -1.453125), airborne ---[[ -itest = integration_test('platformer full jump flat', {stage.state.type}) -itest_manager:register(itest) - -itest.setup = function () - setup_map_data() - - -- add tiles where the character will move - mset(0, 10, 64) - - flow:change_gamestate_by_type(stage.state.type) +itest_dsl_parser.register( + 'platformer full jump flat', [[ +@stage # +. +# - -- respawn character on the ground (important to always start with grounded state) - stage.state.player_char:spawn_at(vector(4., 80. - pc_data.center_height_standing)) -- set bottom y at 80 - stage.state.player_char.control_mode = control_modes.puppet - stage.state.player_char.motion_mode = motion_modes.platformer +warp 4 8 +jump +wait 31 - -- start jump - stage.state.player_char.jump_intention = true -- will be consumed - stage.state.player_char.hold_jump_intention = true -end +expect pc_bottom_pos 4 -41.921875 +expect pc_motion_state airborne +expect pc_ground_spd 0 +expect pc_velocity 0 -0.078125 +]]) -itest.teardown = function () - clear_map() - teardown_map_data() -end +-- calculation notes -- wait for the apogee (frame 31) and stop -- at frame 1: pos (4, 80), velocity (0, 0), grounded (waits 1 frame before confirming hop/jump) @@ -348,37 +339,6 @@ end -- at frame 32: pos (4, 80 - 49.890625), velocity (0, 0.03125), airborne -> starts going down -- at frame 61: pos (4, 80 - 1.40625), velocity (0, 3.203125), airborne -> about to land -- at frame 62: pos (4, 80), velocity (0, 0), grounded -> has landed -itest:add_action(time_trigger(31, true), function () end) - --- check that player char has moved to the right and fell -itest.final_assertion = function () - local is_motion_state_expected, motion_state_message = motion_states.airborne == stage.state.player_char.motion_state, "Expected motion state 'airborne', got "..stage.state.player_char.motion_state - local is_position_expected, position_message = almost_eq_with_message(vector(4, 80 - 49.921875), stage.state.player_char:get_bottom_center(), 1/256) - local is_ground_speed_expected, ground_speed_message = almost_eq_with_message(0, stage.state.player_char.ground_speed, 1/256) - local is_velocity_expected, velocity_message = almost_eq_with_message(vector(0, -0.078125), stage.state.player_char.velocity, 1/256) - - local final_message = "" - - local success = is_position_expected and is_ground_speed_expected and is_velocity_expected and is_motion_state_expected - if not success then - if not is_motion_state_expected then - final_message = final_message..motion_state_message.."\n" - end - if not is_position_expected then - final_message = final_message..position_message.."\n" - end - if not is_ground_speed_expected then - final_message = final_message..ground_speed_message.."\n" - end - if not is_velocity_expected then - final_message = final_message..velocity_message.."\n" - end - - end - - return success, final_message -end ---]] --[[ From 51632a5f6d5d895911987e7e6da24949f8945f58 Mon Sep 17 00:00:00 2001 From: hsandt Date: Mon, 25 Feb 2019 23:06:56 +0000 Subject: [PATCH 049/140] [TEST] DSL: fixed calculation error visible in PICO-8 itest only --- src/game/itests/itestplayercharacter.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/game/itests/itestplayercharacter.lua b/src/game/itests/itestplayercharacter.lua index affc5b93..39c53014 100644 --- a/src/game/itests/itestplayercharacter.lua +++ b/src/game/itests/itestplayercharacter.lua @@ -295,7 +295,7 @@ wait 4 stop_jump wait 6 -expect pc_bottom_pos 4 -11.7813 +expect pc_bottom_pos 4 -11.78125 expect pc_motion_state airborne expect pc_ground_spd 0 expect pc_velocity 0 -1.453125 @@ -308,9 +308,9 @@ expect pc_velocity 0 -1.453125 -- wait 5 frames and stop -- at frame 6: bpos (4, 8 - 13.3125), velocity (0, -1.890625), airborne -- at frame 7: bpos (4, 8 - 15.09375), velocity (0, -1.78125), airborne --- at frame 8: bpos (4, 8 - 16.765675), velocity (0, -1.671925), airborne --- at frame 9: bpos (4, 8 - 18.328175), velocity (0, -1.5625), airborne --- at frame 10: bpos (4, 8 - 19.7813), velocity (0, -1.453125), airborne +-- at frame 8: bpos (4, 8 - 16.765625), velocity (0, -1.671875), airborne +-- at frame 9: bpos (4, 8 - 18.328125), velocity (0, -1.5625), airborne +-- at frame 10: bpos (4, 8 - 19.78125), velocity (0, -1.453125), airborne itest_dsl_parser.register( From 4026b0778ee77735a6f06cdc926d888a274c3a70 Mon Sep 17 00:00:00 2001 From: hsandt Date: Wed, 27 Feb 2019 00:16:23 +0000 Subject: [PATCH 050/140] [TEST] DSL: Added set_control_mode action and control_mode type Do not exclude control_modes table from pico8 build anymore to allow access by enum key string --- prebuild/replace_strings.py | 32 ++++++++++---------- src/engine/test/itest_dsl.lua | 46 +++++++++++++++++++---------- src/game/ingame/playercharacter.lua | 4 +-- tests/utestitest_dsl.lua | 23 +++++++++++++++ 4 files changed, 71 insertions(+), 34 deletions(-) diff --git a/prebuild/replace_strings.py b/prebuild/replace_strings.py index b1fa3d69..63bdc6e7 100755 --- a/prebuild/replace_strings.py +++ b/prebuild/replace_strings.py @@ -123,24 +123,26 @@ # itest_dsl 'parsable_types': { - 'none': 1, - 'number': 2, - 'vector': 3, - 'horizontal_dir': 4, - 'motion_mode': 5, - 'motion_state': 6, - 'expect': 7, + 'none': 1, + 'number': 2, + 'vector': 3, + 'horizontal_dir': 4, + 'control_mode': 5, + 'motion_mode': 6, + 'motion_state': 7, + 'expect': 8, }, 'command_types': { - 'warp': 1, - 'set_motion_mode': 2, - 'move': 3, - 'stop': 4, - 'jump': 5, - 'stop_jump': 6, - 'wait': 7, - 'expect': 8, + 'warp': 1, + 'set_control_mode': 2, + 'set_motion_mode': 3, + 'move': 4, + 'stop': 5, + 'jump': 6, + 'stop_jump': 7, + 'wait': 8, + 'expect': 9, }, 'gp_value_types': { diff --git a/src/engine/test/itest_dsl.lua b/src/engine/test/itest_dsl.lua index 481c1d74..ce2efa13 100644 --- a/src/engine/test/itest_dsl.lua +++ b/src/engine/test/itest_dsl.lua @@ -84,6 +84,7 @@ parsable_types = enum { "number", "vector", "horizontal_dir", + "control_mode", "motion_mode", "motion_state", "expect", -- meta-type meaning we must check the 1st arg (gp_value_type) to know what the rest should be @@ -96,15 +97,16 @@ parsable_type_strings = invert_table(parsable_types) -- type of commands available command_types = enum { - "warp", -- warp player character bottom args: {bottom_position: vector} - "set_motion_mode", -- set motion mode args: {motion_mode_str: motion_modes key} - "move", -- set sticky pc move intention args: {move_dir_str: horizontal_dirs key} - "stop", -- stop moving horizontally args: {} - "jump", -- start and hold jump args: {} - "stop_jump", -- stop any jump intention args: {} + "warp", -- warp player character bottom args: {bottom_position: vector} + "set_control_mode", -- set control mode args: {control_mode_str: control_modes key} + "set_motion_mode", -- set motion mode args: {motion_mode_str: motion_modes key} + "move", -- set sticky pc move intention args: {move_dir_str: horizontal_dirs key} + "stop", -- stop moving horizontally args: {} + "jump", -- start and hold jump args: {} + "stop_jump", -- stop any jump intention args: {} -- todo: crouch, spin_dash - "wait", -- wait some frames args: {frames: int} - "expect", -- expect a gameplay value args: {gp_value_type: gp_value_types, expected_args...: matching gp value parsable type} + "wait", -- wait some frames args: {frames: int} + "expect", -- expect a gameplay value args: {gp_value_type: gp_value_types, expected_args...: matching gp value parsable type} } --#if assert @@ -113,14 +115,15 @@ command_type_strings = invert_table(command_types) -- argument types expected after those commands command_arg_types = { - [command_types.warp] = parsable_types.vector, - [command_types.set_motion_mode] = parsable_types.motion_mode, - [command_types.move] = parsable_types.horizontal_dir, - [command_types.stop] = parsable_types.none, - [command_types.jump] = parsable_types.none, - [command_types.stop_jump] = parsable_types.none, - [command_types.wait] = parsable_types.number, - [command_types.expect] = parsable_types.expect, + [command_types.warp] = parsable_types.vector, + [command_types.set_control_mode] = parsable_types.control_mode, + [command_types.set_motion_mode] = parsable_types.motion_mode, + [command_types.move] = parsable_types.horizontal_dir, + [command_types.stop] = parsable_types.none, + [command_types.jump] = parsable_types.none, + [command_types.stop_jump] = parsable_types.none, + [command_types.wait] = parsable_types.number, + [command_types.expect] = parsable_types.expect, } @@ -171,6 +174,13 @@ function itest_dsl.parse_horizontal_dir(arg_strings) return horizontal_dir end +function itest_dsl.parse_control_mode(arg_strings) + assert(#arg_strings == 1, "parse_control_mode: got "..#arg_strings.." args, expected 1") + local control_mode = control_modes[arg_strings[1]] + assert(control_mode, "control_modes["..arg_strings[1].."] is not defined") + return control_mode +end + function itest_dsl.parse_motion_mode(arg_strings) assert(#arg_strings == 1, "parse_motion_mode: got "..#arg_strings.." args, expected 1") local motion_mode = motion_modes[arg_strings[1]] @@ -220,6 +230,10 @@ function itest_dsl.execute_warp(args) stage.state.player_char:warp_bottom_to(args[1]) end +function itest_dsl.execute_set_control_mode(args) + stage.state.player_char.control_mode = args[1] +end + function itest_dsl.execute_set_motion_mode(args) stage.state.player_char.motion_mode = args[1] end diff --git a/src/game/ingame/playercharacter.lua b/src/game/ingame/playercharacter.lua index fa03d060..29f16aa9 100644 --- a/src/game/ingame/playercharacter.lua +++ b/src/game/ingame/playercharacter.lua @@ -8,7 +8,6 @@ local collision = require("engine/physics/collision") local world = require("engine/physics/world") local pc_data = require("game/data/playercharacter_data") ---#ifn pico8 -- enum for character control control_modes = { @@ -17,8 +16,6 @@ control_modes = { puppet = 3 -- itest script controls character } ---#endif - -- motion_modes and motion_states are accessed dynamically via variant name in itest_dsl -- so we don't strip them away from pico8 builds -- it is only used for debug and expectations, though, so it could be #if cheat/test only, @@ -38,6 +35,7 @@ motion_states = { airborne = 2 -- character is in the air } + local player_char = new_class() -- parameters diff --git a/tests/utestitest_dsl.lua b/tests/utestitest_dsl.lua index 4c7e2f9f..62babbdc 100644 --- a/tests/utestitest_dsl.lua +++ b/tests/utestitest_dsl.lua @@ -108,6 +108,20 @@ describe('itest_dsl', function () end) + describe('parse_control_mode', function () + + it('should assert when the number of arguments is wrong', function () + assert.has_error(function () + itest_dsl.parse_control_mode({"too", "many"}) + end, "parse_control_mode: got 2 args, expected 1") + end) + + it('should return the single argument as control mode', function () + assert.are_equal(control_modes.ai, itest_dsl.parse_control_mode({"ai"})) + end) + + end) + describe('parse_motion_mode', function () it('should assert when the number of arguments is wrong', function () @@ -184,6 +198,15 @@ describe('itest_dsl', function () end) + describe('execute_set_control_mode', function () + + it('should set the control mode', function () + itest_dsl.execute_set_control_mode({control_modes.puppet}) + assert.are_equal(control_modes.puppet, stage.state.player_char.control_mode) + end) + + end) + describe('execute_set_motion_mode', function () it('should set the motion mode', function () From 2871d83b1462a8235586eb93ae02f3f3e48c7ad6 Mon Sep 17 00:00:00 2001 From: hsandt Date: Wed, 27 Feb 2019 00:35:42 +0000 Subject: [PATCH 051/140] [TEST] DSL: Added button_id type and press/release actions Don't exclude button_ids enum table from pico8 build anymore to allow access by enum key string --- prebuild/replace_strings.py | 9 ++++++--- src/engine/input/input.lua | 2 +- src/engine/test/itest_dsl.lua | 35 +++++++++++++++++++++++++++++------ tests/utestitest_dsl.lua | 35 +++++++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 10 deletions(-) diff --git a/prebuild/replace_strings.py b/prebuild/replace_strings.py index 63bdc6e7..f241f4ae 100755 --- a/prebuild/replace_strings.py +++ b/prebuild/replace_strings.py @@ -130,7 +130,8 @@ 'control_mode': 5, 'motion_mode': 6, 'motion_state': 7, - 'expect': 8, + 'button_id': 8, + 'expect': 9, }, 'command_types': { @@ -141,8 +142,10 @@ 'stop': 5, 'jump': 6, 'stop_jump': 7, - 'wait': 8, - 'expect': 9, + 'press': 8, + 'release': 9, + 'wait': 10, + 'expect': 11, }, 'gp_value_types': { diff --git a/src/engine/input/input.lua b/src/engine/input/input.lua index ab9322e0..d15fb920 100644 --- a/src/engine/input/input.lua +++ b/src/engine/input/input.lua @@ -1,6 +1,5 @@ require("engine/core/math") ---#ifn pico8 button_ids = { left = 0, right = 1, @@ -10,6 +9,7 @@ button_ids = { x = 5 } +--#ifn pico8 btn_states = { released = 0, just_pressed = 1, diff --git a/src/engine/test/itest_dsl.lua b/src/engine/test/itest_dsl.lua index ce2efa13..338ed3e8 100644 --- a/src/engine/test/itest_dsl.lua +++ b/src/engine/test/itest_dsl.lua @@ -37,7 +37,8 @@ local tilemap = require("engine/data/tilemap") -- dsl interpretation requirements local flow = require("engine/application/flow") -local stage = require("game/ingame/stage") -- required +local input = require("engine/input/input") +local stage = require("game/ingame/stage") local player_char = require("game/ingame/playercharacter") local pc_data = require("game/data/playercharacter_data") @@ -87,6 +88,7 @@ parsable_types = enum { "control_mode", "motion_mode", "motion_state", + "button_id", "expect", -- meta-type meaning we must check the 1st arg (gp_value_type) to know what the rest should be } @@ -105,6 +107,8 @@ command_types = enum { "jump", -- start and hold jump args: {} "stop_jump", -- stop any jump intention args: {} -- todo: crouch, spin_dash + "press", -- press and hold button args: {button_id_str: button_ids key} + "release", -- release button args: {button_id_str: button_ids key} "wait", -- wait some frames args: {frames: int} "expect", -- expect a gameplay value args: {gp_value_type: gp_value_types, expected_args...: matching gp value parsable type} } @@ -122,6 +126,8 @@ command_arg_types = { [command_types.stop] = parsable_types.none, [command_types.jump] = parsable_types.none, [command_types.stop_jump] = parsable_types.none, + [command_types.press] = parsable_types.button_id, + [command_types.release] = parsable_types.button_id, [command_types.wait] = parsable_types.number, [command_types.expect] = parsable_types.expect, } @@ -142,11 +148,11 @@ gp_value_type_strings = invert_table(gp_value_types) -- data for each gameplay value type local gp_value_data_t = { - [gp_value_types.pc_bottom_pos] = gameplay_value_data("player character bottom position", parsable_types.vector), - [gp_value_types.pc_velocity] = gameplay_value_data("player character velocity", parsable_types.vector), - [gp_value_types.pc_ground_spd] = gameplay_value_data("player character ground speed", parsable_types.number), - [gp_value_types.pc_motion_state] = gameplay_value_data("player character motion state", parsable_types.motion_state), - [gp_value_types.pc_slope] = gameplay_value_data("player character slope", parsable_types.number), + [gp_value_types.pc_bottom_pos] = gameplay_value_data("player character bottom position", parsable_types.vector), + [gp_value_types.pc_velocity] = gameplay_value_data("player character velocity", parsable_types.vector), + [gp_value_types.pc_ground_spd] = gameplay_value_data("player character ground speed", parsable_types.number), + [gp_value_types.pc_motion_state] = gameplay_value_data("player character motion state", parsable_types.motion_state), + [gp_value_types.pc_slope] = gameplay_value_data("player character slope", parsable_types.number), } @@ -195,6 +201,13 @@ function itest_dsl.parse_motion_state(arg_strings) return motion_states[arg_strings[1]] end +function itest_dsl.parse_button_id(arg_strings) + assert(#arg_strings == 1, "parse_button_id: got "..#arg_strings.." args, expected 1") + local button_id = button_ids[arg_strings[1]] + assert(button_id, "button_ids["..arg_strings[1].."] is not defined") + return button_ids[arg_strings[1]] +end + -- convert string args to vector function itest_dsl.parse_expect(arg_strings) assert(#arg_strings > 1, "parse_expect: got "..#arg_strings.." args, expected at least 2") @@ -255,6 +268,16 @@ function itest_dsl.execute_stop_jump(args) stage.state.player_char.hold_jump_intention = false end +function itest_dsl.execute_press(args) + -- simulate sticky press for player 0 + input.simulated_buttons_down[0][args[1]] = true +end + +function itest_dsl.execute_release(args) + -- simulate release for player 0 + input.simulated_buttons_down[0][args[1]] = false +end + -- wait and expect are not timed actions and will be handled as special cases -- table of functions to call when applying a command with args, indexed by command type diff --git a/tests/utestitest_dsl.lua b/tests/utestitest_dsl.lua index 62babbdc..04c23daa 100644 --- a/tests/utestitest_dsl.lua +++ b/tests/utestitest_dsl.lua @@ -14,6 +14,7 @@ local integrationtest = require("engine/test/integrationtest") local itest_manager, time_trigger, integration_test = get_members(integrationtest, "itest_manager", "time_trigger", "integration_test") local flow = require("engine/application/flow") +local input = require("engine/input/input") local gameapp = require("game/application/gameapp") local gamestate = require("game/application/gamestate") local stage = require("game/ingame/stage") @@ -136,6 +137,20 @@ describe('itest_dsl', function () end) + describe('parse_button_id', function () + + it('should assert when the number of arguments is wrong', function () + assert.has_error(function () + itest_dsl.parse_button_id({"too", "many"}) + end, "parse_button_id: got 2 args, expected 1") + end) + + it('should return the single argument as motion mode', function () + assert.are_equal(button_ids.o, itest_dsl.parse_button_id({"o"})) + end) + + end) + describe('parse_motion_state', function () it('should assert when the number of arguments is wrong', function () @@ -255,6 +270,26 @@ describe('itest_dsl', function () end) + describe('execute_press', function () + + it('should set the simulated button down state to true', function () + input.simulated_buttons_down[0][button_ids.x] = false + itest_dsl.execute_press({button_ids.x}) + assert.is_true(input.simulated_buttons_down[0][button_ids.x]) + end) + + end) + + describe('execute_release', function () + + it('should set the simulated button down state to true', function () + input.simulated_buttons_down[0][button_ids.up] = true + itest_dsl.execute_release({button_ids.up}) + assert.is_false(input.simulated_buttons_down[0][button_ids.up]) + end) + + end) + end) describe('eval_', function () From 4a19a11a2f913cb53086cc6868a9936a4c573cc6 Mon Sep 17 00:00:00 2001 From: hsandt Date: Wed, 27 Feb 2019 01:01:01 +0000 Subject: [PATCH 052/140] [INPUT] Consume jump intention to support puppet control mode (and input update at different frequencies than physics update) --- src/game/ingame/playercharacter.lua | 3 ++- tests/utestplayercharacter.lua | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/game/ingame/playercharacter.lua b/src/game/ingame/playercharacter.lua index 29f16aa9..25068d94 100644 --- a/src/game/ingame/playercharacter.lua +++ b/src/game/ingame/playercharacter.lua @@ -672,7 +672,8 @@ end -- depending whether input is hold or not function player_char:_check_jump_intention() if self.jump_intention then - -- jump_intention is set each frame, no need to consume it here + -- consume intention so puppet control mode (which is sticky) also works + self.jump_intention = false self.should_jump = true end end diff --git a/tests/utestplayercharacter.lua b/tests/utestplayercharacter.lua index 02bb0592..61d891fa 100644 --- a/tests/utestplayercharacter.lua +++ b/tests/utestplayercharacter.lua @@ -2570,10 +2570,10 @@ describe('player_char', function () assert.are_same({false, false}, {pc.jump_intention, pc.should_jump}) end) - it('should *not* consume jump_intention and set should_jump to true if jump_intention is true', function () + it('should consume jump_intention and set should_jump to true if jump_intention is true', function () pc.jump_intention = true pc:_check_jump_intention() - assert.are_same({true, true}, {pc.jump_intention, pc.should_jump}) + assert.are_same({false, true}, {pc.jump_intention, pc.should_jump}) end) end) From 9a12a25e773ab4ccf83cbac8fe54e297df74da9a Mon Sep 17 00:00:00 2001 From: hsandt Date: Wed, 27 Feb 2019 01:02:37 +0000 Subject: [PATCH 053/140] [TEST] DSL: converted "ignore hold jump landing" by simulating input --- src/game/itests/itestplayercharacter.lua | 112 ++++++----------------- 1 file changed, 30 insertions(+), 82 deletions(-) diff --git a/src/game/itests/itestplayercharacter.lua b/src/game/itests/itestplayercharacter.lua index 39c53014..a4b4addc 100644 --- a/src/game/itests/itestplayercharacter.lua +++ b/src/game/itests/itestplayercharacter.lua @@ -341,41 +341,38 @@ expect pc_velocity 0 -0.078125 -- at frame 62: pos (4, 80), velocity (0, 0), grounded -> has landed ---[[ - --- if the player presses the jump button in mid-air, the character should not --- jump again when he lands on the ground (later, it will trigger a special action) -itest = integration_test('platformer no predictive jump in air', {stage.state.type}) -itest_manager:register(itest) - -itest.setup = function () - setup_map_data() - - -- add tiles where the character will move - mset(0, 10, 64) +itest_dsl_parser.register( + 'ignore hold jump landing', [[ +@stage # +. +# - flow:change_gamestate_by_type(stage.state.type) +warp 4 8 +jump +stop_jump +wait 20 +set_control_mode human +press o +wait 20 - -- respawn character on the ground (important to always start with grounded state) - stage.state.player_char:spawn_at(vector(4., 80. - pc_data.center_height_standing)) -- set bottom y at 80 - -- this is an end-to-end test because we don't want bother with how mid-air predicitve jump order is ignored - -- indeed, if it is ignored by ignoring the input itself, then hijacking the jump_intention - -- in puppet mode will prove nothing - -- if it is ignored by resetting the jump intention on land, the puppet test would be useful - -- to show that the intention itself is reset, but here we only want to ensure the end-to-end behavior is correct - -- so we us a human control mode and hijack the input directly - stage.state.player_char.control_mode = control_modes.human - stage.state.player_char.motion_mode = motion_modes.platformer +expect pc_bottom_pos 4 8 +expect pc_motion_state grounded +expect pc_ground_spd 0 +expect pc_velocity 0 0 +]]) - -- start hop - input.simulated_buttons_down[0][button_ids.o] = true -end +-- if the player presses the jump button in mid-air, the character should not +-- jump again when he lands on the ground (later, it will trigger a special action) -itest.teardown = function () - clear_map() - teardown_map_data() -end +-- input note: +-- this is an end-to-end test because we don't want bother with how mid-air predicitve jump order is ignored +-- indeed, if it is ignored by ignoring the input itself, then hijacking the jump_intention +-- in puppet mode will prove nothing +-- if it is ignored by resetting the jump intention on land, the puppet test would be useful +-- to show that the intention itself is reset, but here we only want to ensure the end-to-end behavior is correct +-- so we us a human control mode and hijack the input directly +-- calculation notes: -- wait for apogee (frame 20) and stop -- at frame 1: bpos (4, 80), velocity (0, 0), grounded (waits 1 frame before confirming hop/jump) -- at frame 2: bpos (4, 80 - 2), velocity (0, -2), airborne (hop confirmed) @@ -386,59 +383,10 @@ end -- at frame 38: pos (4, 80 - 1.15625), velocity (0, 1.9375), airborne -> about to land -- at frame 39: pos (4, 80), velocity (0, 0), grounded -> has landed --- end of frame 2: end short press for a hop -itest:add_action(time_trigger(1, true), function () - input.simulated_buttons_down[0][button_ids.o] = false -end) - --- frame bug: it seems that 1+19!=20, time_trigger(1) is just ignored and it will give frame 19 --- end of frame 20: at the jump apogee, try another jump press -itest:add_action(time_trigger(19, true), function () - input.simulated_buttons_down[0][button_ids.o] = true -end) - --- end of frame 21: end short press -itest:add_action(time_trigger(1, true), function () - input.simulated_buttons_down[0][button_ids.o] = false -end) - --- frame bug: it seems that character will be on ground during frames +16 and +17 --- not sure why since he only needs 1 frame to confirm a hop --- wait for character to land (frame 39) and see if he hops again --- for now, to be safe we +19 -> frame 40, but actually supposedly frame 39 also works due to frame bug mentioned above --- frame 40: character should still be on ground, not re-jump -itest:add_action(time_trigger(19, true), function () -end) - --- check that player char has moved to the right and fell -itest.final_assertion = function () - local is_motion_state_expected, motion_state_message = motion_states.grounded == stage.state.player_char.motion_state, "Expected motion state 'airborne', got "..stage.state.player_char.motion_state - local is_position_expected, position_message = almost_eq_with_message(vector(4, 80.), stage.state.player_char:get_bottom_center(), 1/256) - local is_ground_speed_expected, ground_speed_message = almost_eq_with_message(0, stage.state.player_char.ground_speed, 1/256) - local is_velocity_expected, velocity_message = almost_eq_with_message(vector(0, 0), stage.state.player_char.velocity, 1/256) - - local final_message = "" - - local success = is_position_expected and is_ground_speed_expected and is_velocity_expected and is_motion_state_expected - if not success then - if not is_motion_state_expected then - final_message = final_message..motion_state_message.."\n" - end - if not is_position_expected then - final_message = final_message..position_message.."\n" - end - if not is_ground_speed_expected then - final_message = final_message..ground_speed_message.."\n" - end - if not is_velocity_expected then - final_message = final_message..velocity_message.."\n" - end - - end - - return success, final_message -end +-- and wait an extra frame to see if Sonic will jump due to holding jump input, +-- so stop at frame 40 +--[[ itest = integration_test('platformer jump air accel', {stage.state.type}) itest_manager:register(itest) From fcc4915d184183b86ee464317fa315b13184953c Mon Sep 17 00:00:00 2001 From: hsandt Date: Thu, 28 Feb 2019 21:18:49 +0000 Subject: [PATCH 054/140] [TEST] DSL: converted "platformer jump air accel" --- src/game/itests/itestplayercharacter.lua | 88 +++++++----------------- 1 file changed, 25 insertions(+), 63 deletions(-) diff --git a/src/game/itests/itestplayercharacter.lua b/src/game/itests/itestplayercharacter.lua index a4b4addc..ed65d6fe 100644 --- a/src/game/itests/itestplayercharacter.lua +++ b/src/game/itests/itestplayercharacter.lua @@ -386,82 +386,44 @@ expect pc_velocity 0 0 -- and wait an extra frame to see if Sonic will jump due to holding jump input, -- so stop at frame 40 ---[[ -itest = integration_test('platformer jump air accel', {stage.state.type}) -itest_manager:register(itest) -itest.setup = function () - setup_map_data() - - -- add tiles where the character will move - mset(0, 10, 64) +itest_dsl_parser.register( + 'platformer jump air accel', [[ +@stage # +. +# - flow:change_gamestate_by_type(stage.state.type) +warp 4 8 +jump +wait 2 +move right +wait 29 - -- respawn character on the ground (important to always start with grounded state) - stage.state.player_char:spawn_at(vector(4., 80. - pc_data.center_height_standing)) -- set bottom y at 80 - stage.state.player_char.control_mode = control_modes.puppet - stage.state.player_char.motion_mode = motion_modes.platformer +expect pc_bottom_pos 24.390625 -41.921875 +expect pc_motion_state airborne +expect pc_ground_spd 0 +expect pc_velocity 1.359375 -0.078125 +]]) - -- start full jump and immediately try to move right - stage.state.player_char.jump_intention = true -- will be consumed - stage.state.player_char.hold_jump_intention = true -end -itest.teardown = function () - clear_map() - teardown_map_data() -end +-- calculation notes -- wait 2 frame (1 to register jump, 1 to confirm and leave ground) then move to the right -- this is just to avoid starting moving on the ground, as we only want to test air control here, -- not how ground speed is transferred to air velocity -itest:add_action(time_trigger(2, true), function () - stage.state.player_char.move_intention = vector(1, 0) -end) - -- wait for the apogee (frame 31) and stop --- at frame 1: pos (4, 80), velocity (0, 0), grounded (waits 1 frame before confirming hop/jump) --- at frame 2: pos (4, 80 - 3.25), velocity (0, -3.25), airborne (do not apply gravity on first frame of jump, no air accel yet) --- at frame 3: pos (4 + 0.046875, 80 - 49.84375), velocity (0.046875, -3.140625), airborne -> accel forward --- at frame 30: pos (4 + 19.03125, 80 - 49.84375), velocity (1.3125, -0.1875), airborne -> before apogee --- at frame 31: pos (4 + 20.390625, 80 - 49.921875), velocity (1.359375, -0.078125), airborne -> reached apogee (100px in 16-bit, matches SPG on Jumping) --- at frame 32: pos (4 + 21.796875, 80 - 49.890625), velocity (1.40625, 0.03125), airborne -> starts going down --- at frame 61: pos (4 + 82.96875, 80 - 1.40625), velocity (2.765625, 3.203125), airborne -> about to land --- at frame 62: pos (4 + 85.78125, 80), velocity (2.8125, 0), grounded -> has landed, preserve x speed -itest:add_action(time_trigger(29, true), function () end) - --- check that player char has moved to the right and fell -itest.final_assertion = function () - local is_motion_state_expected, motion_state_message = motion_states.airborne == stage.state.player_char.motion_state, "Expected motion state 'airborne', got "..stage.state.player_char.motion_state - local is_position_expected, position_message = almost_eq_with_message(vector(4 + 20.390625, 80 - 49.921875), stage.state.player_char:get_bottom_center(), 1/256) - local is_ground_speed_expected, ground_speed_message = almost_eq_with_message(0, stage.state.player_char.ground_speed, 1/256) - local is_velocity_expected, velocity_message = almost_eq_with_message(vector(1.359375, -0.078125), stage.state.player_char.velocity, 1/256) +-- at frame 1: pos (4, 8), velocity (0, 0), grounded (waits 1 frame before confirming hop/jump) +-- at frame 2: pos (4, 8 - 3.25), velocity (0, -3.25), airborne (do not apply gravity on first frame of jump, no air accel yet) +-- at frame 3: pos (4 + 0.046875, 8 - 49.84375), velocity (0.046875, -3.140625), airborne -> accel forward +-- at frame 30: pos (4 + 19.03125, 8 - 49.84375), velocity (1.3125, -0.1875), airborne -> before apogee +-- at frame 31: pos (4 + 20.390625, 8 - 49.921875), velocity (1.359375, -0.078125), airborne -> reached apogee (100px in 16-bit, matches SPG on Jumping) +-- at frame 32: pos (4 + 21.796875, 8 - 49.890625), velocity (1.40625, 0.03125), airborne -> starts going down +-- at frame 61: pos (4 + 82.96875, 8 - 1.40625), velocity (2.765625, 3.203125), airborne -> about to land +-- at frame 62: pos (4 + 85.78125, 8), velocity (2.8125, 0), grounded -> has landed, preserve x speed - local final_message = "" +-- check for apogee - local success = is_position_expected and is_ground_speed_expected and is_velocity_expected and is_motion_state_expected - if not success then - if not is_motion_state_expected then - final_message = final_message..motion_state_message.."\n" - end - if not is_position_expected then - final_message = final_message..position_message.."\n" - end - if not is_ground_speed_expected then - final_message = final_message..ground_speed_message.."\n" - end - if not is_velocity_expected then - final_message = final_message..velocity_message.."\n" - end - - end - - return success, final_message -end - ---]] --[[ From 328771425f6af632b5a2e5ea7624d026a9e2c403 Mon Sep 17 00:00:00 2001 From: hsandt Date: Thu, 28 Feb 2019 21:36:32 +0000 Subject: [PATCH 055/140] [TEST] DSL: converted "platformer ground wall block right" --- src/game/itests/itestplayercharacter.lua | 87 ++++++------------------ 1 file changed, 19 insertions(+), 68 deletions(-) diff --git a/src/game/itests/itestplayercharacter.lua b/src/game/itests/itestplayercharacter.lua index ed65d6fe..b4b8afa3 100644 --- a/src/game/itests/itestplayercharacter.lua +++ b/src/game/itests/itestplayercharacter.lua @@ -425,81 +425,32 @@ expect pc_velocity 1.359375 -0.078125 -- check for apogee ---[[ - -- bugfix history: -- + revealed that spawn_at was not resetting state vars, so added _setup method -itest = integration_test('platformer ground wall block right', {stage.state.type}) -itest_manager:register(itest) - -itest.setup = function () - setup_map_data() - - -- X - -- XXX - mset(0, 10, 64) -- to walk on - mset(1, 10, 64) -- to walk on - mset(2, 10, 64) -- for now, we need supporting block - mset(2, 9, 64) -- blocking wall - - flow:change_gamestate_by_type(stage.state.type) - - -- respawn character on the ground (important to always start with grounded state) - stage.state.player_char:spawn_at(vector(4., 80. - pc_data.center_height_standing)) -- set bottom y at 80 - stage.state.player_char.control_mode = control_modes.puppet - stage.state.player_char.motion_mode = motion_modes.platformer - - -- start moving to the right from frame 0 by setting intention in setup - stage.state.player_char.move_intention = vector(1, 0) -end - -itest.teardown = function () - clear_map() - teardown_map_data() -end - - --- wait 29 frames and stop --- to compute position x from x0 after n frames at accel a from speed s0: x = x0 + n*s0 + n(n+1)/2*a --- to compute speed s from s0 after n frames at accel a: x = s0 + n*a --- character will be blocked when right wall sensor is at x = 16, so when center is at x = 12 --- at frame 1: pos (4 + 0.0234375, 80), velocity (0.0234375, 0), grounded --- at frame 27: pos (12.8359375, 80 - 8), velocity (0.6328125, 0), about to meet wall --- at frame 28: pos (13, 80 - 8), velocity (0, 0), hit wall -itest:add_action(time_trigger(28, true), function () end) - --- check that player char has moved to the right and is still on the ground -itest.final_assertion = function () - local is_motion_state_expected, motion_state_message = motion_states.grounded == stage.state.player_char.motion_state, "Expected motion state 'grounded', got "..stage.state.player_char.motion_state - -- to compute position x from x0 after n frames at accel a from speed s0: x = x0 + n*s0 + n(n+1)/2*a - local is_position_expected, position_message = almost_eq_with_message(vector(13., 80.), stage.state.player_char:get_bottom_center(), 1/256) - -- to compute speed s from s0 after n frames at accel a: x = s0 + n*a - local is_ground_speed_expected, ground_speed_message = almost_eq_with_message(0, stage.state.player_char.ground_speed, 1/256) - local is_velocity_expected, velocity_message = almost_eq_with_message(vector(0, 0), stage.state.player_char.velocity, 1/256) +itest_dsl_parser.register( + 'platformer ground wall block right', [[ +@stage # +..# +##. - local final_message = "" +warp 4 8 +move right +wait 28 - local success = is_position_expected and is_ground_speed_expected and is_velocity_expected and is_motion_state_expected - if not success then - if not is_motion_state_expected then - final_message = final_message..motion_state_message.."\n" - end - if not is_position_expected then - final_message = final_message..position_message.."\n" - end - if not is_ground_speed_expected then - final_message = final_message..ground_speed_message.."\n" - end - if not is_velocity_expected then - final_message = final_message..velocity_message.."\n" - end +expect pc_bottom_pos 13 8 +expect pc_motion_state grounded +expect pc_ground_spd 0 +expect pc_velocity 0 0 +]]) - end +-- calculation notes - return success, final_message -end +-- wait 28 frames and stop +-- character will be blocked when right wall sensor is at x = 16.5, so when center will be at x = 13 ---]] +-- at frame 1: pos (4 + 0.0234375, 8), velocity (0.0234375, 0), grounded +-- at frame 27: pos (12.8359375, 8), velocity (0.6328125, 0), about to meet wall +-- at frame 28: pos (13, 8), velocity (0, 0), hit wall --[[ From 10f16f9b359cd681e6d8a5b61e4d87771479b71c Mon Sep 17 00:00:00 2001 From: hsandt Date: Thu, 28 Feb 2019 23:04:39 +0000 Subject: [PATCH 056/140] [TILEMAP] Data: Added 2 slope variants --- src/game/data/tile_data.lua | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/game/data/tile_data.lua b/src/game/data/tile_data.lua index 2c4ee32a..926a2a21 100644 --- a/src/game/data/tile_data.lua +++ b/src/game/data/tile_data.lua @@ -3,9 +3,11 @@ local tile_test_data = require("game/test_data/tile_test_data") tile_symbol_to_ids = { - ['.'] = 0, -- empty - ['#'] = 64, -- full tile - ['/'] = 65, -- ascending slope 45 + ['.'] = 0, -- empty + ['#'] = 64, -- full tile + ['/'] = 65, -- ascending slope 45 + ['\\'] = 66, -- descending slope 45 + ['<'] = 67, -- ascending slope 22.5 } -- for itests that need map setup, we exceptionally not teardown From 919c2f77c6b1f6129ab230a4b8d923a045756111 Mon Sep 17 00:00:00 2001 From: hsandt Date: Thu, 28 Feb 2019 23:06:13 +0000 Subject: [PATCH 057/140] [TEST] DSL: Added set command for velocity and ground speed --- prebuild/replace_strings.py | 23 +++++++------- src/engine/test/itest_dsl.lua | 48 +++++++++++++++++++++++------- tests/utestitest_dsl.lua | 56 ++++++++++++++++++++++++++++++++--- 3 files changed, 101 insertions(+), 26 deletions(-) diff --git a/prebuild/replace_strings.py b/prebuild/replace_strings.py index f241f4ae..b53ee845 100755 --- a/prebuild/replace_strings.py +++ b/prebuild/replace_strings.py @@ -131,21 +131,22 @@ 'motion_mode': 6, 'motion_state': 7, 'button_id': 8, - 'expect': 9, + 'gp_value': 9, }, 'command_types': { 'warp': 1, - 'set_control_mode': 2, - 'set_motion_mode': 3, - 'move': 4, - 'stop': 5, - 'jump': 6, - 'stop_jump': 7, - 'press': 8, - 'release': 9, - 'wait': 10, - 'expect': 11, + 'set': 2, + 'set_control_mode': 3, + 'set_motion_mode': 4, + 'move': 5, + 'stop': 6, + 'jump': 7, + 'stop_jump': 8, + 'press': 9, + 'release': 10, + 'wait': 11, + 'expect': 12, }, 'gp_value_types': { diff --git a/src/engine/test/itest_dsl.lua b/src/engine/test/itest_dsl.lua index 338ed3e8..95b3f786 100644 --- a/src/engine/test/itest_dsl.lua +++ b/src/engine/test/itest_dsl.lua @@ -66,6 +66,7 @@ end -- optimize tokens: if this is too much, remove proxy function tables -- altogether and directly access functions via itest_dsl[prefix..type_name] +-- (this requires to keep the enum_strings table in config with #itest) -- return table containing functions named {prefix}{enum_type_name} -- inside a module, indexed by enum value local function generate_function_table(module, enum_types, prefix) @@ -89,7 +90,7 @@ parsable_types = enum { "motion_mode", "motion_state", "button_id", - "expect", -- meta-type meaning we must check the 1st arg (gp_value_type) to know what the rest should be + "gp_value", -- meta-type compounded of [gp_value_type, gp_value_args...] where gp_value_args depend on gp_value_type } --#if assert @@ -100,6 +101,7 @@ parsable_type_strings = invert_table(parsable_types) -- type of commands available command_types = enum { "warp", -- warp player character bottom args: {bottom_position: vector} + "set", -- set gameplay value args: {gp_value_type: gp_value_types, new_value_args...: matching gp value parsable type} "set_control_mode", -- set control mode args: {control_mode_str: control_modes key} "set_motion_mode", -- set motion mode args: {motion_mode_str: motion_modes key} "move", -- set sticky pc move intention args: {move_dir_str: horizontal_dirs key} @@ -120,6 +122,7 @@ command_type_strings = invert_table(command_types) -- argument types expected after those commands command_arg_types = { [command_types.warp] = parsable_types.vector, + [command_types.set] = parsable_types.gp_value, [command_types.set_control_mode] = parsable_types.control_mode, [command_types.set_motion_mode] = parsable_types.motion_mode, [command_types.move] = parsable_types.horizontal_dir, @@ -129,7 +132,7 @@ command_arg_types = { [command_types.press] = parsable_types.button_id, [command_types.release] = parsable_types.button_id, [command_types.wait] = parsable_types.number, - [command_types.expect] = parsable_types.expect, + [command_types.expect] = parsable_types.gp_value, } @@ -208,16 +211,15 @@ function itest_dsl.parse_button_id(arg_strings) return button_ids[arg_strings[1]] end --- convert string args to vector -function itest_dsl.parse_expect(arg_strings) - assert(#arg_strings > 1, "parse_expect: got "..#arg_strings.." args, expected at least 2") +function itest_dsl.parse_gp_value(arg_strings) + assert(#arg_strings > 1, "parse_gp_value: got "..#arg_strings.." args, expected at least 2") -- same principle as itest_dsl_parser.parse, the type of the first arg -- determines how we parse the rest of the args, named "value components" local gp_value_type_str = arg_strings[1] -- gather all the value components as strings (e.g. {"3", "4"} for vector(3, 4)) - local expected_value_comps = {} + local gp_value_comps = {} for i = 2, #arg_strings do - add(expected_value_comps, arg_strings[i]) + add(gp_value_comps, arg_strings[i]) end -- determine the type of value reference tested for comparison (e.g. pc position) local gp_value_type = gp_value_types[gp_value_type_str] @@ -225,10 +227,10 @@ function itest_dsl.parse_expect(arg_strings) -- parse the value components to semantical type (e.g. vector) local gp_value_data = gp_value_data_t[gp_value_type] assert(gp_value_data, "gp_value_data_t["..gp_value_type.."] (for '"..gp_value_type_str.."') is not defined") - local expected_value_parser = value_parsers[gp_value_data.parsable_type] - assert(expected_value_parser, "no value parser defined for gp value type '"..parsable_type_strings[gp_value_data.parsable_type].."'") - local expected_value = expected_value_parser(expected_value_comps) - return gp_value_type, expected_value + local gp_value_parser = value_parsers[gp_value_data.parsable_type] + assert(gp_value_parser, "no value parser defined for gp value type '"..parsable_type_strings[gp_value_data.parsable_type].."'") + local gp_value = gp_value_parser(gp_value_comps) + return gp_value_type, gp_value end -- table of parsers for command args and gameplay values, indexed by parsed type @@ -243,6 +245,18 @@ function itest_dsl.execute_warp(args) stage.state.player_char:warp_bottom_to(args[1]) end +function itest_dsl.execute_set(args) + local gp_value_type = args[1] + local new_gp_value = args[2] + + -- if you remove *all* generate_function_table, it's worth having parse_gp_value + -- return a gp_value_type_str rather than an index to avoid going back and forth + -- between key and value + local setter = itest_dsl["set_"..gp_value_type_strings[gp_value_type]] + assert(setter, "itest_dsl.set_"..gp_value_type_strings[gp_value_type].." is not defined") + setter(new_gp_value) +end + function itest_dsl.execute_set_control_mode(args) stage.state.player_char.control_mode = args[1] end @@ -311,6 +325,18 @@ end evaluators = generate_function_table(itest_dsl, gp_value_types, "eval_") itest_dsl.evaluators = evaluators + +-- gameplay value setters (only when setting value directly makes sense) + +function itest_dsl.set_pc_velocity(value) + stage.state.player_char.velocity = value +end + +function itest_dsl.set_pc_ground_spd(value) + stage.state.player_char.ground_speed = value +end + + -- command struct -- attributes diff --git a/tests/utestitest_dsl.lua b/tests/utestitest_dsl.lua index 04c23daa..72c0604c 100644 --- a/tests/utestitest_dsl.lua +++ b/tests/utestitest_dsl.lua @@ -165,17 +165,17 @@ describe('itest_dsl', function () end) - describe('parse_expect', function () + describe('parse_gp_value', function () it('should assert when the number of arguments is wrong', function () assert.has_error(function () - itest_dsl.parse_expect({"too few"}) - end, "parse_expect: got 1 args, expected at least 2") + itest_dsl.parse_gp_value({"too few"}) + end, "parse_gp_value: got 1 args, expected at least 2") end) it('should return the gameplay value type and the expected value, itself recursively parsed', function () assert.are_same({gp_value_types.pc_bottom_pos, vector(1, 3)}, - {itest_dsl.parse_expect({"pc_bottom_pos", "1", "3"})}) + {itest_dsl.parse_gp_value({"pc_bottom_pos", "1", "3"})}) end) end) @@ -213,6 +213,21 @@ describe('itest_dsl', function () end) + describe('"execute_set', function () + + it('should set pc velocity to (1, -3)', function () + itest_dsl.execute_set({gp_value_types.pc_velocity, vector(1, -3)}) + assert.are_equal(vector(1, -3), stage.state.player_char.velocity) + end) + + it('should fail with unsupported gp_value_type for setting', function () + assert.has_error(function () + itest_dsl.execute_set({gp_value_types.pc_slope, -2}) + end, "itest_dsl.set_pc_slope is not defined") + end) + + end) + describe('execute_set_control_mode', function () it('should set the control mode', function () @@ -352,6 +367,39 @@ describe('itest_dsl', function () end) + describe('set_', function () + + before_each(function () + -- some setters require the player character + stage.state.player_char = player_char() + end) + + after_each(function () + -- clean up dummy player character + stage.state:init() + end) + + describe('set_pc_velocity', function () + + it('should return the velocity the current player character', function () + itest_dsl.set_pc_velocity(vector(1, -4)) + assert.are_equal(vector(1, -4), stage.state.player_char.velocity) + end) + + end) + + describe('set_pc_ground_spd', function () + + it('should return the ground speed current player character', function () + itest_dsl.set_pc_ground_spd(3.5) + assert.are_equal(3.5, stage.state.player_char.ground_speed) + end) + + end) + + end) + + describe('command', function () describe('_init', function () From d6ef48ef7d3c3e6c0e4c4508eef66548656c898a Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 1 Mar 2019 00:12:35 +0000 Subject: [PATCH 058/140] [TEST] DSL: Converted "blocked by wall" tests --- src/game/itests/itestplayercharacter.lua | 234 +++++++++-------------- 1 file changed, 94 insertions(+), 140 deletions(-) diff --git a/src/game/itests/itestplayercharacter.lua b/src/game/itests/itestplayercharacter.lua index b4b8afa3..42beecd2 100644 --- a/src/game/itests/itestplayercharacter.lua +++ b/src/game/itests/itestplayercharacter.lua @@ -38,6 +38,11 @@ expect pc_bottom_pos 0x0038.b7f1 8 -- ground motion +-- common calculation notes: +-- to compute position x from x0 after n frames at accel a from speed s0: x = x0 + n*s0 + n(n+1)/2*a +-- to compute speed s from s0 after n frames at accel a: x = s0 + n*a + + -- bugfix history: -- . test was wrong, initialize in setup, not at time trigger 0 itest_dsl_parser.register( @@ -150,6 +155,95 @@ expect pc_velocity 0x0000.2fa4 -0x0000.2fa5 -- at frame 15: bpos (6.519668501758, 15), velocity (0.1860961140625, -0.1860961140625), ground_speed(0.26318359375), still under slope factor effect and velocity following slope tangent +-- calculation notes + +-- wait 2 frame (1 to register jump, 1 to confirm and leave ground) then move to the right +-- this is just to avoid starting moving on the ground, as we only want to test air control here, +-- not how ground speed is transferred to air velocity + +-- wait for the apogee (frame 31) and stop +-- at frame 1: pos (4, 8), velocity (0, 0), grounded (waits 1 frame before confirming hop/jump) +-- at frame 2: pos (4, 8 - 3.25), velocity (0, -3.25), airborne (do not apply gravity on first frame of jump, no air accel yet) +-- at frame 3: pos (4 + 0.046875, 8 - 49.84375), velocity (0.046875, -3.140625), airborne -> accel forward +-- at frame 30: pos (4 + 19.03125, 8 - 49.84375), velocity (1.3125, -0.1875), airborne -> before apogee +-- at frame 31: pos (4 + 20.390625, 8 - 49.921875), velocity (1.359375, -0.078125), airborne -> reached apogee (100px in 16-bit, matches SPG on Jumping) +-- at frame 32: pos (4 + 21.796875, 8 - 49.890625), velocity (1.40625, 0.03125), airborne -> starts going down +-- at frame 61: pos (4 + 82.96875, 8 - 1.40625), velocity (2.765625, 3.203125), airborne -> about to land +-- at frame 62: pos (4 + 85.78125, 8), velocity (2.8125, 0), grounded -> has landed, preserve x speed + +-- check for apogee + + +-- bugfix history: +-- + revealed that spawn_at was not resetting state vars, so added _setup method +itest_dsl_parser.register( + 'platformer ground wall block right', [[ +@stage # +..# +##. + +warp 4 8 +move right +wait 28 + +expect pc_bottom_pos 13 8 +expect pc_motion_state grounded +expect pc_ground_spd 0 +expect pc_velocity 0 0 +]]) + +-- calculation notes + +-- wait 28 frames and stop +-- character will be blocked when right wall sensor is at x = 16.5, so when center will be at x = 13 + +-- at frame 1: pos (4 + 0.0234375, 8), velocity (0.0234375, 0), grounded +-- at frame 27: pos (12.8359375, 8), velocity (0.6328125, 0), about to meet wall +-- at frame 28: pos (13, 8), velocity (0, 0), hit wall + + +itest_dsl_parser.register( + '#solo platformer slope ceiling block right', [[ +@stage # +..# +.<. +#.. + +warp 4 16 +set pc_ground_spd 3 +move right +wait 4 + +expect pc_bottom_pos 13 11 +expect pc_motion_state grounded +expect pc_ground_spd 0 +expect pc_velocity 0 0 +]]) + + +-- calculation notes + +-- ground speed start at 40 for fast startup (velocity will be updated on first frame) + +-- wait 29 frames and stop + +-- character will be blocked when right wall sensor is at x = 16.5, so when center will be at x = 13 + +-- if move intention is applied after slope factor (or both are applied, then ground speed is clamped as we should): +-- at frame 1: pos (7, 14), velocity (3, 0), grounded +-- at frame 2: pos (7 + 0x0002.c589 = 9.771621704102, 13), velocity (3, 0), grounded +-- at frame 3: pos (7 + 2 * 0x0002.c589 = 12.543243408204, 11), velocity (3, 0), grounded +-- at frame 4: pos (13, 11), velocity (3, 0), grounded + +-- in practice, slope after is applied after intention, causing a slight decel: + +-- frame 2: ground speed 2.9995 +-- frame 3: ground speed 2.9991 + +-- however, this strongly depends on the slope factor x intention combination before clamping +-- and is likely to change, so no need to test this far for being blocked by the final ceiling + + -- air motion -- bugfix history: @@ -406,146 +500,6 @@ expect pc_velocity 1.359375 -0.078125 ]]) --- calculation notes - --- wait 2 frame (1 to register jump, 1 to confirm and leave ground) then move to the right --- this is just to avoid starting moving on the ground, as we only want to test air control here, --- not how ground speed is transferred to air velocity - --- wait for the apogee (frame 31) and stop --- at frame 1: pos (4, 8), velocity (0, 0), grounded (waits 1 frame before confirming hop/jump) --- at frame 2: pos (4, 8 - 3.25), velocity (0, -3.25), airborne (do not apply gravity on first frame of jump, no air accel yet) --- at frame 3: pos (4 + 0.046875, 8 - 49.84375), velocity (0.046875, -3.140625), airborne -> accel forward --- at frame 30: pos (4 + 19.03125, 8 - 49.84375), velocity (1.3125, -0.1875), airborne -> before apogee --- at frame 31: pos (4 + 20.390625, 8 - 49.921875), velocity (1.359375, -0.078125), airborne -> reached apogee (100px in 16-bit, matches SPG on Jumping) --- at frame 32: pos (4 + 21.796875, 8 - 49.890625), velocity (1.40625, 0.03125), airborne -> starts going down --- at frame 61: pos (4 + 82.96875, 8 - 1.40625), velocity (2.765625, 3.203125), airborne -> about to land --- at frame 62: pos (4 + 85.78125, 8), velocity (2.8125, 0), grounded -> has landed, preserve x speed - --- check for apogee - - --- bugfix history: --- + revealed that spawn_at was not resetting state vars, so added _setup method -itest_dsl_parser.register( - 'platformer ground wall block right', [[ -@stage # -..# -##. - -warp 4 8 -move right -wait 28 - -expect pc_bottom_pos 13 8 -expect pc_motion_state grounded -expect pc_ground_spd 0 -expect pc_velocity 0 0 -]]) - --- calculation notes - --- wait 28 frames and stop --- character will be blocked when right wall sensor is at x = 16.5, so when center will be at x = 13 - --- at frame 1: pos (4 + 0.0234375, 8), velocity (0.0234375, 0), grounded --- at frame 27: pos (12.8359375, 8), velocity (0.6328125, 0), about to meet wall --- at frame 28: pos (13, 8), velocity (0, 0), hit wall - ---[[ - -itest = integration_test('platformer slope wall block right', {stage.state.type}) -itest_manager:register(itest) - -itest.setup = function () - setup_map_data() - - mset(0, 10, 64) -- to walk on - mset(1, 10, 64) -- support ground for slope - mset(1, 9, 67) -- ascending slope 22.5 to walk on - mset(2, 8, 64) -- blocking wall at the top of the slope - - flow:change_gamestate_by_type(stage.state.type) - - -- respawn character on the ground (important to always start with grounded state) - stage.state.player_char:spawn_at(vector(4., 80. - pc_data.center_height_standing)) -- set bottom y at 80 - stage.state.player_char.control_mode = control_modes.puppet - stage.state.player_char.motion_mode = motion_modes.platformer - - -- start moving to the right from frame 0 by setting intention in setup - stage.state.player_char.move_intention = vector(1, 0) - -- cheat for fast startup (velocity will be updated on first frame) - stage.state.player_char.ground_speed = 40 -end - -itest.teardown = function () - clear_map() - teardown_map_data() -end - --- wait 29 frames and stop - --- to compute position x from x0 after n frames at accel a from speed s0: x = x0 + n*s0 + n(n+1)/2*a --- to compute speed s from s0 after n frames at accel a: x = s0 + n*a --- character will be blocked when right wall sensor is at x = 16, so when center is at x = 12 --- remember character must reach x=13 (not visible, inside frame calculation) to detect the wall, then snap to 12! --- at frame 1: pos (4 + 0.0234375, 80), velocity (0.0234375, 0), grounded - --- at frame 12: bpos (5.828125, 80), velocity (0.28125, 0), ground_speed(0.28125) --- at frame 13: bpos (6.1328125, 79), velocity (0.3046875, 0), ground_speed(0.3046875), first step on slope and at higher level than flat ground, acknowledge slope as current ground --- at frame 14: bpos (6.333572387695, 79), velocity (0.2007598876953125, -0.2007598876953125), ground_speed(0.283935546875), because slope was current ground at frame start, slope factor was applied with 0.0625*sin(45) = -0.044189453125 (in PICO-8 16.16 fixed point precision) --- at frame 15: bpos (6.519668501758, 79), velocity (0.1860961140625, -0.1860961140625), ground_speed(0.26318359375), still under slope factor effect and velocity following slope tangent --- problem: with slope 45, character slows down and never get past x=7 --- instead, we just cheat and add an extra speed, then just check the final position after a long time enough to reach the block at the top - --- at frame 27: pos (12.8359375, 80 - 8), velocity (0.6328125, 0), about to meet wall --- at frame 28: pos (13, 80 - 8), velocity (0, 0), hit wall - --- at frame 1: bpos (4.0234375, 80), velocity (0.0234375, 0), ground_speed(0.0234375) --- at frame 9: bpos (5.0546875, 80), velocity (0.2109375, 0), ground_speed(0.2109375) --- at frame 10: bpos (5.2890625, 80), velocity (0.234375, 0), ground_speed(0.234375) --- at frame 11: bpos (5.546875, 80), velocity (0.2578125, 0), ground_speed(0.2578125) - --- even at 22.5, character doesn't manage to climb up perfectly and oscillates near the top... - --- note that speed decrease on slope is not implemented yet (via cosine but also gravity), so this test will have to change when it is --- when it is, prefer passing a very low slope or apply slope factor to adapt the position/velocity calculation - -itest:add_action(time_trigger(28, true), function () end) - --- check that player char has moved to the right and is still on the ground -itest.final_assertion = function () - local is_motion_state_expected, motion_state_message = motion_states.grounded == stage.state.player_char.motion_state, "Expected motion state 'grounded', got "..stage.state.player_char.motion_state - -- to compute position x from x0 after n frames at accel a from speed s0: x = x0 + n*s0 + n(n+1)/2*a - -- actually 13 if we use more narrow ground sensor - local is_position_expected, position_message = almost_eq_with_message(vector(13, 80 - 8), stage.state.player_char:get_bottom_center(), 1/256) - -- to compute speed s from s0 after n frames at accel a: x = s0 + n*a - local is_ground_speed_expected, ground_speed_message = almost_eq_with_message(0, stage.state.player_char.ground_speed, 1/256) - local is_velocity_expected, velocity_message = almost_eq_with_message(vector(0, 0), stage.state.player_char.velocity, 1/256) - - local final_message = "" - - local success = is_position_expected and is_ground_speed_expected and is_velocity_expected and is_motion_state_expected - if not success then - if not is_motion_state_expected then - final_message = final_message..motion_state_message.."\n" - end - if not is_position_expected then - final_message = final_message..position_message.."\n" - end - if not is_ground_speed_expected then - final_message = final_message..ground_speed_message.."\n" - end - if not is_velocity_expected then - final_message = final_message..velocity_message.."\n" - end - - end - - return success, final_message -end - ---]] --[[ Really comment this block out for now, as it makes too many chars From 6c6e3b2b41cd176fcccb1b45100719bba0752ed1 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 1 Mar 2019 00:45:24 +0000 Subject: [PATCH 059/140] [TEST] DSL: Converted last itest (rendering human test) --- src/game/itests/itestplayercharacter.lua | 40 +++++++----------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/src/game/itests/itestplayercharacter.lua b/src/game/itests/itestplayercharacter.lua index 42beecd2..3126da33 100644 --- a/src/game/itests/itestplayercharacter.lua +++ b/src/game/itests/itestplayercharacter.lua @@ -203,7 +203,7 @@ expect pc_velocity 0 0 itest_dsl_parser.register( - '#solo platformer slope ceiling block right', [[ + 'platformer slope ceiling block right', [[ @stage # ..# .<. @@ -500,37 +500,21 @@ expect pc_velocity 1.359375 -0.078125 ]]) - ---[[ Really comment this block out for now, as it makes too many chars - ---[[#pico8 --- human test for pico8 only to check rendering +-- human tests +-- pico8 only, since human must check rendering +--[=[#pico8 -- bugfix history: -- = fixed character pivot computed from drawn sprite topleft (with some gap above character's head) -- and not actual sprite topleft in the spritesheet -itest = integration_test('character is correctly rendered idle', {stage.state.type}) -itest_manager:register(itest) - -itest.setup = function () - setup_map_data() - - mset(0, 10, 64) -- to stand on - - flow:change_gamestate_by_type(stage.state.type) - stage.state.player_char:warp_bottom_to(vector(4., 80.)) - stage.state.player_char.control_mode = control_modes.puppet - stage.state.player_char.motion_mode = motion_modes.debug -end - -itest.teardown = function () - clear_map() - teardown_map_data() -end +itest_dsl_parser.register( + 'pc render idle', [[ +@stage # +. +# --- wait just 0.1 second so the character can be rendered at least 1 frame because the test pauses -itest:add_action(time_trigger(1.), function () end) +warp 4 8 +]]) -- no final assertion, let the user check if result is correct or not (note it will display success whatever) ---#pico8]] ---]] +--#pico8]=] From eda76d2f71cd1585aa18ce329f95782552b08129 Mon Sep 17 00:00:00 2001 From: hsandt Date: Thu, 4 Apr 2019 01:16:29 +0100 Subject: [PATCH 060/140] [DATA] Added comments to read player character data as /64 fractions --- src/game/data/playercharacter_data.lua | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/game/data/playercharacter_data.lua b/src/game/data/playercharacter_data.lua index 4e8557c2..4c03d63a 100644 --- a/src/game/data/playercharacter_data.lua +++ b/src/game/data/playercharacter_data.lua @@ -3,22 +3,22 @@ return { -- platformer motion -- ground acceleration (px/frame^2) - ground_accel_frame2 = 0.0234375, + ground_accel_frame2 = 0.0234375, -- 1.5/64 -- ground active deceleration (brake) (px/frame^2) - ground_decel_frame2 = 0.25, + ground_decel_frame2 = 0.25, -- 16/64 -- ground friction (passive deceleration) (px/frame^2) - ground_friction_frame2 = 0.0234375, + ground_friction_frame2 = 0.0234375, -- 1.5/64 -- gravity acceleration (px/frame^2) - gravity_frame2 = 0.109375, + gravity_frame2 = 0.109375, -- 7/64 -- slope accel acceleration factor (px/frame^2), to multiply by sin(angle) - slope_accel_factor_frame2 = 0.0625, + slope_accel_factor_frame2 = 0.0625, -- 7/64 -- air acceleration on x axis (px/frames^2) - air_accel_x_frame2 = 0.046875, + air_accel_x_frame2 = 0.046875, -- 3/64 -- ground acceleration (px/frame) max_ground_speed = 3, From 4884a88f8210843536ca2580925f716eaa19dd4b Mon Sep 17 00:00:00 2001 From: hsandt Date: Thu, 4 Apr 2019 01:17:19 +0100 Subject: [PATCH 061/140] [MOTION] Fixed _is_column_blocked_by_ceiling_at to support character heights lower than tile height --- src/game/ingame/playercharacter.lua | 26 +++++++++++++++++--------- tests/utestplayercharacter.lua | 12 +++++++++++- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/game/ingame/playercharacter.lua b/src/game/ingame/playercharacter.lua index 25068d94..ac7f69d0 100644 --- a/src/game/ingame/playercharacter.lua +++ b/src/game/ingame/playercharacter.lua @@ -565,6 +565,12 @@ function player_char:_next_ground_step(horizontal_dir, motion_result) else -- step fall: step down is too low, character will fall -- in some rare instances, character may find ground again farther, so don't stop the outside loop yet + -- caution: we are not updating y at all, which means the character starts + -- "walking horizontally in the air". in sonic games, we would expect + -- momentum to take over and send the character upward/downward, preserving + -- velocity y from last frame + -- so when adding momentum, consider reusing the last delta y (e.g. signed_distance_to_closest_ground) + -- and applying it this frame motion_result.is_falling = true end else @@ -582,8 +588,8 @@ function player_char:_next_ground_step(horizontal_dir, motion_result) -- only advance if character is still not blocked (else, preserve previous position, -- which should be floored) - -- this only works because the wall sensors are 1px farther from the character - -- center than the ground sensors; if there were even farther, we'd even need to + -- this only works because the wall sensors are 1px farther from the character center + -- than the ground sensors; if there were even farther, we'd even need to -- move the position backward by hypothetical wall_sensor_extent_x - ground_sensor_extent_x - 1 -- when motion_result.is_blocked (and adapt y) if not motion_result.is_blocked then @@ -623,7 +629,7 @@ end -- so the step up itself will be ignored (e.g. when moving from a flat ground to an ascending slope) function player_char._is_column_blocked_by_ceiling_at(sensor_position) - assert(flr(sensor_position.x) == sensor_position.x, "player_char:_is_blocked_by_ceiling_at_column: sensor_position.x must be floored") + assert(flr(sensor_position.x) == sensor_position.x, "player_char:_is_column_blocked_by_ceiling_at: sensor_position.x must be floored") -- find the tile where this sensor is located local curr_tile_loc = sensor_position:to_location() @@ -643,6 +649,14 @@ function player_char._is_column_blocked_by_ceiling_at(sensor_position) -- (90 and 270-rotated tiles will be ignored as they are not supposed to block the character's head) curr_tile_loc.j = curr_tile_loc.j - 1 local current_tile_top = curr_tile_loc:to_topleft_position().y + local current_tile_bottom = current_tile_top + tile_size + + -- if the bottom of next ceiling to check is already higher than, or equal to + -- one character height, if cannot block him, so return false + local height_distance = sensor_position.y - current_tile_bottom + if height_distance >= pc_data.full_height_standing then + return false + end local ground_array_height, _ = world._compute_column_height_at(curr_tile_loc, column_index0) if ground_array_height ~= nil and ground_array_height > 0 then @@ -653,16 +667,10 @@ function player_char._is_column_blocked_by_ceiling_at(sensor_position) -- with ceiling tiles, we will need to check if the ceiling column height -- hits the head or not. if it doesn't stop here, return false, -- the head is below the ceiling: - -- local current_tile_bottom = current_tile_top + tile_size -- local height_distance = sensor_position.y - current_tile_bottom -- return height_distance < pc_data.full_height_standing end - local height_distance = sensor_position.y - current_tile_top - if height_distance >= pc_data.full_height_standing then - return false - end - end end diff --git a/tests/utestplayercharacter.lua b/tests/utestplayercharacter.lua index 61d891fa..38731152 100644 --- a/tests/utestplayercharacter.lua +++ b/tests/utestplayercharacter.lua @@ -2504,7 +2504,7 @@ describe('player_char', function () describe('(1 full tile)', function () before_each(function () - -- X + -- .X mset(1, 0, 64) -- full tile (act like a full ceiling if position is at bottom) end) @@ -2517,6 +2517,11 @@ describe('player_char', function () assert.is_false(pc._is_column_blocked_by_ceiling_at(vector(7, 8))) end) + -- bugfix history: + -- ? i thought that by design, function should return true but realized it was not consistent + -- ? actually I was right, since if the character moves inside the 2nd of a diagonal tile pattern, + -- it *must* be blocked. when character has a foot on the lower tile, it is considered to be + -- in this lower tile it('should return true for sensor position at the bottom-left of the tile', function () assert.is_true(pc._is_column_blocked_by_ceiling_at(vector(8, 8))) end) @@ -2533,6 +2538,11 @@ describe('player_char', function () assert.is_true(pc._is_column_blocked_by_ceiling_at(vector(12, 8 + pc_data.full_height_standing - 1))) end) + -- bugfix history: + -- < i realized that values of full_height_standing < 8 would fail the test + -- so i moved the height_distance >= pc_data.full_height_standing check above + -- the ground_array_height check (computing height_distance from tile bottom instead of top) + -- to pass it in this case too it('should return false for sensor position below the tile, at character height', function () assert.is_false(pc._is_column_blocked_by_ceiling_at(vector(12, 8 + pc_data.full_height_standing))) end) From 468024c6a4cf72b2d66d342c82e72eb7100f68ec Mon Sep 17 00:00:00 2001 From: hsandt Date: Thu, 4 Apr 2019 21:03:27 +0100 Subject: [PATCH 062/140] [COLLISION] Added air_motion_result --- src/engine/physics/collision.lua | 28 ++++++++++++++++++++++++++-- tests/utestcollision.lua | 27 +++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/engine/physics/collision.lua b/src/engine/physics/collision.lua index 0c6396ed..7e638fca 100644 --- a/src/engine/physics/collision.lua +++ b/src/engine/physics/collision.lua @@ -169,8 +169,8 @@ end --#endif --- struct representing the expected result of a character move over a frame, --- computed step by step +-- struct representing the expected result of a character ground move over a frame, +-- computed step by step. similar to a raycast hit info, specialized for ground motion local ground_motion_result = new_struct() collision.ground_motion_result = ground_motion_result @@ -192,6 +192,30 @@ end --#endif +-- struct representing the expected result of a character air move over a frame, +-- computed step by step. similar to a raycast hit info, specialized for air motion +local air_motion_result = new_struct() +collision.air_motion_result = air_motion_result + +-- position vector position at the end of motion +-- is_blocked_by_ceiling bool was the character blocked by a ceiling during motion? +-- is_blocked_by_wall bool was the character blocked by a left/right wall during motion? +-- is_landing bool bool has the character landed at the end of this motion? +function air_motion_result:_init(position, is_blocked_by_ceiling, is_blocked_by_wall, is_landing) + self.position = position + self.is_blocked_by_ceiling = is_blocked_by_ceiling + self.is_blocked_by_wall = is_blocked_by_wall + self.is_landing = is_landing +end + +--#if log +function air_motion_result:_tostring() + return "air_motion_result("..joinstr(", ", + self.position, self.is_blocked_by_ceiling, self.is_blocked_by_wall, self.is_landing)..")" +end +--#endif + + local tile_data = new_struct() collision.tile_data = tile_data diff --git a/tests/utestcollision.lua b/tests/utestcollision.lua index 1103d516..94acf76c 100644 --- a/tests/utestcollision.lua +++ b/tests/utestcollision.lua @@ -6,7 +6,8 @@ local aabb = collision.aabb local tile_data = collision.tile_data local height_array = collision.height_array local ground_query_info = collision.ground_query_info -local ground_motion_result = collision.ground_motion_result +local ground_motion_result, air_motion_result = get_members(collision, + "ground_motion_result", "air_motion_result") -- retrieve the filter arguments so we can optimize by only generating tests we will need local cli = require('busted.modules.cli')() @@ -715,7 +716,7 @@ describe('collision', function () describe('_tostring', function () - it('should return "height_array({4, 5, 6, 7, 8, 9, 10, 11}, 0.125)"', function () + it('should return "ground_motion_result(vector(2, 3), 0.25, false, true)"', function () local gmr = ground_motion_result(vector(2, 3), 0.25, false, true) assert.are_equal("ground_motion_result(vector(2, 3), 0.25, false, true)", gmr:_tostring()) end) @@ -724,6 +725,28 @@ describe('collision', function () end) + describe('air_motion_result', function () + + describe('_init', function () + + it('should create a air_motion_result with position, is_blocked_by_ceiling, is_blocked_by_wall, is_landing', function () + local gmr = air_motion_result(vector(2, 3), false, false, true) + assert.are_same({vector(2, 3), false, false, true}, {gmr.position, gmr.is_blocked_by_ceiling, gmr.is_blocked_by_wall, gmr.is_landing}) + end) + + end) + + describe('_tostring', function () + + it('should return "air_motion_result(vector(2, 3), false, false, true)"', function () + local gmr = air_motion_result(vector(2, 3), false, false, true) + assert.are_equal("air_motion_result(vector(2, 3), false, false, true)", gmr:_tostring()) + end) + + end) + + end) + describe('tile_data', function () describe('_init', function () From 36a40f7254eb9eb3b71ecdc0af941483b667ed96 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 5 Apr 2019 00:06:06 +0100 Subject: [PATCH 063/140] [COLLISION] Added air_motion_result:is_blocked_along --- src/engine/physics/collision.lua | 17 +++++++++-- tests/utestcollision.lua | 48 ++++++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/engine/physics/collision.lua b/src/engine/physics/collision.lua index 7e638fca..d32c790c 100644 --- a/src/engine/physics/collision.lua +++ b/src/engine/physics/collision.lua @@ -198,16 +198,27 @@ local air_motion_result = new_struct() collision.air_motion_result = air_motion_result -- position vector position at the end of motion --- is_blocked_by_ceiling bool was the character blocked by a ceiling during motion? -- is_blocked_by_wall bool was the character blocked by a left/right wall during motion? +-- is_blocked_by_ceiling bool was the character blocked by a ceiling during motion? -- is_landing bool bool has the character landed at the end of this motion? -function air_motion_result:_init(position, is_blocked_by_ceiling, is_blocked_by_wall, is_landing) +function air_motion_result:_init(position, is_blocked_by_wall, is_blocked_by_ceiling, is_landing) self.position = position - self.is_blocked_by_ceiling = is_blocked_by_ceiling self.is_blocked_by_wall = is_blocked_by_wall + self.is_blocked_by_ceiling = is_blocked_by_ceiling self.is_landing = is_landing end +-- return true iff motion result indicates a blocker in the given direction +function air_motion_result:is_blocked_along(direction) + if direction == directions.left or direction == directions.right then + return self.is_blocked_by_wall + elseif direction == directions.up then + return self.is_blocked_by_ceiling + else -- direction == directions.down + return self.is_landing + end +end + --#if log function air_motion_result:_tostring() return "air_motion_result("..joinstr(", ", diff --git a/tests/utestcollision.lua b/tests/utestcollision.lua index 94acf76c..41cc6bbb 100644 --- a/tests/utestcollision.lua +++ b/tests/utestcollision.lua @@ -729,9 +729,53 @@ describe('collision', function () describe('_init', function () - it('should create a air_motion_result with position, is_blocked_by_ceiling, is_blocked_by_wall, is_landing', function () + it('should create a air_motion_result with position, is_blocked_by_wall, is_blocked_by_ceiling, is_landing', function () + local gmr = air_motion_result(vector(2, 3), true, false, true) + assert.are_same({vector(2, 3), true, false, true}, {gmr.position, gmr.is_blocked_by_wall, gmr.is_blocked_by_ceiling, gmr.is_landing}) + end) + + end) + + describe('is_blocked_along', function () + + it('return false if direction is left and is_blocked_by_wall is false', function () + local gmr = air_motion_result(vector(2, 3), false, false, false) + assert.is_false(gmr:is_blocked_along(directions.left)) + end) + + it('return true if direction is left and is_blocked_by_wall is true', function () + local gmr = air_motion_result(vector(2, 3), true, false, false) + assert.is_true(gmr:is_blocked_along(directions.left)) + end) + + it('return false if direction is right and is_blocked_by_wall is false', function () + local gmr = air_motion_result(vector(2, 3), false, false, false) + assert.is_false(gmr:is_blocked_along(directions.right)) + end) + + it('return true if direction is right and is_blocked_by_wall is true', function () + local gmr = air_motion_result(vector(2, 3), true, false, false) + assert.is_true(gmr:is_blocked_along(directions.right)) + end) + + it('return false if direction is up and is_blocked_by_ceiling is false', function () + local gmr = air_motion_result(vector(2, 3), false, false, false) + assert.is_false(gmr:is_blocked_along(directions.up)) + end) + + it('return true if direction is up and is_blocked_by_ceiling is true', function () + local gmr = air_motion_result(vector(2, 3), false, true, false) + assert.is_true(gmr:is_blocked_along(directions.up)) + end) + + it('return false if direction is down and is_landing is false', function () + local gmr = air_motion_result(vector(2, 3), false, false, false) + assert.is_false(gmr:is_blocked_along(directions.down)) + end) + + it('return true if direction is down and is_landing is true', function () local gmr = air_motion_result(vector(2, 3), false, false, true) - assert.are_same({vector(2, 3), false, false, true}, {gmr.position, gmr.is_blocked_by_ceiling, gmr.is_blocked_by_wall, gmr.is_landing}) + assert.is_true(gmr:is_blocked_along(directions.down)) end) end) From 6324be6ae836f9bcbfea6cd94e5bb642ab80a5de Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 5 Apr 2019 00:21:02 +0100 Subject: [PATCH 064/140] [TEST] Cleaned up comment in utesthelper --- tests/utesthelper.lua | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/utesthelper.lua b/tests/utesthelper.lua index cf39b192..882a9905 100644 --- a/tests/utesthelper.lua +++ b/tests/utesthelper.lua @@ -25,8 +25,6 @@ describe('get_members', function () end) end) --- local a, b = import_members(mymodule, "a", "b") -- v2 - describe('is_empty', function () it('return true if the table is empty', function () assert.is_true(is_empty({})) @@ -43,7 +41,6 @@ describe('is_empty', function () end) describe('are_same', function () - local single_t = {} local comparable_mt = { From 304242444be9b3fbf179aefb3448c1fe9accbac8 Mon Sep 17 00:00:00 2001 From: hsandt Date: Sun, 7 Apr 2019 19:55:35 +0100 Subject: [PATCH 065/140] [STRUCT] Added copy_assign method to copy struct content while preserving reference --- src/engine/core/class.lua | 33 ++++++++-- tests/utestclass.lua | 126 +++++++++++++++++++++++++++++--------- 2 files changed, 125 insertions(+), 34 deletions(-) diff --git a/src/engine/core/class.lua b/src/engine/core/class.lua index ca024fbb..5878ee7e 100644 --- a/src/engine/core/class.lua +++ b/src/engine/core/class.lua @@ -30,15 +30,15 @@ local function struct_eq(lhs, rhs) return getmetatable(lhs) == getmetatable(rhs) and are_same(lhs, rhs, true, true) end --- return a copy of a struct instance +-- return a copy of a struct instance 'self' -- this is a simplified version of deepcopy implementations and only support -- structs referencing primitive types or structs (at least copy-able tables) -- with no reference cycle -local function copy(struct_instance) - -- we can't access the 'struct' variable from here so we get it back via getmetatable - local copied = setmetatable({}, getmetatable(struct_instance)) +local function copy(self) + -- we can't access the struct type from here so we get it back via getmetatable + local copied = setmetatable({}, getmetatable(self)) - for key, value in pairs(struct_instance) do + for key, value in pairs(self) do if type(value) == 'table' then --#if assert assert(type(value.copy) == 'function', "value "..stringify(value).. @@ -55,6 +55,28 @@ local function copy(struct_instance) return copied end +-- copy assign struct members of 'from' to struct members of 'self' +-- from and to must be struct instances of the same type +-- copy_assign is useful when manipulating a struct instance reference whose content +-- must be changed in-place, because the function caller will continue using the same reference +local function copy_assign(self, from) + assert(getmetatable(self) == getmetatable(from), "copy_assign: expected 'self' ("..self..") and 'from' ("..from..") to have the same struct type") + + for key, value in pairs(from) do + if type(value) == 'table' then +--#if assert + assert(type(value.copy_assign) == 'function', "value "..stringify(value).. + " is a table member of a struct but it doesn't have expected copy_assign method, so it's not a struct itself") +--#endif + -- recursively copy-assign the struct members. never use circular references + -- between structs or you'll get an infinite recursion + self[key]:copy_assign(value) + else + self[key] = value + end + end +end + -- create and return a new class -- every class should implement :_init(), -- if useful for logging :_tostring(), and if relevant .__eq() @@ -98,6 +120,7 @@ function new_struct() struct.__concat = concat struct.__eq = struct_eq struct.copy = copy + struct.copy_assign = copy_assign setmetatable(struct, { __call = new diff --git a/tests/utestclass.lua b/tests/utestclass.lua index 95373f42..d56fa530 100644 --- a/tests/utestclass.lua +++ b/tests/utestclass.lua @@ -1,25 +1,25 @@ require("bustedhelper") local class = require("engine/core/class") -describe('new_class', function () +local dummy_class = new_class() - local dummy_class = new_class() +function dummy_class:_init(value) + self.value = value +end - function dummy_class:_init(value) - self.value = value - end +function dummy_class:_tostring() + return "dummy:"..tostr(self.value) +end - function dummy_class:_tostring() - return "dummy:"..tostr(self.value) - end +function dummy_class.__eq(lhs, rhs) + return lhs.value == rhs.value +end - function dummy_class.__eq(lhs, rhs) - return lhs.value == rhs.value - end +function dummy_class:get_incremented_value() + return self.value + 1 +end - function dummy_class:get_incremented_value() - return self.value + 1 - end +describe('new_class', function () it('should create a new class with _init()', function () local dummy = dummy_class(3) @@ -143,6 +143,31 @@ describe('new_struct', function () return self.value1 + self.value2 end + local complex_struct = new_struct() + + function complex_struct:_init(value1, value2) + self.sum = value1 + value2 + self.sub_struct = dummy_struct(value1, value2) + end + + function complex_struct:_tostring() + return "complex_struct: "..joinstr(", ", self.sum, self.sub_struct) + end + + local invalid_struct = new_struct() + + function invalid_struct:_init(value) + self.table = dummy_class(value) -- struct should never contain non-struct tables + end + + function invalid_struct:_tostring() + return "invalid_struct: "..tostr(self.value) + end + + function invalid_struct:_tostring() + return "invalid_struct: "..joinstr(", ", self.sum, self.sub_struct) + end + it('should create a new struct with _init()', function () local dummy = dummy_struct(3, 7) assert.are_same({3, 7}, {dummy.value1, dummy.value2}) @@ -182,31 +207,75 @@ describe('new_struct', function () describe('copy', function () - it('+ should return a copy of the struct, with the same content but not the same reference', function () + it('should error if the struct contains non-struct members at some depth level', function () + assert.has_error(function () + invalid_struct(99):copy() + end, "value dummy:99 is a table member of a struct but it doesn't have expected copy method, so it's not a struct itself") + end) + + -- bugfix history: + + it('should return a copy of the struct, with the same content but not the same reference', function () local dummy = dummy_struct(3, 7) local copied_dummy = dummy:copy() - assert.are_same(dummy, copied_dummy) + + assert.are_same(dummy, copied_dummy) -- are_equal also works, provided __eq is working assert.is_false(rawequal(dummy, copied_dummy)) end) describe('with struct containing struct', function () - local complex_struct = new_struct() - - function complex_struct:_init(value1, value2) - self.sum = value1 + value2 - self.sub_struct = dummy_struct(value1, value2) - end - - function complex_struct:_tostring() - return "complex_struct: "..joinstr(", ", self.sum, self.sub_struct) - end - it('should return a copy of the struct and its struct members, with the same contents but not the same references', function () local complex = complex_struct(3, 7) local copied_complex = complex:copy() + assert.are_same(complex, copied_complex) - assert.is_false(rawequal(picomplcomplexex, copied_complex)) + assert.is_false(rawequal(complex, copied_complex)) + assert.are_same(complex.sub_struct, copied_complex.sub_struct) + assert.is_false(rawequal(complex.sub_struct, copied_complex.sub_struct)) + end) + + end) + + end) + + describe('copy_assign', function () + + it('should error if self and from have different types', function () + local simple_from = dummy_struct(3, 7) + local complex_to = complex_struct(4, 5) + + assert.has_error(function () + complex_to:copy_assign(simple_from) + end, "copy_assign: expected 'self' (complex_struct: 9, dummy: 4, 5) and 'from' (dummy: 3, 7) to have the same struct type") + end) + + it('should error if the struct contains non-struct members at some depth level', function () + assert.has_error(function () + invalid_struct(9):copy_assign(invalid_struct(99)) + end, "value dummy:99 is a table member of a struct but it doesn't have expected copy_assign method, so it's not a struct itself") + end) + + it('should assign all the values of `from` to `to`', function () + local from = dummy_struct(3, 7) + local to = dummy_struct(99, -99) + + to:copy_assign(from) + + assert.are_same(from, to) -- are_equal also works, provided __eq is working + end) + + describe('with struct containing struct', function () + + it('should return a copy of the struct and its struct members, with the same contents but not the same references', function () + local from = complex_struct(3, 7) + local to = complex_struct(99, -99) + + to:copy_assign(from) + + assert.are_same(from, to) + assert.is_false(rawequal(from, to)) + assert.are_same(from.sub_struct, to.sub_struct) + assert.is_false(rawequal(from.sub_struct, to.sub_struct)) end) end) @@ -268,7 +337,6 @@ describe('new_struct', function () end) - end) end) From 580cb43b702d5b901a97447526863c28086276f0 Mon Sep 17 00:00:00 2001 From: hsandt Date: Sun, 7 Apr 2019 20:54:53 +0100 Subject: [PATCH 066/140] [COLLISION] Added slope_angle member to air_motion_result to support landing --- src/engine/physics/collision.lua | 22 ++++++++++++---------- tests/utestcollision.lua | 22 +++++++++++----------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/engine/physics/collision.lua b/src/engine/physics/collision.lua index d32c790c..b7aaa5db 100644 --- a/src/engine/physics/collision.lua +++ b/src/engine/physics/collision.lua @@ -174,10 +174,10 @@ end local ground_motion_result = new_struct() collision.ground_motion_result = ground_motion_result --- position vector position at the end of motion --- slope_angle float slope angle of the final position --- is_blocked bool was the character blocked during motion? --- is_falling bool should the character fall after this motion? +-- position vector position at the end of motion +-- slope_angle float|nil slope angle of the final position (nil if is_falling is true) +-- is_blocked bool was the character blocked during motion? +-- is_falling bool should the character fall after this motion? function ground_motion_result:_init(position, slope_angle, is_blocked, is_falling) self.position = position self.slope_angle = slope_angle @@ -197,15 +197,17 @@ end local air_motion_result = new_struct() collision.air_motion_result = air_motion_result --- position vector position at the end of motion --- is_blocked_by_wall bool was the character blocked by a left/right wall during motion? --- is_blocked_by_ceiling bool was the character blocked by a ceiling during motion? --- is_landing bool bool has the character landed at the end of this motion? -function air_motion_result:_init(position, is_blocked_by_wall, is_blocked_by_ceiling, is_landing) +-- position vector position at the end of motion +-- is_blocked_by_wall bool was the character blocked by a left/right wall during motion? +-- is_blocked_by_ceiling bool was the character blocked by a ceiling during motion? +-- is_landing bool has the character landed at the end of this motion? +-- slope_angle float|nil slope angle of the final position (nil unless is_landing is true) +function air_motion_result:_init(position, is_blocked_by_wall, is_blocked_by_ceiling, is_landing, slope_angle) self.position = position self.is_blocked_by_wall = is_blocked_by_wall self.is_blocked_by_ceiling = is_blocked_by_ceiling self.is_landing = is_landing + self.slope_angle = slope_angle end -- return true iff motion result indicates a blocker in the given direction @@ -222,7 +224,7 @@ end --#if log function air_motion_result:_tostring() return "air_motion_result("..joinstr(", ", - self.position, self.is_blocked_by_ceiling, self.is_blocked_by_wall, self.is_landing)..")" + self.position, self.is_blocked_by_ceiling, self.is_blocked_by_wall, self.is_landing, self.slope_angle)..")" end --#endif diff --git a/tests/utestcollision.lua b/tests/utestcollision.lua index 41cc6bbb..cab73bc0 100644 --- a/tests/utestcollision.lua +++ b/tests/utestcollision.lua @@ -708,17 +708,17 @@ describe('collision', function () describe('_init', function () it('should create a ground_motion_result with position, slope_angle, is_blocked, is_falling', function () - local gmr = ground_motion_result(vector(2, 3), 0.25, false, true) - assert.are_same({vector(2, 3), 0.25, false, true}, {gmr.position, gmr.slope_angle, gmr.is_blocked, gmr.is_falling}) + local gmr = ground_motion_result(vector(2, 3), 0.25, true, false) + assert.are_same({vector(2, 3), 0.25, true, false}, {gmr.position, gmr.slope_angle, gmr.is_blocked, gmr.is_falling}) end) end) describe('_tostring', function () - it('should return "ground_motion_result(vector(2, 3), 0.25, false, true)"', function () - local gmr = ground_motion_result(vector(2, 3), 0.25, false, true) - assert.are_equal("ground_motion_result(vector(2, 3), 0.25, false, true)", gmr:_tostring()) + it('should return "ground_motion_result(vector(2, 3), 0.25, true, false)"', function () + local gmr = ground_motion_result(vector(2, 3), 0.25, true, false) + assert.are_equal("ground_motion_result(vector(2, 3), 0.25, true, false)", gmr:_tostring()) end) end) @@ -729,9 +729,9 @@ describe('collision', function () describe('_init', function () - it('should create a air_motion_result with position, is_blocked_by_wall, is_blocked_by_ceiling, is_landing', function () - local gmr = air_motion_result(vector(2, 3), true, false, true) - assert.are_same({vector(2, 3), true, false, true}, {gmr.position, gmr.is_blocked_by_wall, gmr.is_blocked_by_ceiling, gmr.is_landing}) + it('should create a air_motion_result with position, is_blocked_by_wall, is_blocked_by_ceiling, is_landing, slope_angle', function () + local gmr = air_motion_result(vector(2, 3), true, false, true, -0.25) + assert.are_same({vector(2, 3), true, false, true, -0.25}, {gmr.position, gmr.is_blocked_by_wall, gmr.is_blocked_by_ceiling, gmr.is_landing, gmr.slope_angle}) end) end) @@ -782,9 +782,9 @@ describe('collision', function () describe('_tostring', function () - it('should return "air_motion_result(vector(2, 3), false, false, true)"', function () - local gmr = air_motion_result(vector(2, 3), false, false, true) - assert.are_equal("air_motion_result(vector(2, 3), false, false, true)", gmr:_tostring()) + it('should return "air_motion_result(vector(2, 3), false, false, true, -0.25)"', function () + local gmr = air_motion_result(vector(2, 3), false, false, true, -0.25) + assert.are_equal("air_motion_result(vector(2, 3), false, false, true, -0.25)", gmr:_tostring()) end) end) From 3a2489b4a9e9ad637a894019fcac41b51e2f5c15 Mon Sep 17 00:00:00 2001 From: hsandt Date: Thu, 11 Apr 2019 20:22:19 +0100 Subject: [PATCH 067/140] [COLLISION] Fixed ground_query_info _tostring when slope_angle is nil --- src/engine/physics/collision.lua | 6 +++--- tests/utestcollision.lua | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/engine/physics/collision.lua b/src/engine/physics/collision.lua index b7aaa5db..326b9726 100644 --- a/src/engine/physics/collision.lua +++ b/src/engine/physics/collision.lua @@ -155,8 +155,8 @@ end local ground_query_info = new_struct() collision.ground_query_info = ground_query_info --- signed_distance float signed distance to the detected ground (clamped to min-1 amd max+1) --- slope_angle float slope angle of the detected ground (nil if no ground) +-- signed_distance float signed distance to the detected ground (clamped to min-1 amd max+1) +-- slope_angle float|nil slope angle of the detected ground (nil if no ground) function ground_query_info:_init(signed_distance, slope_angle) self.signed_distance = signed_distance self.slope_angle = slope_angle @@ -164,7 +164,7 @@ end --#if log function ground_query_info:_tostring() - return "ground_query_info("..joinstr(", ", self.signed_distance, self.slope_angle)..")" + return "ground_query_info("..joinstr(", ", self.signed_distance, tostr(self.slope_angle))..")" end --#endif diff --git a/tests/utestcollision.lua b/tests/utestcollision.lua index cab73bc0..361ae55d 100644 --- a/tests/utestcollision.lua +++ b/tests/utestcollision.lua @@ -699,6 +699,11 @@ describe('collision', function () assert.are_equal("ground_query_info(-2.0, 0.25)", info:_tostring()) end) + it('should return "ground_query_info({self.signed_distance}, [nil])"', function () + local info = ground_query_info(-2.0, nil) + assert.are_equal("ground_query_info(-2.0, [nil])", info:_tostring()) + end) + end) end) From 62daba0a6724174f39fee89d312aaa2d1ab971ef Mon Sep 17 00:00:00 2001 From: hsandt Date: Thu, 11 Apr 2019 23:22:50 +0100 Subject: [PATCH 068/140] [MOTION] Added air motion block with wall, ceiling and landing Replaced mset with mock_mset for setup safety --- src/game/data/playercharacter_data.lua | 2 + src/game/data/tile_data.lua | 3 + src/game/ingame/playercharacter.lua | 254 +++++++- src/game/test_data/tile_test_data.lua | 9 + tests/utestplayercharacter.lua | 796 +++++++++++++++++++++---- tests/utestworld.lua | 8 +- 6 files changed, 922 insertions(+), 150 deletions(-) diff --git a/src/game/data/playercharacter_data.lua b/src/game/data/playercharacter_data.lua index 4c03d63a..b795a9d6 100644 --- a/src/game/data/playercharacter_data.lua +++ b/src/game/data/playercharacter_data.lua @@ -46,9 +46,11 @@ return { -- max vertical distance allowed to escape from inside ground (must be < tile_size as -- _compute_signed_distance_to_closest_ground uses it as upper_limit tile_size) + -- also the max step up of the character in ground motion max_ground_escape_height = 4, -- max vertical distance allowed to snap to a lower ground while running (on step or curve) + -- a.k.a. max step down max_ground_snap_height = 4, -- debug motion diff --git a/src/game/data/tile_data.lua b/src/game/data/tile_data.lua index 926a2a21..9b956091 100644 --- a/src/game/data/tile_data.lua +++ b/src/game/data/tile_data.lua @@ -1,6 +1,9 @@ -- this script is similar to tile_test_data, but has some parts -- useful for itest in pico8, whereas tile_test_data is only for busted utests/itests +-- it is used by tilemap for the dsl +--#ifn pico8 local tile_test_data = require("game/test_data/tile_test_data") +--#endif tile_symbol_to_ids = { ['.'] = 0, -- empty diff --git a/src/game/ingame/playercharacter.lua b/src/game/ingame/playercharacter.lua index ac7f69d0..89918ba3 100644 --- a/src/game/ingame/playercharacter.lua +++ b/src/game/ingame/playercharacter.lua @@ -262,6 +262,7 @@ end -- return the position of the ground sensor in horizontal_dir when the character center is at center_position -- subpixels are ignored function player_char:_get_ground_sensor_position_from(center_position, horizontal_dir) + -- ignore subpixels from center position in x local x_floored_center_position = vector(flr(center_position.x), center_position.y) local x_floored_bottom_center = x_floored_center_position + vector(0, pc_data.center_height_standing) @@ -469,7 +470,7 @@ function player_char:_compute_ground_motion_result() local horizontal_dir = signed_speed_to_dir(self.ground_speed) - -- initialise result + -- initialise result with floored x (we will reinject subpixels if character didn't touch a wall) local motion_result = collision.ground_motion_result( vector(flr(self.position.x), self.position.y), self.slope_angle, @@ -477,12 +478,10 @@ function player_char:_compute_ground_motion_result() false ) - -- fixme: consider slope angle cos to reduce motion along x - -- only full pixels matter for collisions, but subpixels may sum up to a full pixel -- so first estimate how many full pixel columns the character may actually explore this frame local distance_x = self.ground_speed * cos(self.slope_angle) - local max_column_distance = player_char._compute_max_column_distance(self.position.x, distance_x) + local max_column_distance = player_char._compute_max_pixel_distance(self.position.x, distance_x) -- iterate pixel by pixel on the x direction until max possible distance is reached -- only stopping if the character is blocked by a wall (not if falling, since we want @@ -511,7 +510,8 @@ function player_char:_compute_ground_motion_result() -- character has not touched a wall at all, so add the remaining subpixels -- (it's simpler to just recompute the full motion in x; don't touch y tough, -- as it depends on the shape of the ground) - -- also set the slope since we may have moved to another tile + -- do not apply other changes (like slope) since technically we have not reached + -- the next tile yet, only advanced of some subpixels motion_result.position.x = self.position.x + distance_x end end @@ -520,23 +520,23 @@ function player_char:_compute_ground_motion_result() return motion_result end --- return the number of new pixel columns explored when moving from initial_position_x --- over ground_speed * 1 frame. this is either flr(ground_speed) --- or flr(ground_speed) + 1 (if subpixels from initial position x and speed sum up to 1.0 or more) -function player_char._compute_max_column_distance(initial_position_x, ground_speed) - return abs(flr(initial_position_x + ground_speed) - flr(initial_position_x)) +-- return the number of new pixel columns explored when moving from initial_position_coord (x or y) +-- over velocity_coord (x or y) * 1 frame. this is either flr(velocity_coord) +-- or flr(velocity_coord) + 1 (if subpixels from initial position coord and speed sum up to 1.0 or more) +function player_char._compute_max_pixel_distance(initial_position_coord, velocity_coord) + return abs(flr(initial_position_coord + velocity_coord) - flr(initial_position_coord)) end --- update motion_result: collision.ground_motion_result for a character trying to move +-- update ref_motion_result: collision.ground_motion_result for a character trying to move -- by 1 pixel step in horizontal_dir, taking obstacles into account -- if character is blocked, it doesn't update the position and flag is_blocked --- if character is fallling, it updates the position and flag is_falling +-- if character is falling, it updates the position and flag is_falling -- ground_motion_result.position.x should be floored for these steps --- although _compute_ground_sensors_signed_distance will floor in x anyway -function player_char:_next_ground_step(horizontal_dir, motion_result) +-- (some functions assert when giving subpixel coordinates) +function player_char:_next_ground_step(horizontal_dir, ref_motion_result) -- compute candidate position on next step. only flat slopes supported local step_vec = horizontal_dir_vectors[horizontal_dir] - local next_position_candidate = motion_result.position + step_vec + local next_position_candidate = ref_motion_result.position + step_vec -- check if next position is inside/above ground local query_info = self:_compute_ground_sensors_signed_distance(next_position_candidate) @@ -548,12 +548,12 @@ function player_char:_next_ground_step(horizontal_dir, motion_result) -- step up next_position_candidate.y = next_position_candidate.y - penetration_height -- if we left the ground during a previous step, cancel that (step up land, very rare) - motion_result.is_falling = false + ref_motion_result.is_falling = false else -- step blocked: step up is too high, character is blocked -- if character left the ground during a previous step, let it this way; -- character will simply hit the wall, then fall - motion_result.is_blocked = true + ref_motion_result.is_blocked = true end elseif signed_distance_to_closest_ground > 0 then -- position is above ground, check if we can step down during this step @@ -561,7 +561,7 @@ function player_char:_next_ground_step(horizontal_dir, motion_result) -- step down next_position_candidate.y = next_position_candidate.y + signed_distance_to_closest_ground -- if character left the ground during a previous step, cancel that (step down land, very rare) - motion_result.is_falling = false + ref_motion_result.is_falling = false else -- step fall: step down is too low, character will fall -- in some rare instances, character may find ground again farther, so don't stop the outside loop yet @@ -571,33 +571,33 @@ function player_char:_next_ground_step(horizontal_dir, motion_result) -- velocity y from last frame -- so when adding momentum, consider reusing the last delta y (e.g. signed_distance_to_closest_ground) -- and applying it this frame - motion_result.is_falling = true + ref_motion_result.is_falling = true end else -- step flat -- if character left the ground during a previous step, cancel that (very rare) - motion_result.is_falling = false + ref_motion_result.is_falling = false end -- character is not blocked by a steep step up/wall, but we need to check if it is -- blocked by a ceiling too low; in the extreme case, a diagonal tile pattern -- ->X -- X - if not motion_result.is_blocked then - motion_result.is_blocked = self:_is_blocked_by_ceiling_at(next_position_candidate) + if not ref_motion_result.is_blocked then + ref_motion_result.is_blocked = self:_is_blocked_by_ceiling_at(next_position_candidate) -- only advance if character is still not blocked (else, preserve previous position, -- which should be floored) -- this only works because the wall sensors are 1px farther from the character center -- than the ground sensors; if there were even farther, we'd even need to -- move the position backward by hypothetical wall_sensor_extent_x - ground_sensor_extent_x - 1 - -- when motion_result.is_blocked (and adapt y) - if not motion_result.is_blocked then - motion_result.position = next_position_candidate - if motion_result.is_falling then - motion_result.slope_angle = nil + -- when ref_motion_result.is_blocked (and adapt y) + if not ref_motion_result.is_blocked then + ref_motion_result.position = next_position_candidate + if ref_motion_result.is_falling then + ref_motion_result.slope_angle = nil else - motion_result.slope_angle = next_slope_angle + ref_motion_result.slope_angle = next_slope_angle end end end @@ -696,7 +696,8 @@ function player_char:_check_jump() -- apply initial jump speed for variable jump -- note: if the player is doing a hop, the vertical speed will be reset -- to the interrupt speed during the same frame in _update_platformer_motion_airborne - -- (we don't do it here so we centralize the check and don't apply gravity during such a frame) + -- via _check_hold_jump (we don't do it here so we centralize the check and + -- don't apply gravity during such a frame) -- limitation: only support flat ground for now self.velocity.y = self.velocity.y - pc_data.initial_var_jump_speed_frame self:_enter_motion_state(motion_states.airborne) @@ -724,11 +725,21 @@ function player_char:_update_platformer_motion_airborne() self.velocity.x = self.velocity.x + self.move_intention.x * pc_data.air_accel_x_frame2 -- apply air motion - self:move_by(self.velocity) - -- detect ground and snap up for landing - local has_landed = self:_check_escape_from_ground() - if has_landed then + local air_motion_result = self:_compute_air_motion_result() + + self.position = air_motion_result.position + + if air_motion_result.is_blocked_by_wall then + self.velocity.x = 0 + end + + if air_motion_result.is_blocked_by_ceiling then + self.velocity.y = 0 + end + + if air_motion_result.is_landing then + self.slope_angle = air_motion_result.slope_angle self:_enter_motion_state(motion_states.grounded) end @@ -752,6 +763,183 @@ function player_char:_check_hold_jump() end end +-- return {next_position: vector, is_blocked_by_ceiling: bool, is_blocked_by_wall: bool, is_landing: bool} where +-- - next_position is the position of the character next frame considering his current (air) velocity +-- - is_blocked_by_ceiling is true iff the character encounters a ceiling during this motion +-- - is_blocked_by_wall is true iff the character encounters a wall during this motion +-- - is_landing is true iff the character touches a ground from above during this motion +function player_char:_compute_air_motion_result() + -- if character is not moving, he is not blocked nor landing (we assume the environment is static) + if self.velocity == vector.zero() then + return collision.air_motion_result( + self.position, + false, + false, + false, + nil + ) + end + + -- initialize air motion result (do not floor coordinates, _advance_in_air_along will do it) + local motion_result = collision.air_motion_result( + vector(self.position.x, self.position.y), + false, + false, + false, + nil + ) + + -- from here, unlike ground motion, there are 3 ways to iterate: + -- a. describe a Bresenham's line, stepping on x and y, for the best precision + -- b. step on x until you reach the max distance x, then step on y (may hit wall you wouldn't have with a. or c.) + -- c. step on y until you reach the max distance y, then step on x (may hit ceiling you wouldn't have with a. or b.) + + -- we focus on landing/ceiling first, and prefer simplicity to precision as long as motion seems ok, + -- so we choose c. + self:_advance_in_air_along(motion_result, self.velocity, "y") + self:_advance_in_air_along(motion_result, self.velocity, "x") + + return motion_result +end + +-- modifies ref_motion_result in-place, setting it to the result of an air motion from ref_motion_result.position +-- over velocity[coord] px, where coord is "x" or "y" +function player_char:_advance_in_air_along(ref_motion_result, velocity, coord) + if velocity_coord == 0 then return end + + -- only full pixels matter for collisions, but subpixels may sum up to a full pixel + -- so first estimate how many full pixel columns the character may actually explore this frame + local initial_position_coord = ref_motion_result.position[coord] + local max_pixel_distance = player_char._compute_max_pixel_distance(initial_position_coord, velocity[coord]) + + -- floor coordinate to simplify step by step pixel detection (mostly useful along x to avoid + -- flooring every time we query column heights) + -- since initial_position_coord is storing the original position with subpixels, we are losing information + ref_motion_result.position[coord] = flr(ref_motion_result.position[coord]) + + -- iterate pixel by pixel on the x direction until max possible distance is reached + -- only stopping if the character is blocked by a wall (not if falling, since we want + -- him to continue moving in the air as far as possible; in edge cases, he may even + -- touch the ground again some pixels farther) + local pixel_distance = 1 + local direction + if coord == "x" then + direction = directions.right + else + direction = directions.down + end + if velocity[coord] < 0 then + direction = oppose_direction(direction) + end + + local is_blocked = coord == "x" and ref_motion_result.is_blocked_by_wall or (ref_motion_result.is_blocked_by_wall or ref_motion_result.is_landing) + while pixel_distance <= max_pixel_distance and not ref_motion_result:is_blocked_along(direction) do + self:_next_air_step(direction, ref_motion_result) + pixel_distance = pixel_distance + 1 + end + + + -- check if we need to add or cut subpixels + if not ref_motion_result:is_blocked_along(direction) then + local are_subpixels_left = initial_position_coord + max_pixel_distance > ref_motion_result.position[coord] + if are_subpixels_left then + -- character has not been blocked and has some subpixels left to go + -- check if character has touched a wall (we need an extra step to "ceil" the subpixels) + local extra_step_motion_result = ref_motion_result:copy() + self:_next_air_step(direction, extra_step_motion_result) + if extra_step_motion_result:is_blocked_along(direction) then + -- character has just reached a wall, plus a few subpixels + -- unlike classic sonic, we decide to cut the subpixels and block the character + -- on the spot (we just reuse the result of the extra step, since is_falling doesn't change if is_blocked is true) + -- it's very important to keep the reference and assign member values instead + ref_motion_result:copy_assign(extra_step_motion_result) + else + -- character has not touched a wall at all, so add the remaining subpixels + -- (it's simpler to just recompute the full motion in x; don't touch y tough, + -- as it depends on the shape of the ground) + -- do not apply other changes (like slope) since technically we have not reached + -- the next tile yet, only advanced of some subpixels + ref_motion_result.position[coord] = initial_position_coord + velocity[coord] + end + end + end +end + +-- update ref_motion_result: collision.air_motion_result for a character trying to move +-- by 1 pixel step in direction in the air, taking obstacles into account +-- if character is blocked by wall, ceiling or landing when moving toward left/right, up or down resp., +-- it doesn't update the position and the corresponding flag is set +-- air_motion_result.position.x/y should be floored for these steps +function player_char:_next_air_step(direction, ref_motion_result) + local step_vec = dir_vectors[direction] + local next_position_candidate = ref_motion_result.position + step_vec + + -- we can only hit walls or the ground when moving left, right or down + if direction ~= directions.up then + -- query ground to check for obstacles (we only care about distance, not slope angle) + -- note that we reuse the ground sensors for air motion, because they are good at finding + -- collisions around the bottom left/right corners + local query_info = self:_compute_ground_sensors_signed_distance(next_position_candidate) + local signed_distance_to_closest_ground, next_slope_angle = query_info.signed_distance, query_info.slope_angle + + -- check if the character has hit a ground or a wall + if signed_distance_to_closest_ground < 0 then + -- we do not activate step up during air motion, so any pixel above the character's bottom + -- is considered a hard obstacle + -- depending on the direction, we consider we were blocked by either a ceiling or a wall + if direction == directions.down then + -- landing: the character has just set foot on ground, flag it and initialize slope angle + -- note that we only consider the character to touch ground when it is about to enter it + -- therefore, if he exactly reaches signed_distance_to_closest_ground == 0 this frame, + -- it is still technically considered in the air + -- if this step is blocked by landing, there is no extra motion, + -- but character will enter grounded state + ref_motion_result.is_landing = true + ref_motion_result.slope_angle = next_slope_angle + else + ref_motion_result.is_blocked_by_wall = true + end + elseif signed_distance_to_closest_ground > 0 then + -- in the air: the most common case, in general requires nothing to do + -- in rare cases, the character has landed on a previous step, and we must cancel that now + ref_motion_result.is_landing = false + ref_motion_result.slope_angle = nil + elseif ref_motion_result.is_landing then + -- if we enter this, direction must be horizontal, so update slope angle with new ground + ref_motion_result.slope_angle = next_slope_angle + end + end + + -- we can only hit ceiling when moving left, right or up + -- note that the ceiling check is necessary during horizontal motion to complement + -- ground sensors, the edge case being when the bottom of the character matches + -- the bottom of a collision tile, ground sensors could only detect the tile below + -- if we have already found a blocker above (only possible for left and right), + -- then there is no need to check further, though + if direction ~= directions.down and not ref_motion_result.is_blocked_by_wall then + local is_blocked_by_ceiling_at_next = self:_is_blocked_by_ceiling_at(next_position_candidate) + if is_blocked_by_ceiling_at_next then + if direction == directions.up then + ref_motion_result.is_blocked_by_ceiling = true + else + -- we would be blocked by ceiling on the next position, but since we can't even go there, + -- we are actually blocked by the wall preventing the horizontal move + ref_motion_result.is_blocked_by_wall = true + end + end + end + + -- only advance if character is still not blocked (else, preserve previous position, + -- which should be floored) + if not ref_motion_result:is_blocked_along(direction) then + -- this only works because the wall sensors are 1px farther from the character center + -- than the ground sensors; if there were even farther, we'd even need to + -- move the position backward by hypothetical wall_sensor_extent_x - ground_sensor_extent_x - 1 + -- when ref_motion_result:is_blocked_along() (and adapt y) + ref_motion_result.position = next_position_candidate + end +end + --#if cheat -- update the velocity and position of the character following debug motion rules diff --git a/src/game/test_data/tile_test_data.lua b/src/game/test_data/tile_test_data.lua index 23c94fd6..8f445c94 100644 --- a/src/game/test_data/tile_test_data.lua +++ b/src/game/test_data/tile_test_data.lua @@ -59,6 +59,15 @@ function tile_test_data.teardown() height_array_init_mock:revert() end +-- helper safety function that verifies that mock tile data is active when creating mock maps for utests +-- always use it instead of mset in utest setup meant to test collisions +function mock_mset(x, y, v) + -- verify that tile_test_data.setup has been called since the last tile_test_data.teardown + -- just check if the mock of height_array exists and is active + assert(height_array_init_mock and not height_array_init_mock.reverted, "mock_mset: tile_test_data.setup has not been called since the last tile_test_data.teardown") + mset(x, y, v) +end + return tile_test_data --#endif diff --git a/tests/utestplayercharacter.lua b/tests/utestplayercharacter.lua index 38731152..9d862797 100644 --- a/tests/utestplayercharacter.lua +++ b/tests/utestplayercharacter.lua @@ -10,26 +10,26 @@ local tile_test_data = require("game/test_data/tile_test_data") describe('player_char', function () -- static method - describe('_compute_max_column_distance', function () + describe('_compute_max_pixel_distance', function () it('(2, 0) => 0', function () - assert.are_equal(0, player_char._compute_max_column_distance(2, 0)) + assert.are_equal(0, player_char._compute_max_pixel_distance(2, 0)) end) it('(2, 1.5) => 1', function () - assert.are_equal(1, player_char._compute_max_column_distance(2, 1.5)) + assert.are_equal(1, player_char._compute_max_pixel_distance(2, 1.5)) end) it('(2, 3) => 3', function () - assert.are_equal(3, player_char._compute_max_column_distance(2, 3)) + assert.are_equal(3, player_char._compute_max_pixel_distance(2, 3)) end) it('(2.2, 1.7) => 1', function () - assert.are_equal(1, player_char._compute_max_column_distance(2.2, 1.7)) + assert.are_equal(1, player_char._compute_max_pixel_distance(2.2, 1.7)) end) it('(2.2, 1.8) => 2', function () - assert.are_equal(2, player_char._compute_max_column_distance(2.2, 1.8)) + assert.are_equal(2, player_char._compute_max_pixel_distance(2.2, 1.8)) end) end) @@ -722,7 +722,7 @@ describe('player_char', function () before_each(function () -- create a full tile at (1, 1), i.e. (8, 8) to (15, 15) px - mset(1, 1, 64) + mock_mset(1, 1, 64) end) -- on the sides @@ -809,7 +809,7 @@ describe('player_char', function () before_each(function () -- create a half-tile at (1, 1), top-left at (8, 12), top-right at (15, 16) included - mset(1, 1, 70) + mock_mset(1, 1, 70) end) -- just above @@ -899,7 +899,7 @@ describe('player_char', function () before_each(function () -- create an ascending slope at (1, 1), i.e. (8, 15) to (15, 8) px - mset(1, 1, 65) + mock_mset(1, 1, 65) end) it('should return 0.0625, -45/360 if just above slope column 0', function () @@ -968,7 +968,7 @@ describe('player_char', function () before_each(function () -- create a descending slope at (1, 1), i.e. (8, 8) to (15, 15) px - mset(1, 1, 66) + mock_mset(1, 1, 66) end) it('. should return 0.0625, 45/360 if right sensors are just a little above column 0', function () @@ -1033,7 +1033,7 @@ describe('player_char', function () before_each(function () -- create an ascending slope 22.5 at (1, 1), i.e. (8, 14) to (15, 11) px - mset(1, 1, 67) + mock_mset(1, 1, 67) end) it('should return -4, -22.5/360 if below column 7 by 4px)', function () @@ -1047,7 +1047,7 @@ describe('player_char', function () before_each(function () -- create a quarter-tile at (1, 1), i.e. (12, 12) to (15, 15) px -- note that the quarter-tile is made of 2 subtiles of slope 0, hence overall slope is considered 0, not an average slope between min and max height - mset(1, 1, 71) + mock_mset(1, 1, 71) end) it('should return ground_query_info(max_ground_snap_height + 1, nil) if just at the bottom of the tile, on the left part, so in the air (and not 0 just because it is at height 0)', function () @@ -1082,8 +1082,8 @@ describe('player_char', function () -- 11111111 -- 11111111 23 - mset(1, 1, 72) - mset(1, 2, 64) + mock_mset(1, 1, 72) + mock_mset(1, 2, 64) end) it('should return -4, 0 if below top by 4px, with character crossing 2 tiles', function () @@ -1101,7 +1101,7 @@ describe('player_char', function () before_each(function () -- create a full tile at (1, 1), i.e. (8, 8) to (15, 15) px - mset(1, 1, 64) + mock_mset(1, 1, 64) end) it('should do nothing when character is not touching ground at all, and return false', function () @@ -1142,7 +1142,7 @@ describe('player_char', function () before_each(function () -- create a descending slope at (1, 1), i.e. (8, 8) to (15, 15) px - mset(1, 1, 66) + mock_mset(1, 1, 66) end) it('should do nothing when character is not touching ground at all, and return false', function () @@ -1375,7 +1375,7 @@ describe('player_char', function () end) - end) + end) -- _update_platformer_motion -- bugfix history: -- ^ use fractional speed to check that fractional moves are supported @@ -1510,7 +1510,7 @@ describe('player_char', function () end) - end) + end) -- _update_platformer_motion_grounded describe('_update_ground_speed', function () @@ -1554,7 +1554,8 @@ describe('player_char', function () assert.spy(player_char._clamp_ground_speed).was_called(1) assert.spy(player_char._clamp_ground_speed).was_called_with(match.ref(pc)) end) - end) + + end) -- _update_ground_speed describe('_update_ground_speed_by_slope', function () @@ -1579,7 +1580,7 @@ describe('player_char', function () assert.are_equal(2 + pc_data.slope_accel_factor_frame2 * sqrt(2)/2, pc.ground_speed) end) - end) + end) -- _update_ground_speed_by_slope describe('_update_ground_speed_by_intention', function () @@ -1689,7 +1690,7 @@ describe('player_char', function () assert.are_equal(0, pc.ground_speed) end) - end) + end) -- _update_ground_speed_by_intention describe('_clamp_ground_speed', function () @@ -1763,7 +1764,7 @@ describe('player_char', function () it('(vector(3, 4) at speed 0.5) should return vector(3.5, 4), slope: 0, is_blocked: false, is_falling: false', function () pc.position = vector(3, 4) pc.ground_speed = 0.5 - -- we assume _compute_max_column_distance is correct, so it should return 0 + -- we assume _compute_max_pixel_distance is correct, so it should return 0 -- but as there is no blocking, the remaining subpixels will still be added assert.are_equal(collision.ground_motion_result( @@ -1796,7 +1797,7 @@ describe('player_char', function () it('(vector(3.5, 4) at speed 0.5) should return vector(0.5, 4), is_blocked: false, is_falling: false', function () pc.position = vector(3.5, 4) pc.ground_speed = 0.5 - -- we assume _compute_max_column_distance is correct, so it should return 1 + -- we assume _compute_max_pixel_distance is correct, so it should return 1 assert.are_equal(collision.ground_motion_result( vector(4, 4), @@ -1847,7 +1848,7 @@ describe('player_char', function () it('(vector(3.5, 4) at speed 1.5) should return vector(5, 4), slope before blocked, is_blocked: false, is_falling: false', function () pc.position = vector(3.5, 4) pc.ground_speed = 1.5 - -- we assume _compute_max_column_distance is correct, so it should return 2 + -- we assume _compute_max_pixel_distance is correct, so it should return 2 assert.are_equal(collision.ground_motion_result( vector(5, 4), @@ -1864,7 +1865,7 @@ describe('player_char', function () it('(vector(4.5, 4) at speed 0.5) should return vector(5, 4), slope before blocked, is_blocked: false, is_falling: false', function () pc.position = vector(4.5, 4) pc.ground_speed = 0.5 - -- we assume _compute_max_column_distance is correct, so it should return 2 + -- we assume _compute_max_pixel_distance is correct, so it should return 2 assert.are_equal(collision.ground_motion_result( vector(5, 4), @@ -1897,7 +1898,7 @@ describe('player_char', function () it('(vector(4, 4) at speed 1.5) should return vector(5, 4), slope before blocked, is_blocked: true, is_falling: false', function () pc.position = vector(4, 4) pc.ground_speed = 1.5 - -- we assume _compute_max_column_distance is correct, so it should return 1 + -- we assume _compute_max_pixel_distance is correct, so it should return 1 -- the character will just touch the wall but because it has some extra subpixels -- going "into" the wall, we floor them and consider character as blocked -- (unlike Classic Sonic that would simply ignore subpixels) @@ -1951,7 +1952,7 @@ describe('player_char', function () it('+ (vector(5, 4) at speed 0.5) should return vector(5, 4), slope before moving, is_blocked: false, is_falling: false', function () pc.position = vector(5, 4) pc.ground_speed = 0.5 - -- we assume _compute_max_column_distance is correct, so it should return 0 + -- we assume _compute_max_pixel_distance is correct, so it should return 0 -- the character is already touching the wall, so any motion, even of just a few subpixels, -- is considered blocked @@ -1972,7 +1973,7 @@ describe('player_char', function () -- the character to x=5 pc.position = vector(5.5, 4) pc.ground_speed = 0.5 - -- we assume _compute_max_column_distance is correct, so it should return 1 + -- we assume _compute_max_pixel_distance is correct, so it should return 1 -- but we will be blocked by the wall anyway assert.are_equal(collision.ground_motion_result( @@ -1988,7 +1989,7 @@ describe('player_char', function () it('(vector(3, 4) at speed 3) should return vector(5, 4), slope before blocked, is_blocked: false, is_falling: false', function () pc.position = vector(3, 4) pc.ground_speed = 3.5 - -- we assume _compute_max_column_distance is correct, so it should return 3 + -- we assume _compute_max_pixel_distance is correct, so it should return 3 -- but because of the blocking, we stop at x=5 instead of 6.5 assert.are_equal(collision.ground_motion_result( @@ -2034,7 +2035,7 @@ describe('player_char', function () it('(vector(3, 4) at speed 3) should return vector(6, 4), slope_angle: nil, is_blocked: false, is_falling: false', function () pc.position = vector(3, 4) pc.ground_speed = 3 - -- we assume _compute_max_column_distance is correct, so it should return 3 + -- we assume _compute_max_pixel_distance is correct, so it should return 3 -- we are falling but not blocked, so we continue running in the air until x=6 assert.are_equal(collision.ground_motion_result( @@ -2050,7 +2051,7 @@ describe('player_char', function () it('(vector(3, 4) at speed 3) should return vector(7, 4), slope_angle: nil, is_blocked: false, is_falling: false', function () pc.position = vector(3, 4) pc.ground_speed = 5 - -- we assume _compute_max_column_distance is correct, so it should return 3 + -- we assume _compute_max_pixel_distance is correct, so it should return 3 -- we are falling then blocked on 7 assert.are_equal(collision.ground_motion_result( @@ -2065,7 +2066,7 @@ describe('player_char', function () end) - end) + end) -- _compute_ground_motion_result describe('_next_ground_step', function () @@ -2076,7 +2077,7 @@ describe('player_char', function () describe('(with flat ground)', function () before_each(function () - mset(0, 1, 64) -- full tile + mock_mset(0, 1, 64) -- full tile end) it('when stepping left with the right sensor still on the ground, decrement x', function () @@ -2191,11 +2192,11 @@ describe('player_char', function () before_each(function () -- X X -- XXX - mset(0, 0, 64) -- full tile (left wall) - mset(0, 1, 64) -- full tile - mset(1, 1, 64) -- full tile - mset(2, 0, 64) -- full tile - mset(2, 1, 64) -- full tile (right wall) + mock_mset(0, 0, 64) -- full tile (left wall) + mock_mset(0, 1, 64) -- full tile + mock_mset(1, 1, 64) -- full tile + mock_mset(2, 0, 64) -- full tile + mock_mset(2, 1, 64) -- full tile (right wall) end) it('when stepping left and hitting the wall, preserve x and block', function () @@ -2247,8 +2248,8 @@ describe('player_char', function () before_each(function () -- X -- X - mset(0, 1, 64) -- full tile (ground) - mset(1, 0, 64) -- full tile (wall without ground below) + mock_mset(0, 1, 64) -- full tile (ground) + mock_mset(1, 0, 64) -- full tile (wall without ground below) end) -- it will fail until _compute_signed_distance_to_closest_ground @@ -2281,8 +2282,8 @@ describe('player_char', function () before_each(function () -- X -- = - mset(0, 1, 70) -- bottom half-tile - mset(1, 0, 64) -- full tile (head wall) + mock_mset(0, 1, 70) -- bottom half-tile + mock_mset(1, 0, 64) -- full tile (head wall) end) -- it will fail until _compute_signed_distance_to_closest_ground @@ -2318,8 +2319,8 @@ describe('player_char', function () before_each(function () -- / -- X - mset(0, 1, 64) -- full tile (ground) - mset(1, 0, 65) -- ascending slope 45 + mock_mset(0, 1, 64) -- full tile (ground) + mock_mset(1, 0, 65) -- ascending slope 45 end) it('when stepping right from the bottom of the ascending slope, increment x and adjust y', function () @@ -2350,10 +2351,10 @@ describe('player_char', function () before_each(function () -- X X -- X/X - mset(0, 0, 64) -- full tile (high wall, needed to block motion to the left as right sensor makes the character quite high on the slope) - mset(0, 1, 64) -- full tile (wall) - mset(1, 1, 65) -- ascending slope 45 - mset(2, 0, 64) -- full tile (wall) + mock_mset(0, 0, 64) -- full tile (high wall, needed to block motion to the left as right sensor makes the character quite high on the slope) + mock_mset(0, 1, 64) -- full tile (wall) + mock_mset(1, 1, 65) -- ascending slope 45 + mock_mset(2, 0, 64) -- full tile (wall) end) it('when stepping left on the ascending slope without leaving the ground, decrement x and adjust y', function () @@ -2442,7 +2443,7 @@ describe('player_char', function () end) - end) + end) -- _next_ground_step describe('_is_blocked_by_ceiling_at', function () @@ -2489,7 +2490,7 @@ describe('player_char', function () assert.is_true(pc:_is_blocked_by_ceiling_at(vector(0, 4))) end) - end) + end) -- _is_blocked_by_ceiling_at describe('_is_column_blocked_by_ceiling_at', function () @@ -2505,7 +2506,7 @@ describe('player_char', function () before_each(function () -- .X - mset(1, 0, 64) -- full tile (act like a full ceiling if position is at bottom) + mock_mset(1, 0, 64) -- full tile (act like a full ceiling if position is at bottom) end) it('should return false for sensor position just above the bottom of the tile', function () @@ -2553,7 +2554,7 @@ describe('player_char', function () before_each(function () -- / - mset(0, 0, 65) + mock_mset(0, 0, 65) end) it('should return false for sensor position on the left of the tile', function () @@ -2571,7 +2572,7 @@ describe('player_char', function () end) - end) + end) -- _is_column_blocked_by_ceiling_at describe('_check_jump_intention', function () @@ -2626,113 +2627,251 @@ describe('player_char', function () player_char._enter_motion_state:clear() end) - it('should preserve (supposedly initial hop) velocity y on first frame of hop and clear has_jumped_this_frame flag', function () - pc.velocity.y = -3 -- must be < -pc_data.jump_interrupt_speed_frame (-2) - pc.has_jumped_this_frame = true - pc.hold_jump_intention = false + describe('(when _compute_air_motion_result returns a motion result with position vector(2, 8), is_blocked_by_ceiling: false, is_blocked_by_wall: false, is_landing: false)', function () - pc:_update_platformer_motion_airborne() + setup(function () + compute_air_motion_result_mock = stub(player_char, "_compute_air_motion_result", function (self) + return collision.air_motion_result( + vector(2, 8), + false, + false, + false, + nil + ) + end) + end) - assert.are_same({-pc_data.jump_interrupt_speed_frame, false}, {pc.velocity.y, pc.has_jumped_this_frame}) - end) + teardown(function () + compute_air_motion_result_mock:revert() + end) - it('should preserve (supposedly initial jump) velocity y on first frame of jump (not hop) and clear has_jumped_this_frame flag', function () - pc.velocity.y = -3 - pc.has_jumped_this_frame = true - pc.hold_jump_intention = true + after_each(function () + compute_air_motion_result_mock:clear() + end) - pc:_update_platformer_motion_airborne() + it('should set velocity y to -jump_interrupt_speed_frame on first frame of hop if velocity.y is not already greater, and clear has_jumped_this_frame flag', function () + pc.velocity.y = -3 -- must be < -pc_data.jump_interrupt_speed_frame (-2) + pc.has_jumped_this_frame = true + pc.hold_jump_intention = false - assert.are_same({-3, false}, {pc.velocity.y, pc.has_jumped_this_frame}) - end) + pc:_update_platformer_motion_airborne() - it('should apply gravity to velocity y when not on first frame of jump and not interrupting jump', function () - pc.velocity.y = -1 - pc.has_jumped_this_frame = false - pc.hold_jump_intention = true + -- interface: we are assessing the effect of _check_hold_jump directly + assert.are_same({-pc_data.jump_interrupt_speed_frame, false}, {pc.velocity.y, pc.has_jumped_this_frame}) + end) - pc:_update_platformer_motion_airborne() + it('should preserve velocity y completely on first frame of hop if velocity.y is already greater, and clear has_jumped_this_frame flag', function () + -- this can happen when character is running down a steep slope, and hops with a normal close to horizontal + pc.velocity.y = -1 -- must be >= -pc_data.jump_interrupt_speed_frame (-2) + pc.has_jumped_this_frame = true + pc.hold_jump_intention = false - assert.are_same({-1 + pc_data.gravity_frame2, false}, {pc.velocity.y, pc.has_jumped_this_frame}) - end) + pc:_update_platformer_motion_airborne() - it('should set to speed y to interrupt speed (no gravity added) when interrupting actual jump', function () - pc.velocity.y = -3 -- must be < -pc_data.jump_interrupt_speed_frame (-2) - pc.has_jumped_this_frame = false - pc.hold_jump_intention = false + -- interface: we are assessing the effect of _check_hold_jump directly + assert.are_same({-1, false}, {pc.velocity.y, pc.has_jumped_this_frame}) + end) - pc:_update_platformer_motion_airborne() + it('should preserve (supposedly initial jump) velocity y on first frame of jump (not hop) and clear has_jumped_this_frame flag', function () + pc.velocity.y = -3 + pc.has_jumped_this_frame = true + pc.hold_jump_intention = true - -- interface: we are assessing the effect of _check_hold_jump directly - assert.are_same({-pc_data.jump_interrupt_speed_frame, false}, {pc.velocity.y, pc.has_jumped_this_frame}) - end) + pc:_update_platformer_motion_airborne() - it('should apply accel x', function () - pc.velocity.x = 4 - pc.move_intention.x = -1 + -- interface: we are assessing the effect of _check_hold_jump directly + assert.are_same({-3, false}, {pc.velocity.y, pc.has_jumped_this_frame}) + end) - pc:_update_platformer_motion_airborne() + it('should apply gravity to velocity y when not on first frame of jump and not interrupting jump', function () + pc.velocity.y = -1 + pc.has_jumped_this_frame = false + pc.hold_jump_intention = true - assert.are_equal(4 - pc_data.air_accel_x_frame2, pc.velocity.x) - end) + pc:_update_platformer_motion_airborne() - -- bugfix history: - -- . - it('should update position with new speed y', function () - pc.position = vector(4, -4) - pc.velocity.y = -3 - pc.hold_jump_intention = true + -- interface: we are assessing the effect of _check_hold_jump directly + assert.are_same({-1 + pc_data.gravity_frame2, false}, {pc.velocity.y, pc.has_jumped_this_frame}) + end) - pc:_update_platformer_motion_airborne() - assert.are_equal(vector(4, -4 - 3 + pc_data.gravity_frame2), pc.position) - end) + it('should set to speed y to interrupt speed (no gravity added) when interrupting actual jump', function () + pc.velocity.y = -3 -- must be < -pc_data.jump_interrupt_speed_frame (-2) + pc.has_jumped_this_frame = false + pc.hold_jump_intention = false - describe('(_check_escape_from_ground returns false, so has not landed)', function () + pc:_update_platformer_motion_airborne() - local check_escape_from_ground_mock + -- interface: we are assessing the effect of _check_hold_jump directly + -- note that gravity is applied *before* interrupt jump, so we don't see it in the final velocity.y + assert.are_same({-pc_data.jump_interrupt_speed_frame, false}, {pc.velocity.y, pc.has_jumped_this_frame}) + end) + + it('should set to speed y to interrupt speed (no gravity added) when interrupting actual jump', function () + pc.velocity.y = -1 -- must be >= -pc_data.jump_interrupt_speed_frame (-2) + pc.has_jumped_this_frame = false + pc.hold_jump_intention = false + + pc:_update_platformer_motion_airborne() + + -- interface: we are assessing the effect of _check_hold_jump directly + assert.are_same({-1 + pc_data.gravity_frame2, false}, {pc.velocity.y, pc.has_jumped_this_frame}) + end) + + it('should apply air accel x', function () + pc.velocity.x = 4 + pc.move_intention.x = -1 + + pc:_update_platformer_motion_airborne() + + -- interface: we are assessing the effect of _check_hold_jump directly + assert.are_equal(4 - pc_data.air_accel_x_frame2, pc.velocity.x) + end) + + -- bugfix history: + -- . + it('should update position with air motion result position', function () + pc.position = vector(0, 0) -- doesn't matter, since we mock _compute_air_motion_result + + pc:_update_platformer_motion_airborne() + + assert.are_equal(vector(2, 8), pc.position) + end) + + it('should preserve velocity.y', function () + -- set those flags to true to make computations more simple: + -- velocity.y will not affected by gravity nor interrupt jump + pc.has_jumped_this_frame = true + pc.hold_jump_intention = true + pc.velocity = vector(10, -10) + + pc:_update_platformer_motion_airborne() + + assert.are_equal(-10, pc.velocity.y) + end) + + end) -- compute_air_motion_result_mock (vector(2, 8), false, false, false) + + describe('(when _compute_air_motion_result returns a motion result with is_blocked_by_wall: false, is_blocked_by_ceiling: true)', function () setup(function () - check_escape_from_ground_mock = stub(player_char, "_check_escape_from_ground", function () - return false + compute_air_motion_result_mock = stub(player_char, "_compute_air_motion_result", function (self) + return collision.air_motion_result( + vector(2, 8), + false, -- not the focus, but verified + true, -- focus in this test + false, + nil + ) end) end) teardown(function () - check_escape_from_ground_mock:revert() + compute_air_motion_result_mock:revert() end) - it('should not enter grounded state', function () + after_each(function () + compute_air_motion_result_mock:clear() + end) + + it('should set velocity.y to 0', function () + -- set those flags to true to make computations more simple: + -- velocity.y will not affected by gravity nor interrupt jump + pc.has_jumped_this_frame = true + pc.hold_jump_intention = true + pc.velocity = vector(10, -10) + pc:_update_platformer_motion_airborne() - -- implementation - assert.spy(pc._enter_motion_state).was_not_called() + assert.are_equal(0, pc.velocity.y) end) - end) + it('should preserve velocity.x', function () + pc.velocity = vector(10, -10) - describe('(_check_escape_from_ground returns true, so has landed)', function () + pc:_update_platformer_motion_airborne() + + assert.are_equal(10, pc.velocity.x) + end) - local check_escape_from_ground_mock + end) -- compute_air_motion_result_mock (is_blocked_by_ceiling: true) + + describe('(when _compute_air_motion_result returns a motion result with is_blocked_by_wall: true, is_blocked_by_ceiling: false)', function () setup(function () - check_escape_from_ground_mock = stub(player_char, "_check_escape_from_ground", function () - return true + compute_air_motion_result_mock = stub(player_char, "_compute_air_motion_result", function (self) + return collision.air_motion_result( + vector(2, 8), + true, -- focus in this test + false, -- not the focus, but verified + false, + nil + ) end) end) teardown(function () - check_escape_from_ground_mock:revert() + compute_air_motion_result_mock:revert() end) - it('should enter grounded state', function () + after_each(function () + compute_air_motion_result_mock:clear() + end) + + it('should preserve velocity.y', function () + -- set those flags to true to make computations more simple: + -- velocity.y will not affected by gravity nor interrupt jump + pc.has_jumped_this_frame = true + pc.hold_jump_intention = true + pc.velocity = vector(10, -10) + + pc:_update_platformer_motion_airborne() + + assert.are_equal(-10, pc.velocity.y) + end) + + it('should set velocity.x to 0', function () + pc.velocity = vector(10, -10) + + pc:_update_platformer_motion_airborne() + + assert.are_equal(0, pc.velocity.x) + end) + + end) + + describe('(when _compute_air_motion_result returns a motion result with is_landing: true, slope_angle: 0.5)', function () + + setup(function () + compute_air_motion_result_mock = stub(player_char, "_compute_air_motion_result", function (self) + return collision.air_motion_result( + vector(2, 8), + false, + false, + true, -- focus in this test + 0.5 + ) + end) + end) + + teardown(function () + compute_air_motion_result_mock:revert() + end) + + after_each(function () + compute_air_motion_result_mock:clear() + end) + + it('should enter grounded state with slope_angle: 0.5', function () pc:_update_platformer_motion_airborne() -- implementation assert.spy(pc._enter_motion_state).was_called(1) assert.spy(pc._enter_motion_state).was_called_with(match.ref(pc), motion_states.grounded) + + assert.are_equal(0.5, pc.slope_angle) end) - end) + end) -- compute_air_motion_result_mock (is_blocked_by_wall: true) end) -- _update_platformer_motion_airborne @@ -2781,6 +2920,437 @@ describe('player_char', function () end) + describe('_compute_air_motion_result', function () + + it('(when velocity is zero) should return air_motion_result with initial position and no hits', function () + pc.position = vector(4, 8) + assert.are_equal(collision.air_motion_result( + vector(4, 8), + false, + false, + false, + nil + ), pc:_compute_air_motion_result()) + end) + + describe('(when _advance_in_air_along returns an air_motion_result with full motion done along x, half motion done with hit ceiling along y)', function () + + setup(function () + advance_in_air_along_mock = stub(player_char, "_advance_in_air_along", function (self, ref_motion_result, velocity, coord) + if coord == "x" then + local motion = vector(velocity.x, 0) + ref_motion_result.position = ref_motion_result.position + motion + else -- coord == "y" + -- to make sure we are calling _advance_in_air_along on y before x, we add a check here: + -- if we have already moved from initial pos.x = 4.5 (see test below), block any motion along y + if ref_motion_result.position.x == 4.5 then + local motion = vector(0, velocity.y / 2) + ref_motion_result.position = ref_motion_result.position + motion + end + ref_motion_result.is_blocked_by_ceiling = true + end + end) + end) + + teardown(function () + advance_in_air_along_mock:revert() + end) + + after_each(function () + advance_in_air_along_mock:clear() + end) + + it('(when velocity is zero) should return air_motion_result with initial position and no hits', function () + pc.position = vector(4.5, 8) + pc.velocity = vector(5, -12) + + -- character should advance of (5, -6) resulting in pos (9.5, 2) + + -- interface: check that the final result is correct + assert.are_equal(collision.air_motion_result( + vector(9.5, 2), + false, + true, -- hit ceiling + false, + nil + ), pc:_compute_air_motion_result()) + end) + + end) + + end) + + describe('_advance_in_air_along', function () + + describe('(when _next_air_step moves motion_result.position.x/y by 1px in the given direction, ' .. + 'unless moving along x from x >= 5, where it is blocking by wall)', function () + + local next_air_step_mock + + setup(function () + next_air_step_mock = stub(player_char, "_next_air_step", function (self, direction, motion_result) + if coord == "y" or motion_result.position.x < 5 then + local step_vec = dir_vectors[direction] + motion_result.position = motion_result.position + step_vec + else + motion_result.is_blocked_by_wall = true + end + end) + end) + + teardown(function () + next_air_step_mock:revert() + end) + + after_each(function () + next_air_step_mock:clear() + end) + + it('(vector(0.5, 10) at speed 0.5 along x) should move to vector(1, 10) without blocking', function () + local motion_result = collision.air_motion_result( + vector(1, 10), + false, + false, + false, + nil + ) + + -- we assume _compute_max_pixel_distance is correct + pc:_advance_in_air_along(motion_result, vector(0.5, 99), "x") + + assert.are_equal(collision.air_motion_result( + vector(1, 10), + false, + false, + false, + nil + ), motion_result + ) + end) + + it('(vector(0.4, 10) at speed 2.7 along x) should move to vector(3.1, 10)', function () + local motion_result = collision.air_motion_result( + vector(0.4, 10), + false, + false, + false, + nil + ) + + -- we assume _compute_max_pixel_distance is correct + pc:_advance_in_air_along(motion_result, vector(2.7, 99), "x") + + assert.are_equal(collision.air_motion_result( + vector(3.1, 10), + false, + false, + false, + nil + ), motion_result + ) + end) + + it('(vector(2.5, 10) at speed 2.7 along x) should move to vector(5, 10) and blocked by wall', function () + local motion_result = collision.air_motion_result( + vector(2.5, 10), + false, + false, + false, + nil + ) + + -- we assume _compute_max_pixel_distance is correct + pc:_advance_in_air_along(motion_result, vector(2.7, 99), "x") + + assert.are_equal(collision.air_motion_result( + vector(5, 10), + true, + false, + false, + nil + ), motion_result + ) + end) + + it('(vector(2.5, 7.3) at speed -4.4 along y) should move to vector(2.5, 2.9)', function () + local motion_result = collision.air_motion_result( + vector(2.5, 7.3), + false, + false, + false, + nil + ) + + -- we assume _compute_max_pixel_distance is correct + pc:_advance_in_air_along(motion_result, vector(99, -4.4), "y") + + assert.is_true(almost_eq_with_message(vector(2.5, 2.9), motion_result.position)) + assert.are_same({ + false, + false, + false + }, { + motion_result.is_blocked_by_wall, + motion_result.is_blocked_by_ceiling, + motion_result.is_landing + }) + end) + + end) + + end) + + describe('_next_air_step', function () + it('(in the air) direction up should move 1px up without being blocked', function () + local motion_result = collision.air_motion_result( + vector(2, 7), + false, + false, + false, + nil + ) + + pc:_next_air_step(directions.up, motion_result) + + assert.are_equal(collision.air_motion_result( + vector(2, 6), + false, + false, + false, + nil + ), + motion_result + ) + end) + + it('(in the air) direction down should move 1px down without being blocked', function () + local motion_result = collision.air_motion_result( + vector(2, 7), + false, + false, + false, + nil + ) + + pc:_next_air_step(directions.down, motion_result) + + assert.are_equal(collision.air_motion_result( + vector(2, 8), + false, + false, + false, + nil + ), + motion_result + ) + end) + + it('(in the air) direction left should move 1px left without being blocked', function () + local motion_result = collision.air_motion_result( + vector(2, 7), + false, + false, + false, + nil + ) + + pc:_next_air_step(directions.left, motion_result) + + assert.are_equal(collision.air_motion_result( + vector(1, 7), + false, + false, + false, + nil + ), + motion_result + ) + end) + + it('(in the air) direction right should move 1px right without being blocked', function () + local motion_result = collision.air_motion_result( + vector(2, 7), + false, + false, + false, + nil + ) + + pc:_next_air_step(directions.right, motion_result) + + assert.are_equal(collision.air_motion_result( + vector(3, 7), + false, + false, + false, + nil + ), + motion_result + ) + end) + + describe('(with mock tiles data setup)', function () + + setup(function () + tile_test_data.setup() + end) + + teardown(function () + tile_test_data.teardown() + end) + + after_each(function () + pico8:clear_map() + end) + + -- for these utests, we assume that _compute_ground_sensors_signed_distance and + -- _is_blocked_by_ceiling are correct, + -- so rather than mocking them, so we setup simple tiles to walk on + + describe('(with flat ground)', function () + + before_each(function () + mock_mset(0, 0, 64) -- full tile + end) + + it('direction up into ceiling should not move, and flag is_blocked_by_ceiling', function () + local motion_result = collision.air_motion_result( + vector(4, 8 + pc_data.full_height_standing - pc_data.center_height_standing), + false, + false, + false, + nil + ) + + pc:_next_air_step(directions.up, motion_result) + + assert.are_equal(collision.air_motion_result( + vector(4, 8 + pc_data.full_height_standing - pc_data.center_height_standing), + false, + true, + false, + nil + ), + motion_result + ) + end) + + it('direction down into ground should not move, and flag is_landing with slope_angle', function () + local motion_result = collision.air_motion_result( + vector(4, 0 - pc_data.center_height_standing), + false, + false, + false, + nil + ) + + pc:_next_air_step(directions.down, motion_result) + + assert.are_equal(collision.air_motion_result( + vector(4, 0 - pc_data.center_height_standing), + false, + false, + true, + 0 + ), + motion_result + ) + end) + + it('direction left into wall via ground should not move, and flag is_blocked_by_wall', function () + local motion_result = collision.air_motion_result( + vector(11, 1 - pc_data.center_height_standing), + false, + false, + false, + nil + ) + + pc:_next_air_step(directions.left, motion_result) + + assert.are_equal(collision.air_motion_result( + vector(11, 1 - pc_data.center_height_standing), + true, + false, + false, + nil + ), + motion_result + ) + end) + + it('direction right into wall via ceiling should not move, and flag is_blocked_by_wall', function () + local motion_result = collision.air_motion_result( + vector(-3, 7 + pc_data.full_height_standing - pc_data.center_height_standing), + false, + false, + false, + nil + ) + + pc:_next_air_step(directions.right, motion_result) + + assert.are_equal(collision.air_motion_result( + vector(-3, 7 + pc_data.full_height_standing - pc_data.center_height_standing), + true, + false, + false, + nil + ), + motion_result + ) + end) + + it('(after landing in previous step) direction right onto new ground should move and update slope_angle', function () + local motion_result = collision.air_motion_result( + vector(-3, 0 - pc_data.center_height_standing), + false, + false, + true, + 0.5 + ) + + pc:_next_air_step(directions.right, motion_result) + + assert.are_equal(collision.air_motion_result( + vector(-2, 0 - pc_data.center_height_standing), + false, + false, + true, + 0 + ), + motion_result + ) + end) + + it('(after landing in previous step) direction left into the air should move and unset is_landing', function () + local motion_result = collision.air_motion_result( + vector(-2, 0 - pc_data.center_height_standing), + false, + false, + true, + 0 + ) + + pc:_next_air_step(directions.left, motion_result) + + assert.are_equal(collision.air_motion_result( + vector(-3, 0 - pc_data.center_height_standing), + false, + false, + false, + nil + ), + motion_result + ) + end) + + end) + + end) -- (with mock tiles data setup) + + end) -- _next_air_step + describe('_update_debug', function () local update_velocity_debug_stub diff --git a/tests/utestworld.lua b/tests/utestworld.lua index 736568c8..c1b3659c 100644 --- a/tests/utestworld.lua +++ b/tests/utestworld.lua @@ -29,7 +29,7 @@ describe('world (with mock tiles data setup)', function () before_each(function () -- create an invalid tile with a collision flag but no collision mask associated - mset(1, 1, 1) + mock_mset(1, 1, 1) end) it('should assert if tile has collision flag set but no collision mask id associated', function () @@ -45,7 +45,7 @@ describe('world (with mock tiles data setup)', function () before_each(function () -- create an ascending slope 22.5 at (1, 1), i.e. (8, 14) to (15, 11) px - mset(1, 1, 67) + mock_mset(1, 1, 67) end) it('should return 3 on column 3', function () @@ -62,7 +62,7 @@ describe('world (with mock tiles data setup)', function () before_each(function () -- create a full tile at (1, 1), i.e. (8, 8) to (15, 15) px - mset(1, 1, 64) + mock_mset(1, 1, 64) end) it('should return {false, nil} on (7, 7)', function () @@ -141,7 +141,7 @@ describe('world (with mock tiles data setup)', function () before_each(function () -- create an ascending slope at (1, 1), i.e. (8, 15) to (15, 8) px - mset(1, 1, 65) + mock_mset(1, 1, 65) end) it('should return {false, nil} on (8, 14)', function () From 9fc2fb1eddaa0143decbf705a9e22363d1e7ea17 Mon Sep 17 00:00:00 2001 From: hsandt Date: Sat, 13 Apr 2019 00:03:17 +0100 Subject: [PATCH 069/140] [MOTION] Fixed air motion pixel flooring system --- src/game/ingame/playercharacter.lua | 118 ++++++---- src/game/itests/itestplayercharacter.lua | 70 ++++-- tests/bustedhelper.lua | 1 + tests/utestplayercharacter.lua | 276 +++++++++++++++++++++-- 4 files changed, 397 insertions(+), 68 deletions(-) diff --git a/src/game/ingame/playercharacter.lua b/src/game/ingame/playercharacter.lua index 89918ba3..25d9028c 100644 --- a/src/game/ingame/playercharacter.lua +++ b/src/game/ingame/playercharacter.lua @@ -471,8 +471,10 @@ function player_char:_compute_ground_motion_result() local horizontal_dir = signed_speed_to_dir(self.ground_speed) -- initialise result with floored x (we will reinject subpixels if character didn't touch a wall) + -- note that left and right are not completely symmetrical since floor is asymmetrical + local floored_x = flr(self.position.x) local motion_result = collision.ground_motion_result( - vector(flr(self.position.x), self.position.y), + vector(floored_x, self.position.y), self.slope_angle, false, false @@ -480,39 +482,58 @@ function player_char:_compute_ground_motion_result() -- only full pixels matter for collisions, but subpixels may sum up to a full pixel -- so first estimate how many full pixel columns the character may actually explore this frame - local distance_x = self.ground_speed * cos(self.slope_angle) - local max_column_distance = player_char._compute_max_pixel_distance(self.position.x, distance_x) + local signed_distance_x = self.ground_speed * cos(self.slope_angle) + local max_column_distance = player_char._compute_max_pixel_distance(self.position.x, signed_distance_x) -- iterate pixel by pixel on the x direction until max possible distance is reached -- only stopping if the character is blocked by a wall (not if falling, since we want -- him to continue moving in the air as far as possible; in edge cases, he may even -- touch the ground again some pixels farther) - local column_distance = 1 - while column_distance <= max_column_distance and not motion_result.is_blocked do + local column_distance_before_step = 0 + while column_distance_before_step < max_column_distance and not motion_result.is_blocked do self:_next_ground_step(horizontal_dir, motion_result) - column_distance = column_distance + 1 + column_distance_before_step = column_distance_before_step + 1 end -- check if we need to add or cut subpixels if not motion_result.is_blocked then - local are_subpixels_left = self.position.x + distance_x > motion_result.position.x + -- local max_distance_x = abs(signed_distance_x) + -- local distance_to_floored_x = abs(motion_result.position.x - floored_x) + -- since character was not blocked, we know that we have reached a column distance of max_column_distance + -- local are_subpixels_left = max_distance_x > distance_to_floored_x + -- since subpixels are always counted to the right, the subpixel test below is asymmetrical + -- but this is correct, we will simply move backward a bit when moving left + local are_subpixels_left = self.position.x + signed_distance_x > motion_result.position.x + if are_subpixels_left then -- character has not been blocked and has some subpixels left to go - -- check if character has touched a wall (we need an extra step to "ceil" the subpixels) - local extra_step_motion_result = motion_result:copy() - self:_next_ground_step(horizontal_dir, extra_step_motion_result) - if extra_step_motion_result.is_blocked then - -- character has just reached a wall, plus a few subpixels - -- unlike Classic Sonic, we decide to cut the subpixels and block the character - -- on the spot (we just reuse the result of the extra step, since is_falling doesn't change if is_blocked is true) - motion_result = extra_step_motion_result - else + -- unlike Classic Sonic, and *only* when moving right, we decide to check if those + -- subpixels would leak to hitting a wall on the right, and cut them if so, + -- blocking the character on the spot (we just reuse the result of the extra step, + -- since is_falling doesn't change if is_blocked is true) + -- when moving left, the subpixels are a small "backward" motion to the right and should + -- never hit a wall back + local is_blocked_by_extra_step = false + if signed_distance_x > 0 then + local extra_step_motion_result = motion_result:copy() + self:_next_ground_step(horizontal_dir, extra_step_motion_result) + if extra_step_motion_result.is_blocked then + motion_result = extra_step_motion_result + is_blocked_by_extra_step = true + end + end + + -- unless moving right and hitting a wall due to subpixels, apply the remaining subpixels + -- as they cannot affect collision anymore. when moving left, they go a little backward + if not is_blocked_by_extra_step then -- character has not touched a wall at all, so add the remaining subpixels - -- (it's simpler to just recompute the full motion in x; don't touch y tough, - -- as it depends on the shape of the ground) + -- (it's simpler to just recompute the full motion in x; don't touch y tough, + -- as it depends on the shape of the ground) -- do not apply other changes (like slope) since technically we have not reached - -- the next tile yet, only advanced of some subpixels - motion_result.position.x = self.position.x + distance_x + -- the next tile yet, only advanced of some subpixels + -- note that this calculation equivalent to adding to ref_motion_result.position[coord] + -- sign(signed_distance_x) * (max_distance_x - distance_to_floored_x) + motion_result.position.x = self.position.x + signed_distance_x end end end @@ -521,8 +542,12 @@ function player_char:_compute_ground_motion_result() end -- return the number of new pixel columns explored when moving from initial_position_coord (x or y) --- over velocity_coord (x or y) * 1 frame. this is either flr(velocity_coord) +-- over velocity_coord (x or y) * 1 frame. consider full pixel motion starting at floored coord, +-- even when moving in the negative direction +-- this is either flr(velocity_coord) -- or flr(velocity_coord) + 1 (if subpixels from initial position coord and speed sum up to 1.0 or more) +-- note that for negative motion, we must go a bit beyond the next integer to count a full pixel motion, +-- and that is intended function player_char._compute_max_pixel_distance(initial_position_coord, velocity_coord) return abs(flr(initial_position_coord + velocity_coord) - flr(initial_position_coord)) end @@ -802,10 +827,12 @@ function player_char:_compute_air_motion_result() return motion_result end +-- TODO: factorize with _compute_ground_motion_result -- modifies ref_motion_result in-place, setting it to the result of an air motion from ref_motion_result.position -- over velocity[coord] px, where coord is "x" or "y" function player_char:_advance_in_air_along(ref_motion_result, velocity, coord) - if velocity_coord == 0 then return end + if velocity[coord] == 0 then return end + -- only full pixels matter for collisions, but subpixels may sum up to a full pixel -- so first estimate how many full pixel columns the character may actually explore this frame @@ -821,7 +848,6 @@ function player_char:_advance_in_air_along(ref_motion_result, velocity, coord) -- only stopping if the character is blocked by a wall (not if falling, since we want -- him to continue moving in the air as far as possible; in edge cases, he may even -- touch the ground again some pixels farther) - local pixel_distance = 1 local direction if coord == "x" then direction = directions.right @@ -832,33 +858,49 @@ function player_char:_advance_in_air_along(ref_motion_result, velocity, coord) direction = oppose_direction(direction) end - local is_blocked = coord == "x" and ref_motion_result.is_blocked_by_wall or (ref_motion_result.is_blocked_by_wall or ref_motion_result.is_landing) - while pixel_distance <= max_pixel_distance and not ref_motion_result:is_blocked_along(direction) do + local pixel_distance_before_step = 0 + while pixel_distance_before_step < max_pixel_distance and not ref_motion_result:is_blocked_along(direction) do self:_next_air_step(direction, ref_motion_result) - pixel_distance = pixel_distance + 1 + pixel_distance_before_step = pixel_distance_before_step + 1 end - -- check if we need to add or cut subpixels if not ref_motion_result:is_blocked_along(direction) then - local are_subpixels_left = initial_position_coord + max_pixel_distance > ref_motion_result.position[coord] + -- since subpixels are always counted to the right, the subpixel test below is asymmetrical + -- but this is correct, we will simply move backward a bit when moving left + local are_subpixels_left = initial_position_coord + velocity[coord] > ref_motion_result.position[coord] + -- local are_subpixels_left = initial_position_coord + max_pixel_distance > ref_motion_result.position[coord] if are_subpixels_left then -- character has not been blocked and has some subpixels left to go - -- check if character has touched a wall (we need an extra step to "ceil" the subpixels) - local extra_step_motion_result = ref_motion_result:copy() - self:_next_air_step(direction, extra_step_motion_result) - if extra_step_motion_result:is_blocked_along(direction) then - -- character has just reached a wall, plus a few subpixels - -- unlike classic sonic, we decide to cut the subpixels and block the character - -- on the spot (we just reuse the result of the extra step, since is_falling doesn't change if is_blocked is true) - -- it's very important to keep the reference and assign member values instead - ref_motion_result:copy_assign(extra_step_motion_result) - else + -- *only* when moving in the positive sense (right/up), + -- as a way to clean the subpixels unlike classic sonic, + -- we check if character is theoretically colliding a wall with those subpixels + -- (we need an extra step to "ceil" the subpixels) + -- when moving in the negative sense, the subpixels are a small "backward" motion + -- to the positive sense and should + -- never hit a wall back + local is_blocked_by_extra_step = false + if velocity[coord] > 0 then + local extra_step_motion_result = ref_motion_result:copy() + self:_next_air_step(direction, extra_step_motion_result) + if extra_step_motion_result:is_blocked_along(direction) then + -- character has just reached a wall, plus a few subpixels + -- unlike classic sonic, we decide to cut the subpixels and block the character + -- on the spot (we just reuse the result of the extra step, since is_falling doesn't change if is_blocked is true) + -- it's very important to keep the reference and assign member values instead + ref_motion_result:copy_assign(extra_step_motion_result) + is_blocked_by_extra_step = true + end + end + + if not is_blocked_by_extra_step then -- character has not touched a wall at all, so add the remaining subpixels -- (it's simpler to just recompute the full motion in x; don't touch y tough, -- as it depends on the shape of the ground) -- do not apply other changes (like slope) since technically we have not reached -- the next tile yet, only advanced of some subpixels + -- note that this calculation equivalent to adding to ref_motion_result.position[coord] + -- sign(velocity[coord]) * (max_distance - distance_to_floored_coord) ref_motion_result.position[coord] = initial_position_coord + velocity[coord] end end diff --git a/src/game/itests/itestplayercharacter.lua b/src/game/itests/itestplayercharacter.lua index 3126da33..e49290a4 100644 --- a/src/game/itests/itestplayercharacter.lua +++ b/src/game/itests/itestplayercharacter.lua @@ -426,13 +426,13 @@ expect pc_velocity 0 -0.078125 -- calculation notes -- wait for the apogee (frame 31) and stop --- at frame 1: pos (4, 80), velocity (0, 0), grounded (waits 1 frame before confirming hop/jump) --- at frame 2: pos (4, 80 - 3.25), velocity (0, -3.25), airborne (do not apply gravity on first frame of jump since we were grounded) --- at frame 30: pos (4, 80 - 49.84375), velocity (0, -0.1875), airborne -> before apogee --- at frame 31: pos (4, 80 - 49.921875), velocity (0, -0.078125), airborne -> reached apogee (100px in 16-bit, matches SPG on Jumping) --- at frame 32: pos (4, 80 - 49.890625), velocity (0, 0.03125), airborne -> starts going down --- at frame 61: pos (4, 80 - 1.40625), velocity (0, 3.203125), airborne -> about to land --- at frame 62: pos (4, 80), velocity (0, 0), grounded -> has landed +-- at frame 1: bpos (4, 8), velocity (0, 0), grounded (waits 1 frame before confirming hop/jump) +-- at frame 2: bpos (4, 8 - 3.25), velocity (0, -3.25), airborne (do not apply gravity on first frame of jump since we were grounded) +-- at frame 30: bpos (4, 8 - 49.84375), velocity (0, -0.1875), airborne -> before apogee +-- at frame 31: bpos (4, 8 - 49.921875), velocity (0, -0.078125), airborne -> reached apogee (100px in 16-bit, matches SPG on Jumping) +-- at frame 32: bpos (4, 8 - 49.890625), velocity (0, 0.03125), airborne -> starts going down +-- at frame 61: bpos (4, 8 - 1.40625), velocity (0, 3.203125), airborne -> about to land +-- at frame 62: bpos (4, 8), velocity (0, 0), grounded -> has landed itest_dsl_parser.register( @@ -468,14 +468,14 @@ expect pc_velocity 0 0 -- calculation notes: -- wait for apogee (frame 20) and stop --- at frame 1: bpos (4, 80), velocity (0, 0), grounded (waits 1 frame before confirming hop/jump) --- at frame 2: bpos (4, 80 - 2), velocity (0, -2), airborne (hop confirmed) --- at frame 3: bpos (4, 80 - 3.890625), velocity (0, -1.890625), airborne (hop confirmed) --- at frame 19: pos (4, 80 - 19.265625), velocity (0, -0.140625), airborne -> before apogee --- at frame 20: pos (4, 80 - 19.296875), velocity (0, -0.03125), airborne -> reached apogee --- at frame 21: pos (4, 80 - 19.21875), velocity (0, 0.078125), airborne -> starts going down --- at frame 38: pos (4, 80 - 1.15625), velocity (0, 1.9375), airborne -> about to land --- at frame 39: pos (4, 80), velocity (0, 0), grounded -> has landed +-- at frame 1: bpos (4, 8), velocity (0, 0), grounded (waits 1 frame before confirming hop/jump) +-- at frame 2: bpos (4, 8 - 2), velocity (0, -2), airborne (hop confirmed) +-- at frame 3: bpos (4, 8 - 3.890625), velocity (0, -1.890625), airborne (hop confirmed) +-- at frame 19: bpos (4, 8 - 19.265625), velocity (0, -0.140625), airborne -> before apogee +-- at frame 20: bpos (4, 8 - 19.296875), velocity (0, -0.03125), airborne -> reached apogee +-- at frame 21: bpos (4, 8 - 19.21875), velocity (0, 0.078125), airborne -> starts going down +-- at frame 38: bpos (4, 8 - 1.15625), velocity (0, 1.9375), airborne -> about to land +-- at frame 39: bpos (4, 8), velocity (0, 0), grounded -> has landed -- and wait an extra frame to see if Sonic will jump due to holding jump input, -- so stop at frame 40 @@ -500,6 +500,46 @@ expect pc_velocity 1.359375 -0.078125 ]]) +itest_dsl_parser.register( + 'platformer air wall block', [[ +@stage # +.# +.. +#. + +warp 4 16 +jump +stop_jump +wait 1 +move right +wait 9 + +expect pc_bottom_pos 5 1.9375 +expect pc_motion_state airborne +expect pc_ground_spd 0 +expect pc_velocity 0 -1.125 +]]) + +-- calculation notes: +-- start jump input +-- at frame 1: bpos (4, 16), velocity (0, 0), grounded +-- wait 1 frame to confirm hop, and start moving right, then wait 9 frames +-- at frame 2: bpos (4 + .046875, 16 - 2), velocity (3/64, -2), airborne (hop) +-- at frame 3: bpos (4.140625, 16 - 3.890625), velocity (6/64, -1 - 57/64), airborne +-- at frame 4: bpos (4.28125, 16 - 5.671875), velocity (9/64, -1 - 50/64), airborne +-- at frame 5: bpos (4.46875, 16 - 7.34375), velocity (12/64, -1 - 43/64), airborne +-- at frame 6: bpos (4.703125, 16 - 8.90625), velocity (15/64, -1 - 36/64), airborne +-- at frame 7: bpos (4.984375, 16 - 10.359375), velocity (18/64, -1 - 29/64), airborne +-- after 7 frames, we are almost touching the wall above +-- at frame 8: bpos (5, 16 - 11.703125), velocity (18/64, -1 - 22/64), airborne (hit wall) +-- after 8 frames, we have hit the wall +-- at frame 9: bpos (5, 16 - 12.9375), velocity (0, -1 - 15/64), airborne (hit wall) +-- at frame 10: bpos (5, 16 - 14.0625), velocity (0, -1 - 8/64), airborne (hit wall) + +-- /64 format is nice, but I need to make a helper +-- that converts floats to this format if I want a meaningful +-- comparison with itest trace log + -- human tests -- pico8 only, since human must check rendering --[=[#pico8 diff --git a/tests/bustedhelper.lua b/tests/bustedhelper.lua index b3af79f0..edba0a01 100644 --- a/tests/bustedhelper.lua +++ b/tests/bustedhelper.lua @@ -37,6 +37,7 @@ end -- R the test revealed the same bug/regression as another test, being redundant while not helping to discover another bug -- < the test was incomplete, but by examining the code I spotted suspicious cases that I could verify by improving the test, then fix the cases -- ^ the test was incomplete, so when I stumbled on a special case bug, I improved the test to make sure I fixed it and avoid regression later +-- / the test was missing at first, but when I spotted missing cases I added them, although the test passed immediately anyway -- _ the test was missing at first, but by examining the code I spotted suspicious cases that I could verify by adding a new test, then fix the cases -- = the test was missing at first so when I stumbled on a new bug, I wrote that test to make sure I fixed it and avoid regression later -- + the test helped me identify potential bugs and tricky cases during implementation of the function using the test diff --git a/tests/utestplayercharacter.lua b/tests/utestplayercharacter.lua index 9d862797..8851a1a6 100644 --- a/tests/utestplayercharacter.lua +++ b/tests/utestplayercharacter.lua @@ -32,6 +32,38 @@ describe('player_char', function () assert.are_equal(2, player_char._compute_max_pixel_distance(2.2, 1.8)) end) + -- bugfix history: + -- / I completely forgot the left case, which is important to test flooring asymmetry + -- I thought it was hiding bugs, but I realize my asymmetrical design was actually fine + + it('(2, -0.1) => 1', function () + assert.are_equal(1, player_char._compute_max_pixel_distance(2, -0.1)) + end) + + it('(2, -1) => 1', function () + assert.are_equal(1, player_char._compute_max_pixel_distance(2, -1)) + end) + + it('(2, -1.1) => 2', function () + assert.are_equal(2, player_char._compute_max_pixel_distance(2, -1.1)) + end) + + it('(2.2, -0.2) => 0', function () + assert.are_equal(0, player_char._compute_max_pixel_distance(2.2, -0.2)) + end) + + it('(2.2, -0.3) => 1', function () + assert.are_equal(1, player_char._compute_max_pixel_distance(2.2, -0.3)) + end) + + it('(2.2, -1.2) => 1', function () + assert.are_equal(1, player_char._compute_max_pixel_distance(2.2, -1.2)) + end) + + it('(2.2, -1.3) => 2', function () + assert.are_equal(2, player_char._compute_max_pixel_distance(2.2, -1.3)) + end) + end) describe('_init', function () @@ -1825,18 +1857,21 @@ describe('player_char', function () end) - describe('(when _next_ground_step moves motion_result.position.x by 1px in the horizontal_dir, but blocks when motion_result.position.x >= 5)', function () + describe('(when _next_ground_step moves motion_result.position.x by 1px in the horizontal_dir, but blocks when motion_result.position.x <= -5 or x >= 5)', function () local next_ground_step_mock setup(function () next_ground_step_mock = stub(player_char, "_next_ground_step", function (self, horizontal_dir, motion_result) local step_vec = horizontal_dir_vectors[horizontal_dir] - if motion_result.position.x < 5 then + -- x < -4 <=> x <= -5 for an integer as passed to step functions, + -- but we want to make clear that flooring is asymmetrical + -- and that for floating coordinates, -4.01 is already hitting the left wall + if motion_result.position.x < -4 and step_vec.x < 0 or motion_result.position.x >= 5 and step_vec.x > 0 then + motion_result.is_blocked = true + else motion_result.position = motion_result.position + step_vec motion_result.slope_angle = 0.125 - else - motion_result.is_blocked = true end end) end) @@ -1860,12 +1895,27 @@ describe('player_char', function () ) end) + it('(vector(-3.5, 4) at speed -1.5) should return vector(-5, 4), slope before blocked, is_blocked: false, is_falling: false', function () + pc.position = vector(-3.5, 4) + pc.ground_speed = -1.5 + -- we assume _compute_max_pixel_distance is correct, so it should return 2 + + assert.are_equal(collision.ground_motion_result( + vector(-5, 4), + 0.125, + false, + false + ), + pc:_compute_ground_motion_result() + ) + end) + -- bugfix history: + the test revealed that is_blocked should be false when just touching a wall on arrival -- so I added a check to only check a wall on an extra column farther if there are subpixels left in motion it('(vector(4.5, 4) at speed 0.5) should return vector(5, 4), slope before blocked, is_blocked: false, is_falling: false', function () pc.position = vector(4.5, 4) pc.ground_speed = 0.5 - -- we assume _compute_max_pixel_distance is correct, so it should return 2 + -- we assume _compute_max_pixel_distance is correct, so it should return 1 assert.are_equal(collision.ground_motion_result( vector(5, 4), @@ -1877,13 +1927,29 @@ describe('player_char', function () ) end) + -- the negative motion equivalent is not symmetrical due to flooring + it('(vector(-4, 4) at speed -0.1) should return vector(-5, 4), slope before blocked, is_blocked: false, is_falling: false', function () + pc.position = vector(-4, 4) + pc.ground_speed = -1 + -- we assume _compute_max_pixel_distance is correct, so it should return 1 + + assert.are_equal(collision.ground_motion_result( + vector(-5, 4), + 0.125, + false, + false + ), + pc:_compute_ground_motion_result() + ) + end) + -- bugfix history: < replaced self.ground_speed with distance_x in are_subpixels_left evaluation it('(vector(4.5, 4) at speed 1 on slope cos 0.5) should return vector(5, 4), is_blocked: false, is_falling: false', function () -- this is the same as the test above (we just reach the wall edge without being blocked), -- but we make sure that are_subpixels_left check takes the slope factor into account pc.position = vector(4.5, 4) pc.slope_angle = -1/6 -- cos(-pi/3) = 1/2 - pc.ground_speed = 1 + pc.ground_speed = 1 -- * slope cos = -0.5 assert.are_equal(collision.ground_motion_result( vector(5, 4), @@ -1895,6 +1961,23 @@ describe('player_char', function () ) end) + -- the negative motion equivalent is not symmetrical due to flooring + -- in particular, to update the slope angle, we need to change of full pixel + it('(vector(-4, 4) at speed -2 on slope cos 0.5) should return vector(-5, 4), is_blocked: false, is_falling: false', function () + pc.position = vector(-4, 4) + pc.slope_angle = -1/6 -- cos(-pi/3) = 1/2 + pc.ground_speed = -2 -- * slope cos = -1 + + assert.are_equal(collision.ground_motion_result( + vector(-5, 4), + 0.125, -- new slope angle, no relation with initial one + false, + false + ), + pc:_compute_ground_motion_result() + ) + end) + it('(vector(4, 4) at speed 1.5) should return vector(5, 4), slope before blocked, is_blocked: true, is_falling: false', function () pc.position = vector(4, 4) pc.ground_speed = 1.5 @@ -1913,6 +1996,24 @@ describe('player_char', function () ) end) + it('(vector(-4, 4) at speed -1.5) should return vector(-5, 4), slope before blocked, is_blocked: true, is_falling: false', function () + pc.position = vector(-4, 4) + pc.ground_speed = -1.5 + -- we assume _compute_max_pixel_distance is correct, so it should return 1 + -- the character will just touch the wall but because it has some extra subpixels + -- going "into" the wall, we floor them and consider character as blocked + -- (unlike Classic Sonic that would simply ignore subpixels) + + assert.are_equal(collision.ground_motion_result( + vector(-5, 4), + 0.125, + true, + false + ), + pc:_compute_ground_motion_result() + ) + end) + -- bugfix history: -- ?? same reason as test far above where "character has not moved by a full pixel" so slope should not change it('(vector(4, 4) at speed 1.5 on slope cos 0.5) should return vector(4.75, 4), slope before blocked, is_blocked: false, is_falling: false', function () @@ -1931,6 +2032,22 @@ describe('player_char', function () ) end) + it('(vector(-4.1, 4) at speed -1.5 on slope cos 0.5) should return vector(-4.85, 4), slope before blocked, is_blocked: false, is_falling: false', function () + -- start under -4 so we don't change full pixel and preserve slope angle + pc.position = vector(-4.1, 4) + pc.slope_angle = -1/6 -- cos(-pi/3) = 1/2 + pc.ground_speed = -1.5 -- * slope cos = -0.75 + + assert.are_equal(collision.ground_motion_result( + vector(-4.85, 4), + -1/6, -- character has not moved by a full pixel, so visible position and slope remains the same + false, + false + ), + pc:_compute_ground_motion_result() + ) + end) + it('(vector(4, 4) at speed 3 on slope cos 0.5) should return vector(5, 4), slope before blocked, is_blocked: true, is_falling: false', function () pc.position = vector(4, 4) pc.slope_angle = -1/6 -- cos(-pi/3) = 1/2 @@ -1947,9 +2064,25 @@ describe('player_char', function () ) end) - -- bugfix history: it failed until I added the subpixels check at the end of the method - -- (also fixed in v1: subpixel cut when max_column_distance is 0 and blocked on next column) - it('+ (vector(5, 4) at speed 0.5) should return vector(5, 4), slope before moving, is_blocked: false, is_falling: false', function () + it('(vector(-4, 4) at speed 3 on slope cos 0.5) should return vector(-5, 4), slope before blocked, is_blocked: true, is_falling: false', function () + pc.position = vector(-4, 4) + pc.slope_angle = -1/6 -- cos(-pi/3) = 1/2 + pc.ground_speed = -3 -- * slope cos = -1.5 + + assert.are_equal(collision.ground_motion_result( + vector(-5, 4), + 0.125, + true, + false + ), + pc:_compute_ground_motion_result() + ) + end) + + -- bugfix history: + -- + it failed until I added the subpixels check at the end of the method + -- (also fixed in v1: subpixel cut when max_column_distance is 0 and blocked on next column) + it('(vector(5, 4) at speed 0.5) should return vector(5, 4), slope before moving, is_blocked: true, is_falling: false', function () pc.position = vector(5, 4) pc.ground_speed = 0.5 -- we assume _compute_max_pixel_distance is correct, so it should return 0 @@ -1966,7 +2099,24 @@ describe('player_char', function () ) end) - it('(vector(5.5, 4) at speed 0.5) should return vector(5, 4), slope before moving, is_blocked: false, is_falling: false', function () + it('(vector(-5, 4) at speed 0.5) should return vector(-5, 4), slope before moving, is_blocked: true, is_falling: false', function () + pc.position = vector(-5, 4) + pc.ground_speed = -0.5 + -- we assume _compute_max_pixel_distance is correct, so it should return 0 + -- the character is already touching the wall, so any motion, even of just a few subpixels, + -- is considered blocked + + assert.are_equal(collision.ground_motion_result( + vector(-5, 4), + 0, -- character couldn't move at all, so we preserved the initial slope angle + true, + false + ), + pc:_compute_ground_motion_result() + ) + end) + + it('(vector(5.5, 4) at speed 0.5) should return vector(5, 4), slope before moving, is_blocked: true, is_falling: false', function () -- this is possible e.g. if character walked along 1.5 from x=4 -- to reduce computation we didn't check an extra column for a wall -- at that time, but starting next frame we will, effectively clamping @@ -1977,8 +2127,8 @@ describe('player_char', function () -- but we will be blocked by the wall anyway assert.are_equal(collision.ground_motion_result( - vector(5, 4), - 0, -- character couldn't move at all, so we preserved the initial slope angle + vector(5, 4), -- this works on the *right* thanks to subpixel cut working "inside" a wall + 0, -- character couldn't move and went back, so we preserved the initial slope angle true, false ), @@ -1986,6 +2136,38 @@ describe('player_char', function () ) end) + it('(vector(-5.5, 4) at speed -0.5) should return vector(-6, 4), slope before moving, is_blocked: false, is_falling: false', function () + pc.position = vector(-5.5, 4) + pc.ground_speed = -0.5 + -- we assume _compute_max_pixel_distance is correct, so it should return 1 + -- but we will be blocked by the wall anyway + + assert.are_equal(collision.ground_motion_result( + vector(-6, 4), -- we are already inside the wall, floored to -6 + 0, -- character only snap to floored x, so we preserved the slope angle + false, -- no wall detected from inside! + false + ), + pc:_compute_ground_motion_result() + ) + end) + + it('(vector(-5.5, 4) at speed -1) should return vector(-6, 4), slope before moving, is_blocked: true, is_falling: false', function () + pc.position = vector(-5.5, 4) + pc.ground_speed = -1 + -- we assume _compute_max_pixel_distance is correct, so it should return 1 + -- but we will be blocked by the wall anyway + + assert.are_equal(collision.ground_motion_result( + vector(-6, 4), -- we are already inside the wall, floored to -6 + 0, -- character only snap to floored x, so we preserved the slope angle + true, -- wall detected from inside if moving 1 full pixel toward the next column on the left + false + ), + pc:_compute_ground_motion_result() + ) + end) + it('(vector(3, 4) at speed 3) should return vector(5, 4), slope before blocked, is_blocked: false, is_falling: false', function () pc.position = vector(3, 4) pc.ground_speed = 3.5 @@ -2002,6 +2184,22 @@ describe('player_char', function () ) end) + it('(vector(-3, 4) at speed -3) should return vector(-5, 4), slope before blocked, is_blocked: false, is_falling: false', function () + pc.position = vector(-3, 4) + pc.ground_speed = -3.5 + -- we assume _compute_max_pixel_distance is correct, so it should return 3 + -- but because of the blocking, we stop at x=5 instead of 6.5 + + assert.are_equal(collision.ground_motion_result( + vector(-5, 4), + 0.125, + true, + false + ), + pc:_compute_ground_motion_result() + ) + end) + end) -- bugfix history: the mock was wrong (was using updated position instead of original_position) @@ -3006,9 +3204,57 @@ describe('player_char', function () next_air_step_mock:clear() end) - it('(vector(0.5, 10) at speed 0.5 along x) should move to vector(1, 10) without blocking', function () + -- bugfix history: + -- = the itest 'platformer air wall block' showed that the subpixel check + -- was using the integer max_pixel_distance instead of the float velocity[coord] + -- and this revealed a bug of no motion on x at all when velocity.x is < 1 and x starts integer + it('(vector(0, 10) at speed 0.5 along x) should move to vector(0.7, 10) without being blocked', function () + local motion_result = collision.air_motion_result( + vector(0, 10), + false, + false, + false, + nil + ) + + -- we assume _compute_max_pixel_distance is correct + pc:_advance_in_air_along(motion_result, vector(0.5, 99), "x") + + assert.are_equal(collision.air_motion_result( + vector(0.5, 10), + false, + false, + false, + nil + ), motion_result + ) + end) + + it('(vector(0.2, 10) at speed 0.5 along x) should move to vector(0.7, 10) without being blocked', function () + local motion_result = collision.air_motion_result( + vector(0.2, 10), + false, + false, + false, + nil + ) + + -- we assume _compute_max_pixel_distance is correct + pc:_advance_in_air_along(motion_result, vector(0.5, 99), "x") + + assert.are_equal(collision.air_motion_result( + vector(0.7, 10), + false, + false, + false, + nil + ), motion_result + ) + end) + + it('(vector(0.5, 10) at speed 0.5 along x) should move to vector(1, 10) without being blocked', function () local motion_result = collision.air_motion_result( - vector(1, 10), + vector(0.5, 10), false, false, false, @@ -3072,7 +3318,7 @@ describe('player_char', function () ) end) - it('(vector(2.5, 7.3) at speed -4.4 along y) should move to vector(2.5, 2.9)', function () + it('(vector(2.5, 7.3) at speed -4.4 along y) should move to vector(2.5, 2.9) without being blocked', function () local motion_result = collision.air_motion_result( vector(2.5, 7.3), false, From 063f4c00affe76880140820becb881b8cb98f775 Mon Sep 17 00:00:00 2001 From: hsandt Date: Sat, 13 Apr 2019 00:03:38 +0100 Subject: [PATCH 070/140] [TEST] Added trace log on starting itest --- src/engine/test/integrationtest.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/engine/test/integrationtest.lua b/src/engine/test/integrationtest.lua index 0bee63f3..ab1d7180 100644 --- a/src/engine/test/integrationtest.lua +++ b/src/engine/test/integrationtest.lua @@ -176,6 +176,9 @@ function integration_test_runner:start(test) self:_initialize() end + -- log after _initialize which sets up the logger + log("starting itest: "..test.name, "trace") + if self.current_test then self:stop() end From cbe442d455c43d0e25511a5f8a39bd2d7d6d682f Mon Sep 17 00:00:00 2001 From: hsandt Date: Sat, 13 Apr 2019 00:04:12 +0100 Subject: [PATCH 071/140] [BUILD] Prebuild: added missing gp_value_types.pc_slope substitute --- prebuild/replace_strings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/prebuild/replace_strings.py b/prebuild/replace_strings.py index b53ee845..40c2a643 100755 --- a/prebuild/replace_strings.py +++ b/prebuild/replace_strings.py @@ -154,6 +154,7 @@ 'pc_velocity': 2, 'pc_ground_spd': 3, 'pc_motion_state': 4, + 'pc_slope': 5, }, } From 345acfe654f731494e643307f398c0624d92437f Mon Sep 17 00:00:00 2001 From: hsandt Date: Sat, 13 Apr 2019 01:07:45 +0100 Subject: [PATCH 072/140] [BUILD] Postbuild: added warning if minification gets over 65536 characters --- postbuild/minify.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/postbuild/minify.py b/postbuild/minify.py index 16941ebe..17486472 100755 --- a/postbuild/minify.py +++ b/postbuild/minify.py @@ -69,6 +69,8 @@ def minify_lua_in_p8(cartridge_filepath): min_lua_file.seek(0) min_char_count = sum(len(line) for line in min_lua_file) print(f"Minified lua code to {min_char_count} characters") + if min_char_count > 65536: + logging.warn(f"Maximum character count of 65536 has been exceeded, cartridge will be truncated in PICO-8") # Step 4-6: inject minified lua code phase = Phase.CARTRIDGE_HEADER From 56dec444d929d82077feaa258f1aa8613ed950fa Mon Sep 17 00:00:00 2001 From: hsandt Date: Sat, 13 Apr 2019 01:10:07 +0100 Subject: [PATCH 073/140] [TEST] DSL: Switched to gameplay value arguments by string instead of enum to fix DSL itest failing in builds without assert symbol due to gp_value string table being stripped. --- src/engine/test/itest_dsl.lua | 36 +++++++++++++++-------------------- tests/utestitest_dsl.lua | 36 +++++++++++++++++------------------ 2 files changed, 33 insertions(+), 39 deletions(-) diff --git a/src/engine/test/itest_dsl.lua b/src/engine/test/itest_dsl.lua index 95b3f786..d423b853 100644 --- a/src/engine/test/itest_dsl.lua +++ b/src/engine/test/itest_dsl.lua @@ -101,7 +101,7 @@ parsable_type_strings = invert_table(parsable_types) -- type of commands available command_types = enum { "warp", -- warp player character bottom args: {bottom_position: vector} - "set", -- set gameplay value args: {gp_value_type: gp_value_types, new_value_args...: matching gp value parsable type} + "set", -- set gameplay value args: {gp_value_type_str: string, new_value_args...: matching gp value parsable type} "set_control_mode", -- set control mode args: {control_mode_str: control_modes key} "set_motion_mode", -- set motion mode args: {motion_mode_str: motion_modes key} "move", -- set sticky pc move intention args: {move_dir_str: horizontal_dirs key} @@ -145,9 +145,6 @@ gp_value_types = enum { "pc_slope", -- current slope on which player character is grounded } ---#if assert -gp_value_type_strings = invert_table(gp_value_types) ---#endif -- data for each gameplay value type local gp_value_data_t = { @@ -230,7 +227,7 @@ function itest_dsl.parse_gp_value(arg_strings) local gp_value_parser = value_parsers[gp_value_data.parsable_type] assert(gp_value_parser, "no value parser defined for gp value type '"..parsable_type_strings[gp_value_data.parsable_type].."'") local gp_value = gp_value_parser(gp_value_comps) - return gp_value_type, gp_value + return gp_value_type_str, gp_value end -- table of parsers for command args and gameplay values, indexed by parsed type @@ -246,14 +243,10 @@ function itest_dsl.execute_warp(args) end function itest_dsl.execute_set(args) - local gp_value_type = args[1] - local new_gp_value = args[2] - - -- if you remove *all* generate_function_table, it's worth having parse_gp_value - -- return a gp_value_type_str rather than an index to avoid going back and forth - -- between key and value - local setter = itest_dsl["set_"..gp_value_type_strings[gp_value_type]] - assert(setter, "itest_dsl.set_"..gp_value_type_strings[gp_value_type].." is not defined") + local gp_value_type_str, new_gp_value = unpack(args) + + local setter = itest_dsl["set_"..gp_value_type_str] + assert(setter, "itest_dsl.set_"..gp_value_type_str.." is not defined") setter(new_gp_value) end @@ -354,13 +347,13 @@ end -- expectation struct -- attributes --- gp_value_type gp_value_types type of gameplay value to compare --- expected_value {type used for gp_value_type} expected gameplay value +-- gp_value_type_str string name of gameplay value to compare +-- expected_value {type used for gp_value_type_str} expected gameplay value local expectation = new_struct() itest_dsl.expectation = expectation -function expectation:_init(gp_value_type, expected_value) - self.gp_value_type = gp_value_type +function expectation:_init(gp_value_type_str, expected_value) + self.gp_value_type_str = gp_value_type_str self.expected_value = expected_value end @@ -643,8 +636,9 @@ function itest_dsl_parser:_define_final_assertion() -- check each expectation one by one for exp in all(final_expectations_proxy) do - local evaluator = evaluators[exp.gp_value_type] - assert(evaluator, "evaluators["..exp.gp_value_type.."] (for '"..gp_value_type_strings[exp.gp_value_type].."') is not defined") + local gp_value_type = gp_value_types[exp.gp_value_type_str] + local evaluator = evaluators[gp_value_type] + assert(evaluator, "evaluators["..gp_value_type.."] (for '"..exp.gp_value_type_str.."') is not defined") local gp_value = evaluator() --[[#pico8 -- in pico8, we use fixed point precision, which is what we expect as final values @@ -661,8 +655,8 @@ function itest_dsl_parser:_define_final_assertion() --#endif if not value_success then success = false - local gp_value_data = gp_value_data_t[exp.gp_value_type] - assert(gp_value_data, "gp_value_data_t["..exp.gp_value_type.."] is not defined") + local gp_value_data = gp_value_data_t[gp_value_type] + assert(gp_value_data, "gp_value_data_t["..gp_value_type.."] is not defined") local gp_value_name = gp_value_data.name local value_message = "\nFor gameplay value '"..gp_value_name.."':\n"..value_eq_message full_message = full_message..value_message.."\n" diff --git a/tests/utestitest_dsl.lua b/tests/utestitest_dsl.lua index 72c0604c..507ae854 100644 --- a/tests/utestitest_dsl.lua +++ b/tests/utestitest_dsl.lua @@ -173,8 +173,8 @@ describe('itest_dsl', function () end, "parse_gp_value: got 1 args, expected at least 2") end) - it('should return the gameplay value type and the expected value, itself recursively parsed', function () - assert.are_same({gp_value_types.pc_bottom_pos, vector(1, 3)}, + it('should return the gameplay value type string and the expected value, itself recursively parsed', function () + assert.are_same({"pc_bottom_pos", vector(1, 3)}, {itest_dsl.parse_gp_value({"pc_bottom_pos", "1", "3"})}) end) @@ -215,14 +215,14 @@ describe('itest_dsl', function () describe('"execute_set', function () - it('should set pc velocity to (1, -3)', function () - itest_dsl.execute_set({gp_value_types.pc_velocity, vector(1, -3)}) + it('#solo should set pc velocity to (1, -3)', function () + itest_dsl.execute_set({"pc_velocity", vector(1, -3)}) assert.are_equal(vector(1, -3), stage.state.player_char.velocity) end) - it('should fail with unsupported gp_value_type for setting', function () + it('#solo should fail with unsupported gp_value_type for setting', function () assert.has_error(function () - itest_dsl.execute_set({gp_value_types.pc_slope, -2}) + itest_dsl.execute_set({"pc_slope", -2}) end, "itest_dsl.set_pc_slope is not defined") end) @@ -416,9 +416,9 @@ describe('itest_dsl', function () describe('_init', function () it('should create a new dsl itest', function () - local exp = expectation(gp_value_types.pc_bottom_pos, 24) + local exp = expectation("pc_bottom_pos", 24) assert.is_not_nil(exp) - assert.are_same({gp_value_types.pc_bottom_pos, 24}, {exp.gp_value_type, exp.expected_value}) + assert.are_same({"pc_bottom_pos", 24}, {exp.gp_value_type_str, exp.expected_value}) end) end) @@ -767,8 +767,8 @@ expect command(command_types.wait, { 1 } ), command(command_types.move, { horizontal_dirs.left } ), command(command_types.wait, { 2 } ), - command(command_types.expect, {gp_value_types.pc_bottom_pos, vector(10, 45)}), - command(command_types.expect, {gp_value_types.pc_velocity, vector(2, -3.5)}), + command(command_types.expect, {"pc_bottom_pos", vector(10, 45)}), + command(command_types.expect, {"pc_velocity", vector(2, -3.5)}), }, commands) end) @@ -802,8 +802,8 @@ expect command(command_types.wait, { 1 } ), command(command_types.move, { horizontal_dirs.left } ), command(command_types.wait, { 2 } ), - command(command_types.expect, {gp_value_types.pc_bottom_pos, vector(10, 45)}), - command(command_types.expect, {gp_value_types.pc_velocity, vector(2, -3.5)}), + command(command_types.expect, {"pc_bottom_pos", vector(10, 45)}), + command(command_types.expect, {"pc_velocity", vector(2, -3.5)}), } local test = itest_dsl_parser.create_itest("test 1", dsli) @@ -1001,19 +1001,19 @@ expect itest_dsl_parser._itest = integration_test('test', {}) end) - it('should set the final assertion as returning true, message when the gameplay value is expected', function () + it('#solo should set the final assertion as returning true, message when the gameplay value is expected', function () itest_dsl_parser._final_expectations = { - expectation(gp_value_types.pc_bottom_pos, vector(27, 30)), - expectation(gp_value_types.pc_velocity, vector(-3, 2.5)) + expectation("pc_bottom_pos", vector(27, 30)), + expectation("pc_velocity", vector(-3, 2.5)) } itest_dsl_parser:_define_final_assertion() assert.are_same({true, ""}, {itest_dsl_parser._itest.final_assertion()}) end) - it('should set the final assertion as returning false, message when the gameplay value is not expected', function () + it('#solo should set the final assertion as returning false, message when the gameplay value is not expected', function () itest_dsl_parser._final_expectations = { - expectation(gp_value_types.pc_bottom_pos, vector(27, 30)), -- ok - expectation(gp_value_types.pc_velocity, vector(-3, 7.5)) -- different from actual + expectation("pc_bottom_pos", vector(27, 30)), -- ok + expectation("pc_velocity", vector(-3, 7.5)) -- different from actual } itest_dsl_parser:_define_final_assertion() local expected_message = "\nFor gameplay value 'player character velocity':\nExpected objects to be almost equal with eps: 0.015625.\n".. From 2e69ead46e3ec6b45825ca23a68a8b37c1c5906f Mon Sep 17 00:00:00 2001 From: hsandt Date: Sat, 13 Apr 2019 01:32:43 +0100 Subject: [PATCH 074/140] [BUILD] Prebuild: fixed itest on debug motion by always enabling cheats in itests --- prebuild/preprocess.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/prebuild/preprocess.py b/prebuild/preprocess.py index 5b27e065..c1834b0c 100644 --- a/prebuild/preprocess.py +++ b/prebuild/preprocess.py @@ -27,7 +27,9 @@ 'debug': ['pico8', 'assert', 'log', 'visual_logger', 'tuner', 'profiler', 'mouse', 'cheat'], 'assert': ['pico8', 'assert', 'log', 'visual_logger'], 'itest': ['pico8', 'assert', 'log', 'itest', 'cheat'], - 'itest_light': ['pico8', 'log', 'itest'], + # 'cheat' added as quick fix for itest that must test debug motion, while still stripping cheat input + # when #if or is implemented, remove 'cheat' and add "or itest" in front of "#if cheat" that need it + 'itest_light': ['pico8', 'log', 'itest', 'cheat'], 'profiler': ['pico8', 'log', 'visual_logger', 'profiler'], 'visual_log': ['pico8', 'log', 'visual_logger'], 'pico8_utest': ['pico8', 'assert', 'log'], From 6fb9b966a0ea6495edc4043525f01293c5e47510 Mon Sep 17 00:00:00 2001 From: hsandt Date: Sun, 14 Apr 2019 21:19:28 +0100 Subject: [PATCH 075/140] [LOG] Added file_log_stream --- src/engine/debug/logging.lua | 18 +++++++++++ src/game/application/gameapp.lua | 6 ++++ src/game/main.lua | 1 + tests/utestlogging.lua | 54 ++++++++++++++++++++++++++++---- 4 files changed, 73 insertions(+), 6 deletions(-) diff --git a/src/engine/debug/logging.lua b/src/engine/debug/logging.lua index b971c076..12318be4 100644 --- a/src/engine/debug/logging.lua +++ b/src/engine/debug/logging.lua @@ -61,6 +61,8 @@ end -- function log_stream:on_log() -- end + +-- console log console_log_stream = derived_singleton(log_stream) logging.console_log_stream = console_log_stream @@ -68,6 +70,19 @@ function console_log_stream:on_log(lm) printh(logging.compound_message(lm)) end + +-- file log +file_log_stream = derived_singleton(log_stream, function (self) + self.file_prefix = "game" -- override this to distinguish logs between games and versions +end) +logging.file_log_stream = file_log_stream + +function file_log_stream:on_log(lm) + -- pico8 will add .p8l extension + printh(logging.compound_message(lm), self.file_prefix.."_log") +end + + local logger = singleton(function (self) self.active_categories = { default = true, @@ -103,6 +118,9 @@ function logger:register_stream(stream) add(self._streams, stream) end +-- level logging.level +-- category str +-- content str function logger:_generic_log(level, category, content) category = category or "default" if logger.active_categories[category] and logger.current_level <= level then diff --git a/src/game/application/gameapp.lua b/src/game/application/gameapp.lua index 8ef225c2..ae09f5b8 100644 --- a/src/game/application/gameapp.lua +++ b/src/game/application/gameapp.lua @@ -4,6 +4,12 @@ local gamestate_proxy = require("game/application/gamestate_proxy") local gamestate = require("game/application/gamestate") local visual = require("game/resources/visual") +--#if log +local logging = require("engine/debug/logging") +-- pico8 doesn't support output file path containing "-" so use "_" +logging.file_log_stream.file_prefix = "sonic_pico8_v2.2" +--#endif + --#if visual_logger local vlogger = require("engine/debug/visual_logger") --#endif diff --git a/src/game/main.lua b/src/game/main.lua index a2bed560..0dae40a7 100644 --- a/src/game/main.lua +++ b/src/game/main.lua @@ -4,6 +4,7 @@ local gamestate_proxy = require("game/application/gamestate_proxy") --#if log local logging = require("engine/debug/logging") logging.logger:register_stream(logging.console_log_stream) +logging.logger:register_stream(logging.file_log_stream) --#if visual_logger local vlogger = require("engine/debug/visual_logger") diff --git a/tests/utestlogging.lua b/tests/utestlogging.lua index 76eca1e0..ac5fa26a 100644 --- a/tests/utestlogging.lua +++ b/tests/utestlogging.lua @@ -7,8 +7,8 @@ logging.logger:init() describe('logging', function () - local log_msg = logging.log_msg - local log_stream = logging.log_stream + local log_msg, log_stream, file_log_stream = get_members(logging, + "log_msg", "log_stream", "file_log_stream") describe('log_msg', function () @@ -121,6 +121,49 @@ describe('logging', function () end) + -- for file logging, our tests are low-level and just check that on_log + -- is calling printh on the compounded message + describe('file_log_stream', function () + + local printh_stub + + setup(function () + printh_stub = stub(_G, "printh") + end) + + teardown(function () + printh_stub:revert() + end) + + before_each(function () + logger:register_stream(file_log_stream) + end) + + after_each(function () + file_log_stream:init() + printh_stub:clear() + end) + + describe('derived_init', function () + it('should set file_prefix to "game"', function () + assert.are_equal("game", file_log_stream.file_prefix) + end) + end) + + describe('on_log', function () + it('should call printh with compounded message and target file "{self.file_prefix}_log.txt"', function () + file_log_stream.file_prefix = "my_game" + + local lm = log_msg(logging.level.info, "default", "dummy") + file_log_stream:on_log(lm) + + assert.spy(printh_stub).was_called(1) + assert.spy(printh_stub).was_called_with(logging.compound_message(lm), "my_game_log") + end) + end) + + end) + describe('deactivate_all_categories', function () it('should set all active categories flags to false', function () @@ -259,24 +302,23 @@ describe('logging', function () end) + -- for console logging, our tests are high-level + -- and contain checking that compound_message is doing its job describe('console logging', function () local printh_stub setup(function () - -- important since busted_helper will set it to false - logging.console_log_stream.active = true printh_stub = stub(_G, "printh") end) teardown(function () - logging.console_log_stream.active = false printh_stub:revert() end) before_each(function () logger.active_categories.flow = true - logger:register_stream(logging.console_log_stream) + logger:register_stream(console_log_stream) end) after_each(function () From 06f80d3bcce722d79865cbb625923b96a950e9e0 Mon Sep 17 00:00:00 2001 From: hsandt Date: Sun, 14 Apr 2019 22:38:20 +0100 Subject: [PATCH 076/140] [LOG] Clear log file on main game start --- src/engine/debug/logging.lua | 6 ++++++ src/game/main.lua | 3 +++ tests/pico8api.lua | 2 +- tests/utestclass.lua | 2 +- tests/utestlogging.lua | 11 +++++++++++ 5 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/engine/debug/logging.lua b/src/engine/debug/logging.lua index 12318be4..b946198c 100644 --- a/src/engine/debug/logging.lua +++ b/src/engine/debug/logging.lua @@ -77,6 +77,12 @@ file_log_stream = derived_singleton(log_stream, function (self) end) logging.file_log_stream = file_log_stream +function file_log_stream:clear() + -- clear file by printing nothing while overwriting content + -- note: this will print an empty line at the beginning of the file + printh("", self.file_prefix.."_log", true) +end + function file_log_stream:on_log(lm) -- pico8 will add .p8l extension printh(logging.compound_message(lm), self.file_prefix.."_log") diff --git a/src/game/main.lua b/src/game/main.lua index 0dae40a7..56b701e2 100644 --- a/src/game/main.lua +++ b/src/game/main.lua @@ -33,6 +33,9 @@ input:toggle_mouse(true) -- pico-8 functions must be placed at the end to be parsed by p8tool function _init() + -- clear log file on new game session + logging.file_log_stream:clear() + -- require all gamestate modules, according to preprocessing step gamestate_proxy:require_gamestates() gameapp.init() diff --git a/tests/pico8api.lua b/tests/pico8api.lua index 18018528..ced0a1f1 100644 --- a/tests/pico8api.lua +++ b/tests/pico8api.lua @@ -626,7 +626,7 @@ function del(a, dv) end end --- printh must not refer to the native print directly +-- printh function must not refer to the native print directly (no printh = print) -- because params are different and to avoid spying on -- the wrong calls (busted -o TAP may print natively) function printh(str, filename, overwrite) diff --git a/tests/utestclass.lua b/tests/utestclass.lua index d56fa530..a8f78613 100644 --- a/tests/utestclass.lua +++ b/tests/utestclass.lua @@ -414,7 +414,7 @@ describe('derived_singleton', function () assert.are_equal("custom", my_derived_singleton.types[1]) end) - it('should define a derived_singleton with derived members', function () + it('should define a derived_singleton with derived members using derived_init', function () assert.are_equal("special", my_derived_singleton.subtype) end) diff --git a/tests/utestlogging.lua b/tests/utestlogging.lua index ac5fa26a..6f7f1bb7 100644 --- a/tests/utestlogging.lua +++ b/tests/utestlogging.lua @@ -150,6 +150,17 @@ describe('logging', function () end) end) + describe('clear', function () + it('should call printh with empty message and overwrite mode', function () + file_log_stream.file_prefix = "my_game" + + file_log_stream:clear() + + assert.spy(printh_stub).was_called(1) + assert.spy(printh_stub).was_called_with("", "my_game_log", true) + end) + end) + describe('on_log', function () it('should call printh with compounded message and target file "{self.file_prefix}_log.txt"', function () file_log_stream.file_prefix = "my_game" From 439272bbf96c0681abba296cd1be990f0fecfc73 Mon Sep 17 00:00:00 2001 From: hsandt Date: Sun, 14 Apr 2019 23:09:08 +0100 Subject: [PATCH 077/140] [POSTBUILD] If substitute is not found, insert an assert expression --- prebuild/replace_strings.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/prebuild/replace_strings.py b/prebuild/replace_strings.py index 40c2a643..1d42dc91 100755 --- a/prebuild/replace_strings.py +++ b/prebuild/replace_strings.py @@ -231,8 +231,9 @@ def get_substitute(match): else: original_symbol = match.group(0) # "{namespace}.{member}" # in general, we should substitute all members of a namespace, especially enums - logging.warning(f'no substitute defined for {original_symbol}, but the namespace is present in SYMBOL_SUBSTITUTE_TABLE') - return original_symbol + logging.error(f'no substitute defined for {original_symbol}, but the namespace (first part) is present in SYMBOL_SUBSTITUTE_TABLE') + # return something easy to debug in PICO-8, in case the user missed the error message + return f'assert(false, "UNSUBSTITUTED {original_symbol}")' return get_substitute def replace_all_symbols_in_string(text): From 761920eeea8389d8ee06f0d7d73a2a6b9333f450 Mon Sep 17 00:00:00 2001 From: hsandt Date: Sun, 14 Apr 2019 23:10:10 +0100 Subject: [PATCH 078/140] [DEBUG] Always require Code Tuner in main, but use default value if `tuner` not defined in build config Make sure busted always uses the real Code Tuner --- prebuild/preprocess.py | 5 ++++- src/engine/debug/codetuner.lua | 28 ++++++++++++++++++++++++---- src/game/main.lua | 4 +++- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/prebuild/preprocess.py b/prebuild/preprocess.py index c1834b0c..3bc977c8 100644 --- a/prebuild/preprocess.py +++ b/prebuild/preprocess.py @@ -12,7 +12,10 @@ # 3. it will strip debug function calls like log() or assert() in configs that don't use those symbols # Config for defined symbols (all configs have pico8, to distinguish from busted using the scripts directly) -# Remember that busted will not preprocess at all and will therefore go through all the blocks. +# Remember that busted will not preprocess at all and will therefore go through all the blocks, +# even blocks surrounded by #if symbol and #ifn symbol for the same symbol! +# In this case, we recomment surrounding the block that is never used by busted with --[[#pico8 and --#pico8]] + # For non-pico8 builds, we use --#ifn pico8 to indicate we won't have preprocessing, # but for busted unit tests we prefer using --#if utest (which is never defined) to make clear that # the code is only needed for a purpose of redundancy and unit test harnessing in general. diff --git a/src/engine/debug/codetuner.lua b/src/engine/debug/codetuner.lua index 31a3f99d..3e5a9d11 100644 --- a/src/engine/debug/codetuner.lua +++ b/src/engine/debug/codetuner.lua @@ -1,12 +1,16 @@ --#if tuner ---#if log -local logging = require("engine/debug/logging") ---#endif require("engine/core/class") require("engine/render/color") local wtk = require("engine/wtk/pico8wtk") +-- code tuner: a debug utility that allows to tune +-- any value in code by using a small widget on screen +-- usage: +-- where you need to test different numerical values in your code, +-- use `tuned("my var", default_value)` instead of `default_value` +-- then, in game, in a build config that defines `tuner` symbol, +-- use the number selection widget for entry "my var" to tune it local codetuner = singleton(function (self) -- parameters @@ -135,6 +139,22 @@ end -- at any time, even if the window is not shown codetuner:init_window() -return codetuner +--#endif + +-- prevent busted from parsing both versions of codetuner +--[[#pico8 + +--#ifn tuner + +local codetuner = {} + +-- if tuner is disabled, use default value +function tuned(name, default_value) + return default_value +end --#endif + +--#pico8]] + +return codetuner diff --git a/src/game/main.lua b/src/game/main.lua index 56b701e2..dc55de45 100644 --- a/src/game/main.lua +++ b/src/game/main.lua @@ -5,6 +5,7 @@ local gamestate_proxy = require("game/application/gamestate_proxy") local logging = require("engine/debug/logging") logging.logger:register_stream(logging.console_log_stream) logging.logger:register_stream(logging.file_log_stream) +logging.logger.active_categories["trace"] = true --#if visual_logger local vlogger = require("engine/debug/visual_logger") @@ -19,8 +20,9 @@ local profiler = require("engine/debug/profiler") profiler.window:show() --#endif ---#if tuner +-- always require code tuner, since ifn tuned, `tuned` will simply use the default value local codetuner = require("engine/debug/codetuner") +--#if tuner codetuner:show() codetuner.active = true --#endif From fb011e0382ab52cc6aa5574d3fed8713956a25a6 Mon Sep 17 00:00:00 2001 From: hsandt Date: Mon, 15 Apr 2019 23:37:37 +0100 Subject: [PATCH 079/140] [LOG] Log from utests too, in log directory with .txt extension --- .gitignore | 3 + tests/bustedhelper.lua | 1 + tests/pico8api.lua | 33 +++++++- tests/utestpico8api.lua | 169 +++++++++++++++++++++++++++++++--------- 4 files changed, 166 insertions(+), 40 deletions(-) diff --git a/.gitignore b/.gitignore index 784d17e8..6be425f3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ /intermediate /build +# Log +/log + # Backup /backup diff --git a/tests/bustedhelper.lua b/tests/bustedhelper.lua index edba0a01..280e8c31 100644 --- a/tests/bustedhelper.lua +++ b/tests/bustedhelper.lua @@ -5,6 +5,7 @@ require("engine/test/assertions") -- mute all messages during tests, unless told otherwise during the tests local logging = require("engine/debug/logging") logging.logger:register_stream(logging.console_log_stream) +logging.logger:register_stream(logging.file_log_stream) logging.logger:deactivate_all_categories() -- headless itests will restore "itest" and sometimes "trace" -- return the current file line in the format "{file}:{line}" to make it easy to navigate there from the printed message diff --git a/tests/pico8api.lua b/tests/pico8api.lua index ced0a1f1..4298046c 100644 --- a/tests/pico8api.lua +++ b/tests/pico8api.lua @@ -627,11 +627,36 @@ function del(a, dv) end -- printh function must not refer to the native print directly (no printh = print) --- because params are different and to avoid spying on --- the wrong calls (busted -o TAP may print natively) -function printh(str, filename, overwrite) +-- because params are different and to avoid spying on +-- the wrong calls (busted -o TAP may print natively) +-- exceptionally, we add a custom parameter `log_dirname` +-- to make it easier to test this function itself in busted +function printh(str, file_basename, overwrite, log_dirname) + if not log_dirname then + log_dirname = "log" + end + -- file writing is not supported in tests - print(str) + if file_basename then + -- if log directory doesn't exist, create it + local log_dir_attr = lfs.attributes(log_dirname) + if not log_dir_attr then + lfs.mkdir(log_dirname) + else + assert(log_dir_attr.mode == "directory", "'"..log_dirname.."' is not a directory but a "..log_dir_attr.mode) + end + + local mode = overwrite and "w" or "a" + -- when running in busted, put the logs in a log folder + -- and add the .txt extension (instead of .p8l) + -- for better organization + local log_filepath = log_dirname.."/"..file_basename..".txt" + local file = io.open(log_filepath, mode) + file:write(str, "\n") + file:close() + else + print(str) + end end api = {} diff --git a/tests/utestpico8api.lua b/tests/utestpico8api.lua index 08e2b272..c972944e 100644 --- a/tests/utestpico8api.lua +++ b/tests/utestpico8api.lua @@ -1040,57 +1040,154 @@ describe('pico8api', function () describe('printh', function () - local native_print_stub + -- caution: this will hide *all* native prints, including debug logs + -- so we only do this for the utests that really need it + describe('(stubbing print)', function () - setup(function () - native_print_stub = stub(_G, "print") -- native print - end) + local native_print_stub - teardown(function () - native_print_stub:revert() - end) + setup(function () + native_print_stub = stub(_G, "print") -- native print + end) - after_each(function () - native_print_stub:clear() - end) + teardown(function () + native_print_stub:revert() + end) - it('should print the current file:line with a message', function () - print_at_line("text") - end) + after_each(function () + native_print_stub:clear() + end) + + it('should call native print', function () + printh("hello") + + assert.spy(native_print_stub).was_called(1) + assert.spy(native_print_stub).was_called_with("hello") + end) - it('should call native print', function () - printh("hello") - assert.spy(native_print_stub).was_called(1) - assert.spy(native_print_stub).was_called_with("hello") end) - end) + describe('(with temp file', function () + -- in general we should use os.tmpname, but because of the fact + -- that printh prints to a log folder, we prefer using a custom path + -- make sure to use a temp dir name that is not an actual folder in the project + local temp_dirname = "_temp" + local temp_file_basename = "temp" + local temp_filepath = temp_dirname.."/"..temp_file_basename..".txt" + local temp_file = nil + local function is_dir(dirpath) + local attr = lfs.attributes(dirpath) + return attr and attr.mode == "directory" + end - describe('printh', function () + -- https://stackoverflow.com/questions/37835565/lua-delete-non-empty-directory + local function remove_dir_recursive(dirpath) + for file in lfs.dir(dirpath) do + local file_path = dirpath..'/'..file + if file ~= "." and file ~= ".." then + if lfs.attributes(file_path, 'mode') == 'file' then + os.remove(file_path) + elseif lfs.attributes(file_path, 'mode') == 'directory' then + remove_dir_recursive(file_path) + end + end + end + lfs.rmdir(dirpath) + end - local native_print_stub + local function remove_if_exists(path) + local attr = lfs.attributes(path) + if attr then + if attr.mode == "directory" then + remove_dir_recursive(path) + else + os.remove(path) + end + end + end - setup(function () - native_print_stub = stub(_G, "print") -- native print - end) + local function remove_file_if_exists(filepath) + local f = io.open(temp_filepath) + if f then + f:close() + os.remove(temp_filepath) + end + end - teardown(function () - native_print_stub:revert() - end) + local function get_lines(file) + local lines = {} + for line in file:lines() do + add(lines, line) + end + return lines + end - after_each(function () - native_print_stub:clear() - end) + before_each(function () + remove_if_exists(temp_dirname) + end) - it('should print the current file:line with a message', function () - print_at_line("text") - end) + after_each(function () + if temp_file then + -- an error occurred (maybe the assert failed) + -- and the temp file wasn't closed and set to nil + print("WARNING: emergency close needed, the last write operation likely failed") + temp_file:close() + end + + remove_if_exists(temp_dirname) + end) + + it('should create log directory if it doesn\'t exist', function () + printh("hello", temp_file_basename, true, temp_dirname) + + assert.is_true(is_dir(temp_dirname)) + end) + + it('should assert if a non-directory "log" already exists', function () + local f,error = io.open(temp_dirname, "w") + f:close() + + assert.has_error(function () + printh("hello", temp_file_basename, true, temp_dirname) + end, "'_temp' is not a directory but a file") + end) + + it('should overwrite a file with filepath and true', function () + printh("hello", temp_file_basename, true, temp_dirname) + + temp_file = io.open(temp_filepath) + assert.is_not_nil(temp_file) + assert.are_same({"hello"}, get_lines(temp_file)) + temp_file = nil + end) + + it('should append to a file with filepath and false', function () + lfs.mkdir(temp_dirname) + temp_file = io.open(temp_filepath, "w") + temp_file:write("hello1\n") + temp_file:close() + temp_file = nil + + printh("hello2", temp_file_basename, false, temp_dirname) + + temp_file = io.open(temp_filepath) + assert.is_not_nil(temp_file) + assert.are_same({"hello1", "hello2"}, get_lines(temp_file)) + temp_file = nil + end) + + it('should append to a file with filepath and false, adding newline at the end', function () + printh("hello1", temp_file_basename, false, temp_dirname) + printh("hello2", temp_file_basename, false, temp_dirname) + printh("hello3", temp_file_basename, false, temp_dirname) + + temp_file = io.open(temp_filepath) + assert.is_not_nil(temp_file) + assert.are_same({"hello1", "hello2", "hello3"}, get_lines(temp_file)) + temp_file = nil + end) - it('should call native print', function () - printh("hello") - assert.spy(native_print_stub).was_called(1) - assert.spy(native_print_stub).was_called_with("hello") end) end) From 26181bac50703204ceca4daef1837c64575784d3 Mon Sep 17 00:00:00 2001 From: hsandt Date: Tue, 16 Apr 2019 00:13:39 +0100 Subject: [PATCH 080/140] [TEST] itest: accept no current itest running but show corresponding message Fixed old utests using setup instead of before_each, adding new utests --- src/engine/test/integrationtest.lua | 11 +++++--- tests/utestintegrationtest.lua | 40 +++++++++++++++-------------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/src/engine/test/integrationtest.lua b/src/engine/test/integrationtest.lua index ab1d7180..414857a5 100644 --- a/src/engine/test/integrationtest.lua +++ b/src/engine/test/integrationtest.lua @@ -218,9 +218,12 @@ function integration_test_runner:update() end function integration_test_runner:draw() - assert(self.current_test, "integration_test_runner:draw: current_test is not set") - api.print(self.current_test.name, 2, 2, colors.yellow) - api.print(self.current_state, 2, 9, self:_get_test_state_color(self.current_state)) + if self.current_test then + api.print(self.current_test.name, 2, 2, colors.yellow) + api.print(self.current_state, 2, 9, self:_get_test_state_color(self.current_state)) + else + api.print("no itest running", tuned("x", 8), tuned("y", 8), colors.white) + end end function integration_test_runner:_get_test_state_color(test_state) @@ -249,7 +252,7 @@ function integration_test_runner:_initialize() logging.logger.active_categories["itest"] = true --#pico8]] - logging.logger.active_categories["trace"] = false + logging.logger.active_categories["trace"] = true --#endif self.initialized = true diff --git a/tests/utestintegrationtest.lua b/tests/utestintegrationtest.lua index f4e8c63f..03900e05 100644 --- a/tests/utestintegrationtest.lua +++ b/tests/utestintegrationtest.lua @@ -169,12 +169,11 @@ describe('integration_test_runner', function () describe('(when another test was running)', function () - setup(function () + it('should reinit the gameapp modules first', function () integration_test_runner.current_test = integration_test('previous test', {}) - end) - it('should reinit the gameapp modules first', function () integration_test_runner:init_game_and_start(test) + assert.spy(gameapp_reinit_modules_stub).was_called(1) assert.spy(gameapp_reinit_modules_stub).was_called_with() end) @@ -566,20 +565,9 @@ describe('integration_test_runner', function () describe('draw', function () - it('should assert if no current test is set', function () - assert.has_error(function () - integration_test_runner:draw() - end, - "integration_test_runner:draw: current_test is not set") - end) - - describe('(when curent test is set)', function () - - local api_print_stub + describe('(stubbing api.print)', function () setup(function () - integration_test_runner.current_test = test - integration_test_runner.current_state = test_states.running api_print_stub = stub(api, "print") end) @@ -591,9 +579,23 @@ describe('integration_test_runner', function () api_print_stub:clear() end) - it('should draw information on the current test', function () + it('should draw "no itest running"', function () integration_test_runner:draw() - assert.spy(api_print_stub).was_called(2) + assert.spy(api_print_stub).was_called(1) + end) + + describe('(when current test is set)', function () + + before_each(function () + integration_test_runner.current_test = test + integration_test_runner.current_state = test_states.running + end) + + it('should draw information on the current test', function () + integration_test_runner:draw() + assert.spy(api_print_stub).was_called(2) + end) + end) end) @@ -664,8 +666,7 @@ describe('integration_test_runner', function () local action_callback = spy.new(function () end) local action_callback2 = spy.new(function () end) - before_each(function () - test:add_action(time_trigger(1.0), action_callback, 'action_callback') + setup(function () -- don't stub a function if the return value matters, as in start spy.on(integration_test_runner, "_check_end") end) @@ -678,6 +679,7 @@ describe('integration_test_runner', function () before_each(function () integration_test_runner:start(test) + test:add_action(time_trigger(1.0), action_callback, 'action_callback') end) after_each(function () From 5be04959c933158f8d409a9b049eb1468c17d8e5 Mon Sep 17 00:00:00 2001 From: hsandt Date: Tue, 16 Apr 2019 01:14:53 +0100 Subject: [PATCH 081/140] [PREBUILD] Fixed enum value substitution for directions It causes Sonic to move up when going right in the air, down when going left --- prebuild/replace_strings.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/prebuild/replace_strings.py b/prebuild/replace_strings.py index 1d42dc91..2d458381 100755 --- a/prebuild/replace_strings.py +++ b/prebuild/replace_strings.py @@ -49,6 +49,9 @@ # to its value dynamically with enum_values[dynamic_string] # remember to update the values of any preprocessed enum modified + # TODO: to avoid issues like enum values mismatch, parse the values + # directly from the .lua instead of writing them manually + # color 'colors': { 'black': 0, @@ -72,8 +75,8 @@ # math 'directions': { 'left': 0, - 'right': 1, - 'up': 2, + 'up': 1, + 'right': 2, 'down': 3, }, From c2453b73fe29dd0d21c7f5d005f042ddb0fffd66 Mon Sep 17 00:00:00 2001 From: hsandt Date: Tue, 16 Apr 2019 01:16:14 +0100 Subject: [PATCH 082/140] [COLLISION] Fixed air_motion_result _tostring to match member order --- src/engine/physics/collision.lua | 2 +- tests/utestcollision.lua | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/engine/physics/collision.lua b/src/engine/physics/collision.lua index 326b9726..7219beba 100644 --- a/src/engine/physics/collision.lua +++ b/src/engine/physics/collision.lua @@ -224,7 +224,7 @@ end --#if log function air_motion_result:_tostring() return "air_motion_result("..joinstr(", ", - self.position, self.is_blocked_by_ceiling, self.is_blocked_by_wall, self.is_landing, self.slope_angle)..")" + self.position, self.is_blocked_by_wall, self.is_blocked_by_ceiling, self.is_landing, self.slope_angle)..")" end --#endif diff --git a/tests/utestcollision.lua b/tests/utestcollision.lua index 361ae55d..9a234279 100644 --- a/tests/utestcollision.lua +++ b/tests/utestcollision.lua @@ -787,9 +787,9 @@ describe('collision', function () describe('_tostring', function () - it('should return "air_motion_result(vector(2, 3), false, false, true, -0.25)"', function () - local gmr = air_motion_result(vector(2, 3), false, false, true, -0.25) - assert.are_equal("air_motion_result(vector(2, 3), false, false, true, -0.25)", gmr:_tostring()) + it('should return "air_motion_result(vector(2, 3), true, false, true, -0.25)"', function () + local gmr = air_motion_result(vector(2, 3), true, false, true, -0.25) + assert.are_equal("air_motion_result(vector(2, 3), true, false, true, -0.25)", gmr:_tostring()) end) end) From 02070d9224302e7322f7e24dc693d6bd81e4a603 Mon Sep 17 00:00:00 2001 From: hsandt Date: Tue, 16 Apr 2019 01:17:26 +0100 Subject: [PATCH 083/140] [TEST] itest also outputs to log file, busted utests clear log on test suite start --- src/game/itest_main.lua | 6 ++++++ tests/bustedhelper.lua | 3 +++ 2 files changed, 9 insertions(+) diff --git a/src/game/itest_main.lua b/src/game/itest_main.lua index 7e75b461..b841ddb1 100644 --- a/src/game/itest_main.lua +++ b/src/game/itest_main.lua @@ -11,11 +11,17 @@ local input = require("engine/input/input") --#if log local logging = require("engine/debug/logging") logging.logger:register_stream(logging.console_log_stream) +logging.logger:register_stream(logging.file_log_stream) --#endif +local codetuner = require("engine/debug/codetuner") + local current_itest_index = 0 function _init() + -- clear log file on new itest session + logging.file_log_stream:clear() + -- require only gamestate modules written on first line of the required $itest (pico8-build way) gamestate_proxy:require_gamestates() diff --git a/tests/bustedhelper.lua b/tests/bustedhelper.lua index 280e8c31..a8cd95da 100644 --- a/tests/bustedhelper.lua +++ b/tests/bustedhelper.lua @@ -8,6 +8,9 @@ logging.logger:register_stream(logging.console_log_stream) logging.logger:register_stream(logging.file_log_stream) logging.logger:deactivate_all_categories() -- headless itests will restore "itest" and sometimes "trace" +-- clear log file on new utest session +logging.file_log_stream:clear() + -- return the current file line in the format "{file}:{line}" to make it easy to navigate there from the printed message -- if you call this function from an intermediate helper function, add an extra level for each intermediate step function get_file_line(extra_level) From 066c1d97a810ab9c7e75a9dcfd7515dfa670e799 Mon Sep 17 00:00:00 2001 From: hsandt Date: Tue, 16 Apr 2019 01:18:34 +0100 Subject: [PATCH 084/140] [TEST] Added itest for "platformer air left wall block" --- src/game/data/playercharacter_data.lua | 2 +- src/game/ingame/playercharacter.lua | 19 +++++++- src/game/itests/itestplayercharacter.lua | 62 +++++++++++++++++++++++- 3 files changed, 79 insertions(+), 4 deletions(-) diff --git a/src/game/data/playercharacter_data.lua b/src/game/data/playercharacter_data.lua index b795a9d6..5c21c909 100644 --- a/src/game/data/playercharacter_data.lua +++ b/src/game/data/playercharacter_data.lua @@ -24,7 +24,7 @@ return { max_ground_speed = 3, -- initial variable jump speed (Sonic) (px/frame) - initial_var_jump_speed_frame = 3.25, + initial_var_jump_speed_frame = 3.25, -- 208/64 = 3 + 16/64 -- initial hop vertical speed and new speed when jump is interrupted by releasing jump button (px/frame) -- note that when jump is interrupted mid-air, gravity should still be applied just after that diff --git a/src/game/ingame/playercharacter.lua b/src/game/ingame/playercharacter.lua index 25d9028c..0dcf735d 100644 --- a/src/game/ingame/playercharacter.lua +++ b/src/game/ingame/playercharacter.lua @@ -783,6 +783,7 @@ function player_char:_check_hold_jump() -- character tries to interrupt jump, check if's not too late local signed_jump_interrupt_speed_frame = -pc_data.jump_interrupt_speed_frame if self.velocity.y < signed_jump_interrupt_speed_frame then + log("interrupt jump "..self.velocity.y.." -> "..signed_jump_interrupt_speed_frame, "trace") self.velocity.y = signed_jump_interrupt_speed_frame end end @@ -822,7 +823,9 @@ function player_char:_compute_air_motion_result() -- we focus on landing/ceiling first, and prefer simplicity to precision as long as motion seems ok, -- so we choose c. self:_advance_in_air_along(motion_result, self.velocity, "y") + log("=> "..motion_result, "trace") self:_advance_in_air_along(motion_result, self.velocity, "x") + log("=> "..motion_result, "trace") return motion_result end @@ -831,8 +834,9 @@ end -- modifies ref_motion_result in-place, setting it to the result of an air motion from ref_motion_result.position -- over velocity[coord] px, where coord is "x" or "y" function player_char:_advance_in_air_along(ref_motion_result, velocity, coord) - if velocity[coord] == 0 then return end + log("_advance_in_air_along: "..joinstr(", ", ref_motion_result, velocity, coord), "trace") + if velocity[coord] == 0 then return end -- only full pixels matter for collisions, but subpixels may sum up to a full pixel -- so first estimate how many full pixel columns the character may actually explore this frame @@ -861,6 +865,7 @@ function player_char:_advance_in_air_along(ref_motion_result, velocity, coord) local pixel_distance_before_step = 0 while pixel_distance_before_step < max_pixel_distance and not ref_motion_result:is_blocked_along(direction) do self:_next_air_step(direction, ref_motion_result) + log(" => "..ref_motion_result, "trace") pixel_distance_before_step = pixel_distance_before_step + 1 end @@ -883,6 +888,7 @@ function player_char:_advance_in_air_along(ref_motion_result, velocity, coord) if velocity[coord] > 0 then local extra_step_motion_result = ref_motion_result:copy() self:_next_air_step(direction, extra_step_motion_result) + log(" => "..ref_motion_result, "trace") if extra_step_motion_result:is_blocked_along(direction) then -- character has just reached a wall, plus a few subpixels -- unlike classic sonic, we decide to cut the subpixels and block the character @@ -913,9 +919,15 @@ end -- it doesn't update the position and the corresponding flag is set -- air_motion_result.position.x/y should be floored for these steps function player_char:_next_air_step(direction, ref_motion_result) + log(" _next_air_step: "..joinstr(", ", direction, ref_motion_result), "trace") + local step_vec = dir_vectors[direction] local next_position_candidate = ref_motion_result.position + step_vec + log("direction: "..direction, "trace") + log("step_vec: "..step_vec, "trace") + log("next_position_candidate: "..next_position_candidate, "trace") + -- we can only hit walls or the ground when moving left, right or down if direction ~= directions.up then -- query ground to check for obstacles (we only care about distance, not slope angle) @@ -924,6 +936,8 @@ function player_char:_next_air_step(direction, ref_motion_result) local query_info = self:_compute_ground_sensors_signed_distance(next_position_candidate) local signed_distance_to_closest_ground, next_slope_angle = query_info.signed_distance, query_info.slope_angle + log("signed_distance_to_closest_ground: "..signed_distance_to_closest_ground, "trace") + -- check if the character has hit a ground or a wall if signed_distance_to_closest_ground < 0 then -- we do not activate step up during air motion, so any pixel above the character's bottom @@ -940,6 +954,7 @@ function player_char:_next_air_step(direction, ref_motion_result) ref_motion_result.slope_angle = next_slope_angle else ref_motion_result.is_blocked_by_wall = true + log("is blocked by wall", "trace") end elseif signed_distance_to_closest_ground > 0 then -- in the air: the most common case, in general requires nothing to do @@ -949,6 +964,7 @@ function player_char:_next_air_step(direction, ref_motion_result) elseif ref_motion_result.is_landing then -- if we enter this, direction must be horizontal, so update slope angle with new ground ref_motion_result.slope_angle = next_slope_angle + log("is landing, setting slope angle to "..next_slope_angle, "trace") end end @@ -963,6 +979,7 @@ function player_char:_next_air_step(direction, ref_motion_result) if is_blocked_by_ceiling_at_next then if direction == directions.up then ref_motion_result.is_blocked_by_ceiling = true + log("is blocked by ceiling", "trace") else -- we would be blocked by ceiling on the next position, but since we can't even go there, -- we are actually blocked by the wall preventing the horizontal move diff --git a/src/game/itests/itestplayercharacter.lua b/src/game/itests/itestplayercharacter.lua index e49290a4..d6b6c35b 100644 --- a/src/game/itests/itestplayercharacter.lua +++ b/src/game/itests/itestplayercharacter.lua @@ -15,7 +15,6 @@ local tile_test_data = require("game/test_data/tile_test_data") local itest - -- debug motion itest_dsl_parser.register( @@ -501,7 +500,7 @@ expect pc_velocity 1.359375 -0.078125 itest_dsl_parser.register( - 'platformer air wall block', [[ + 'platformer air right wall block', [[ @stage # .# .. @@ -540,6 +539,65 @@ expect pc_velocity 0 -1.125 -- that converts floats to this format if I want a meaningful -- comparison with itest trace log + +itest_dsl_parser.register( + 'platformer air left wall block', [[ +@stage # +#. +.. +.# + +warp 12 16 +jump +stop_jump +wait 1 +move left +wait 9 + +expect pc_bottom_pos 11 1.9375 +expect pc_motion_state airborne +expect pc_ground_spd 0 +expect pc_velocity 0 -1.125 +]]) + +itest_dsl_parser.register( + 'platformer air ceiling block', [[ +@stage # +# +. +. +. +# + +warp 4 32 +jump +wait 4 + +expect pc_bottom_pos 4 24 +expect pc_motion_state airborne +expect pc_ground_spd 0 +expect pc_velocity 0 0 +]]) + +-- calculation notes + +-- we are still using sonic standing height during jump +-- so he will hit ceiling at bottom pos = 24 = 32 - 8 + +-- wait for the apogee (frame 31) and stop +-- frame bottom pos velocity state event +-- 1 (4, 32) (0, 0) grounded +-- 2 (4, 32 - 3 - 16/64) (0, -3 - 16/64) airborne confirm jump (no gravity on first frame) +-- 3 (4, 32 - 6 - 25/64) (0, -3 - 9/64) airborne +-- 4 (4, 32 - 8) (0, 0) airborne hit ceiling + +-- keep calculation below for later, when sonic will have half height during spin +-- 4 (4, 32 - 9 - 27/64) (0, -3 - 2/64) airborne +-- 5 (4, 32 - 12 - 22/64) (0, -2 - 59/64) airborne +-- 6 (4, 32 - 15 - 10/64) (0, -2 - 52/64) airborne +-- 7 (4, 32 - 16) (0, 0) airborne hit ceiling + + -- human tests -- pico8 only, since human must check rendering --[=[#pico8 From 13494a085cf749ff9d19b3b7aad9433ce5f2785c Mon Sep 17 00:00:00 2001 From: hsandt Date: Thu, 18 Apr 2019 00:38:24 +0100 Subject: [PATCH 085/140] [PREBUILD] Preprocess: support deeper level pico8 comment blocks --[==[#pico8 etc. --- prebuild/preprocess.py | 4 ++-- prebuild/test_preprocess.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/prebuild/preprocess.py b/prebuild/preprocess.py index 3bc977c8..bee86901 100644 --- a/prebuild/preprocess.py +++ b/prebuild/preprocess.py @@ -74,9 +74,9 @@ class ParsingMode(Enum): # tag to enter a pico8-only block (it's a comment block so that busted never runs it but preprocess reactivates it) # unlike normal comment blocks, we expect to match from the line start -pico8_start_pattern = re.compile(r"--\[\[#pico8") +pico8_start_pattern = re.compile(r"--\[=*\[#pico8") # closing tag for pico8-only block. Unlike normal comment blocks, we expect to match from the line start and we ignore anything after the block end! -pico8_end_pattern = re.compile(r"--#pico8]]") +pico8_end_pattern = re.compile(r"--#pico8]=*]") if_pattern = re.compile(r"--#if (\w+)") # ! ignore anything after 1st symbol ifn_pattern = re.compile(r"--#ifn (\w+)") # ! ignore anything after 1st symbol diff --git a/prebuild/test_preprocess.py b/prebuild/test_preprocess.py index 98b62150..ed390829 100644 --- a/prebuild/test_preprocess.py +++ b/prebuild/test_preprocess.py @@ -321,12 +321,12 @@ def test_preprocess_lines_stop_pico8_outside_pico8_block(self): def test_preprocess_lines_refused_if_inside_pico8_block(self): test_lines = [ 'print("start")\n', - '--[[#pico8 pico8 start\n', + '--[=[#pico8 pico8 start\n', 'real pico8 code\n', '--#if log\n', 'log only\n', '--#endif\n', - '--#pico8]] exceptionally ignored\n', + '--#pico8]=] exceptionally ignored\n', 'print("end")\n', ] expected_processed_lines = [ @@ -358,12 +358,12 @@ def test_preprocess_lines_accepted_if_inside_pico8_block(self): def test_preprocess_lines_refused_ifn_inside_pico8_block(self): test_lines = [ 'print("start")\n', - '--[[#pico8 pico8 start\n', + '--[==[#pico8 pico8 start\n', 'real pico8 code\n', '--#ifn log\n', 'release only\n', '--#endif\n', - '--#pico8]] exceptionally ignored\n', + '--#pico8]==] exceptionally ignored\n', 'print("end")\n', ] expected_processed_lines = [ From 0bdbc89a8dc7b36131df69ec86f6884986f537cd Mon Sep 17 00:00:00 2001 From: hsandt Date: Thu, 18 Apr 2019 00:47:34 +0100 Subject: [PATCH 086/140] [BUILD] Fixed file_log_stream:clear() not surrounded by #if log failing release build --- src/game/itest_main.lua | 2 ++ src/game/main.lua | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/game/itest_main.lua b/src/game/itest_main.lua index b841ddb1..012b2a13 100644 --- a/src/game/itest_main.lua +++ b/src/game/itest_main.lua @@ -19,8 +19,10 @@ local codetuner = require("engine/debug/codetuner") local current_itest_index = 0 function _init() +--#if log -- clear log file on new itest session logging.file_log_stream:clear() +--#endif -- require only gamestate modules written on first line of the required $itest (pico8-build way) gamestate_proxy:require_gamestates() diff --git a/src/game/main.lua b/src/game/main.lua index dc55de45..a08c6295 100644 --- a/src/game/main.lua +++ b/src/game/main.lua @@ -35,8 +35,10 @@ input:toggle_mouse(true) -- pico-8 functions must be placed at the end to be parsed by p8tool function _init() +--#if log -- clear log file on new game session logging.file_log_stream:clear() +--#endif -- require all gamestate modules, according to preprocessing step gamestate_proxy:require_gamestates() From f9c2bbdcfd491aab6c7206a4d68372e53a96bde3 Mon Sep 17 00:00:00 2001 From: hsandt Date: Thu, 18 Apr 2019 01:04:04 +0100 Subject: [PATCH 087/140] [ITEST] Added input to navigate between itests --- src/game/itest_main.lua | 38 ++++++++++++++++++++++++++++++++------ tests/pico8api.lua | 2 ++ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/game/itest_main.lua b/src/game/itest_main.lua index 012b2a13..fdbc460f 100644 --- a/src/game/itest_main.lua +++ b/src/game/itest_main.lua @@ -41,19 +41,45 @@ function _draw() end function init_game_and_start_next_itest() - if #itest_manager.itests > current_itest_index then - current_itest_index = current_itest_index + 1 - itest_manager:init_game_and_start_by_index(current_itest_index) + init_game_and_start_itest_by_relative_index(1) +end + +function init_game_and_start_itest_by_relative_index(delta) + -- clamp new index + local new_index = mid(1, current_itest_index + delta, #itest_manager.itests) + -- check that an effective idnex change occurs + if new_index ~= current_itest_index then + current_itest_index = new_index + itest_manager:init_game_and_start_by_index(new_index) end end +-- press left/right to navigate freely in itests, even if not finished +-- press x to skip itest only if finished function handle_input() + -- since input.mode is simulated during itests, use pico8 api directly for input + if btnp(button_ids.left) then + -- go back to previous itest + init_game_and_start_itest_by_relative_index(-1) + return + elseif btnp(button_ids.right) then + -- skip current itest + init_game_and_start_next_itest() + return + elseif btnp(button_ids.up) then + -- go back 10 itests + init_game_and_start_itest_by_relative_index(-10) + return + elseif btnp(button_ids.down) then + -- skip many itests + init_game_and_start_itest_by_relative_index(10) + return + end + if integration_test_runner.current_state == test_states.success or integration_test_runner.current_state == test_states.failure or integration_test_runner.current_state == test_states.timeout then - -- previous itest has finished, wait for x press to continue - -- to next itest - -- since input.mode is simulated during itests, use pico8 api directly + -- previous itest has finished, wait for x press to continue to next itest if btnp(button_ids.x) then init_game_and_start_next_itest() end diff --git a/tests/pico8api.lua b/tests/pico8api.lua index 4298046c..b1a2c1bb 100644 --- a/tests/pico8api.lua +++ b/tests/pico8api.lua @@ -399,6 +399,8 @@ function max(a, b) return b end +-- return value in the middle +-- can also be used for clamping function mid(x, y, z) return (x<=y)and((y<=z)and y or((x Date: Thu, 18 Apr 2019 01:46:26 +0100 Subject: [PATCH 088/140] [SPRITE] Render flipped sprite with correct pivot --- src/engine/render/sprite.lua | 20 +++++++++++++++++-- tests/utestsprite.lua | 37 ++++++++++++++++++++++++++++++++---- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/src/engine/render/sprite.lua b/src/engine/render/sprite.lua index 81b7f657..a34bbb79 100644 --- a/src/engine/render/sprite.lua +++ b/src/engine/render/sprite.lua @@ -1,3 +1,4 @@ +require("engine/application/constants") require("engine/core/math") require("engine/render/color") @@ -26,8 +27,23 @@ end -- flip_y bool function sprite_data:render(position, flip_x, flip_y) set_unique_transparency(colors.pink) - - local draw_pos = position - self.pivot + + local pivot = self.pivot:copy() + + if flip_x then + -- flip pivot on x + local spr_width = self.span.i * tile_size + pivot.x = spr_width - self.pivot.x + end + + if flip_y then + -- flip pivot on y + local spr_height = self.span.j * tile_size + pivot.y = spr_height - self.pivot.y + end + + local draw_pos = position - pivot + spr(self.id_loc:to_sprite_id(), draw_pos.x, draw_pos.y, self.span.i, self.span.j, diff --git a/tests/utestsprite.lua b/tests/utestsprite.lua index a3271a58..8dc30875 100644 --- a/tests/utestsprite.lua +++ b/tests/utestsprite.lua @@ -57,7 +57,8 @@ describe('sprite', function () describe('render', function () - local spr_data = sprite_data(sprite_id_location(1, 3), tile_vector(2, 3), vector(2, 4)) + local spr_data = sprite_data(sprite_id_location(1, 3), tile_vector(2, 3), vector(11, 10)) + local spr_data2 = sprite_data(sprite_id_location(1, 3), tile_vector(2, 1), vector(8, 4)) local spr_stub setup(function () @@ -72,10 +73,38 @@ describe('sprite', function () spr_stub:clear() end) - it('should render the sprite from the id location, at the draw position minus pivot, with correct span and flip', function () - spr_data:render(vector(4, 8), false, true) + it('should render the sprite from the id location, at the draw position minus pivot, with correct span when not flipping', function () + spr_data:render(vector(41, 80), false, false) + assert.spy(spr_stub).was_called(1) + assert.spy(spr_stub).was_called_with(49, 30, 70, 2, 3, false, false) + end) + + it('should render the sprite from the id location, at the draw position minus pivot itself flipped on x, with correct span when flipping x', function () + spr_data:render(vector(41, 80), true, false) + assert.spy(spr_stub).was_called(1) + -- flip pivot (11, 10) around center x axis which is at 8 * span.x / 2 = 8 -> flipped pivot (5, 10) + assert.spy(spr_stub).was_called_with(49, 36, 70, 2, 3, true, false) + end) + + it('should render the sprite from the id location, at the draw position minus pivot itself flipped on y, with correct span when flipping y', function () + spr_data:render(vector(41, 80), false, true) + assert.spy(spr_stub).was_called(1) + -- flip pivot (11, 10) around center y axis which is at 8 * span.y / 2 = 12 -> flipped pivot (11, 14) + assert.spy(spr_stub).was_called_with(49, 30, 66, 2, 3, false, true) + end) + + it('should render the sprite from the id location, at the draw position minus pivot itself flipped on x and y, with correct span when flipping x and y', function () + spr_data:render(vector(41, 80), true, true) + assert.spy(spr_stub).was_called(1) + assert.spy(spr_stub).was_called_with(49, 36, 66, 2, 3, true, true) + end) + + it('should render the sprite from the id location, at the draw position minus pivot located at center, with correct span when flipping x and y', function () + spr_data2:render(vector(8, 4), true, true) assert.spy(spr_stub).was_called(1) - assert.spy(spr_stub).was_called_with(49, 2, 4, 2, 3, false, true) + -- pivot is already at center, so flip has no effect on it + -- and since position == pivot, it draws at the origin + assert.spy(spr_stub).was_called_with(49, 0, 0, 2, 1, true, true) end) end) From 6af0b2431a508c21078e8f5151d84ae3b2b990c9 Mon Sep 17 00:00:00 2001 From: hsandt Date: Thu, 18 Apr 2019 01:47:37 +0100 Subject: [PATCH 089/140] [MOTION] Update horizontal motion with ground motion and render flipped sprite accordingly --- src/game/ingame/playercharacter.lua | 44 ++++--- src/game/itests/itestplayercharacter.lua | 21 +++- tests/utestplayercharacter.lua | 139 +++++++++++++++++------ 3 files changed, 148 insertions(+), 56 deletions(-) diff --git a/src/game/ingame/playercharacter.lua b/src/game/ingame/playercharacter.lua index 0dcf735d..f33d83cc 100644 --- a/src/game/ingame/playercharacter.lua +++ b/src/game/ingame/playercharacter.lua @@ -47,21 +47,22 @@ local player_char = new_class() -- state vars --- control_mode control_modes control mode: human (default) or ai --- motion_mode (cheat) motion_modes motion mode: platformer (under gravity) or debug (fly around) --- motion_state motion_states motion state (platformer mode only) --- position vector current position (character center "between" pixels) --- ground_speed float current speed along the ground (~px/frame) --- velocity vector current velocity in platformer mode (px/frame) --- debug_velocity vector current velocity in debug mode (m/s) --- slope_angle float slope angle of the current ground (clockwise turn ratio) --- move_intention vector current move intention (normalized) --- jump_intention bool current intention to start jump (consumed on jump) --- hold_jump_intention bool current intention to hold jump (always true when jump_intention is true) --- should_jump bool should the character jump when next frame is entered? used to delay variable jump/hop by 1 frame --- has_jumped_this_frame bool has the character started a jump/hop this frame? --- has_interrupted_jump bool has the character already interrupted his jump once? --- current_sprite string current sprite key in the spr_data +-- control_mode control_modes control mode: human (default) or ai +-- motion_mode (cheat) motion_modes motion mode: platformer (under gravity) or debug (fly around) +-- motion_state motion_states motion state (platformer mode only) +-- horizontal_dir horizontal_dirs direction faced by character +-- position vector current position (character center "between" pixels) +-- ground_speed float current speed along the ground (~px/frame) +-- velocity vector current velocity in platformer mode (px/frame) +-- debug_velocity vector current velocity in debug mode (m/s) +-- slope_angle float slope angle of the current ground (clockwise turn ratio) +-- move_intention vector current move intention (normalized) +-- jump_intention bool current intention to start jump (consumed on jump) +-- hold_jump_intention bool current intention to hold jump (always true when jump_intention is true) +-- should_jump bool should the character jump when next frame is entered? used to delay variable jump/hop by 1 frame +-- has_jumped_this_frame bool has the character started a jump/hop this frame? +-- has_interrupted_jump bool has the character already interrupted his jump once? +-- current_sprite string current sprite key in the spr_data function player_char:_init() self.spr_data = pc_data.sonic_sprite_data self.debug_move_max_speed = pc_data.debug_move_max_speed @@ -77,6 +78,7 @@ function player_char:_setup() self.motion_mode = motion_modes.platformer --#endif self.motion_state = motion_states.grounded + self.horizontal_dir = horizontal_dirs.right self.position = vector.zero() self.ground_speed = 0. @@ -427,6 +429,7 @@ end -- update ground speed based on current move intention function player_char:_update_ground_speed_by_intention() if self.move_intention.x ~= 0 then + if self.ground_speed == 0 or sgn(self.ground_speed) == sgn(self.move_intention.x) then -- accelerate self.ground_speed = self.ground_speed + self.move_intention.x * pc_data.ground_accel_frame2 @@ -440,6 +443,14 @@ function player_char:_update_ground_speed_by_intention() self.ground_speed = sgn(self.ground_speed) * pc_data.ground_accel_frame2 end end + + if self.ground_speed ~= 0 then + -- always update direction when player tries to move and the character is moving after update + -- this is useful even when move intention x has same sign as ground speed, + -- as the character may be running backward after failing to run a steep slope up + self.horizontal_dir = signed_speed_to_dir(self.ground_speed) + end + elseif self.ground_speed ~= 0 then -- friction self.ground_speed = sgn(self.ground_speed) * max(0, abs(self.ground_speed) - pc_data.ground_friction_frame2) @@ -1034,7 +1045,8 @@ end -- render the player character sprite at its current position function player_char:render() - self.spr_data[self.current_sprite]:render(self.position) +local flip_x = self.horizontal_dir == horizontal_dirs.left + self.spr_data[self.current_sprite]:render(self.position, flip_x) end return player_char diff --git a/src/game/itests/itestplayercharacter.lua b/src/game/itests/itestplayercharacter.lua index d6b6c35b..ef7423e1 100644 --- a/src/game/itests/itestplayercharacter.lua +++ b/src/game/itests/itestplayercharacter.lua @@ -598,21 +598,36 @@ expect pc_velocity 0 0 -- 7 (4, 32 - 16) (0, 0) airborne hit ceiling +--[=[#pico8 + + -- human tests -- pico8 only, since human must check rendering ---[=[#pico8 +-- they have no final assertion, let the user check if result is correct or not (note it will display success whatever) + -- bugfix history: -- = fixed character pivot computed from drawn sprite topleft (with some gap above character's head) -- and not actual sprite topleft in the spritesheet itest_dsl_parser.register( - 'pc render idle', [[ + 'pc render idle right', [[ +@stage # +. +# + +warp 4 8 +]]) + +itest_dsl_parser.register( + 'pc render idle left', [[ @stage # . # warp 4 8 +move left +wait 1 +stop ]]) --- no final assertion, let the user check if result is correct or not (note it will display success whatever) --#pico8]=] diff --git a/tests/utestplayercharacter.lua b/tests/utestplayercharacter.lua index 8851a1a6..34572ae5 100644 --- a/tests/utestplayercharacter.lua +++ b/tests/utestplayercharacter.lua @@ -9,7 +9,8 @@ local tile_test_data = require("game/test_data/tile_test_data") describe('player_char', function () - -- static method + -- static methods + describe('_compute_max_pixel_distance', function () it('(2, 0) => 0', function () @@ -66,6 +67,9 @@ describe('player_char', function () end) + + -- methods + describe('_init', function () setup(function () @@ -89,13 +93,6 @@ describe('player_char', function () assert.spy(player_char._setup).was_called_with(match.ref(pc)) end) - it('should create a player character with control mode: human, motion mode: platformer, motion state: grounded', function () - local pc = player_char() - assert.is_not_nil(pc) - assert.are_same({control_modes.human, motion_modes.platformer, motion_states.grounded}, - {pc.control_mode, pc.motion_mode, pc.motion_state}) - end) - it('should create a player character storing values from playercharacter_data', function () local pc = player_char() assert.is_not_nil(pc) @@ -123,6 +120,10 @@ describe('player_char', function () assert.is_not_nil(pc) assert.are_same( { + control_modes.human, + motion_modes.platformer, + motion_states.grounded, + horizontal_dirs.right, vector.zero(), 0, vector.zero(), @@ -137,11 +138,17 @@ describe('player_char', function () "idle" }, { + pc.control_mode, + pc.motion_mode, + pc.motion_state, + pc.horizontal_dir, + pc.position, pc.ground_speed, pc.velocity, pc.debug_velocity, pc.slope_angle, + pc.move_intention, pc.jump_intention, pc.hold_jump_intention, @@ -1616,110 +1623,154 @@ describe('player_char', function () describe('_update_ground_speed_by_intention', function () - it('should accelerate when character has ground speed 0 and move intention x is not 0', function () + it('should accelerate and set direction based on new speed when character is facing left, has ground speed 0 and move intention x > 0', function () + pc.horizontal_dir = horizontal_dirs.left pc.move_intention.x = 1 pc:_update_ground_speed_by_intention() - assert.are_equal(pc_data.ground_accel_frame2, pc.ground_speed) + assert.are_same({horizontal_dirs.right, pc_data.ground_accel_frame2}, + {pc.horizontal_dir, pc.ground_speed}) end) - it('should accelerate when character has ground speed > 0 and move intention x > 0', function () + it('should accelerate and set direction when character is facing left, has ground speed > 0 and move intention x > 0', function () + pc.horizontal_dir = horizontal_dirs.left -- rare to oppose ground speed sense, but possible when running backward pc.ground_speed = 1.5 pc.move_intention.x = 1 pc:_update_ground_speed_by_intention() - assert.are_equal(1.5 + pc_data.ground_accel_frame2, pc.ground_speed) + assert.are_same({horizontal_dirs.right, 1.5 + pc_data.ground_accel_frame2}, + {pc.horizontal_dir, pc.ground_speed}) end) - it('should accelerate when character has ground speed < 0 and move intention x < 0', function () + it('should accelerate and preserve direction when character is facing left, has ground speed < 0 and move intention x < 0', function () + pc.horizontal_dir = horizontal_dirs.left -- rare to oppose ground speed sense, but possible when running backward pc.ground_speed = -1.5 pc.move_intention.x = -1 pc:_update_ground_speed_by_intention() - assert.are_equal(-1.5 - pc_data.ground_accel_frame2, pc.ground_speed) + assert.are_same({horizontal_dirs.left, -1.5 - pc_data.ground_accel_frame2}, + {pc.horizontal_dir, pc.ground_speed}) end) - it('should decelerate keeping same sign when character has high ground speed > 0 and move intention x < 0', function () + it('should decelerate keeping same sign and direction when character is facing right, has high ground speed > ground accel * 1 frame and move intention x < 0', function () + pc.horizontal_dir = horizontal_dirs.right pc.ground_speed = 1.5 pc.move_intention.x = -1 pc:_update_ground_speed_by_intention() -- ground_decel_frame2 = 0.25, subtract it from ground_speed - assert.are_equal(1.25, pc.ground_speed) + assert.are_same({horizontal_dirs.right, 1.25}, + {pc.horizontal_dir, pc.ground_speed}) end) - -- bugfix history: missing tests that check the change of sign of ground speed - it('_ should decelerate and change sign when character has low ground speed > 0 and move intention x < 0 '.. + it('should decelerate and stop exactly at speed 0, preserving direction, when character has ground speed = ground accel * 1 frame and move intention x < 0', function () + pc.horizontal_dir = horizontal_dirs.right + pc.ground_speed = 0.25 + pc.move_intention.x = -1 + pc:_update_ground_speed_by_intention() + -- ground_decel_frame2 = 0.25, subtract it from ground_speed + assert.are_same({horizontal_dirs.right, 0}, + {pc.horizontal_dir, pc.ground_speed}) + end) + + -- bugfix history: + -- _ missing tests that check the change of sign of ground speed + it('should decelerate and change sign and direction when character is facing right, '.. + 'has low ground speed > 0 but < ground accel * 1 frame and move intention x < 0 '.. 'but the ground speed is high enough so that the new speed wouldn\'t be over the max ground speed', function () + pc.horizontal_dir = horizontal_dirs.right -- start with speed >= -ground_accel_frame2 + ground_decel_frame2 pc.ground_speed = 0.24 pc.move_intention.x = -1 pc:_update_ground_speed_by_intention() + assert.are_equal(horizontal_dirs.left, pc.horizontal_dir) assert.is_true(almost_eq_with_message(-0.01, pc.ground_speed, 1e-16)) end) - it('should decelerate and clamp to the max ground speed in the opposite sign '.. - 'when character has low ground speed > 0 and move intention x < 0', function () + it('should change direction, decelerate and clamp to the max ground speed in the opposite sign '.. + 'when character is facing right, has low ground speed > 0 and move intention x < 0', function () + pc.horizontal_dir = horizontal_dirs.right -- start with speed < -ground_accel_frame2 + ground_decel_frame2 pc.ground_speed = 0.12 pc.move_intention.x = -1 pc:_update_ground_speed_by_intention() - assert.are_equal(-pc_data.ground_accel_frame2, pc.ground_speed) + assert.are_same({horizontal_dirs.left, -pc_data.ground_accel_frame2}, + {pc.horizontal_dir, pc.ground_speed}) end) + -- tests below seem symmetrical, but as a twist we have the character running backward + -- so he's facing the opposite direction of the run, so we can test direction update + it('should decelerate keeping same sign when character has high ground speed < 0 and move intention x > 0', function () + pc.horizontal_dir = horizontal_dirs.right pc.ground_speed = -1.5 pc.move_intention.x = 1 pc:_update_ground_speed_by_intention() - assert.are_equal(-1.25, pc.ground_speed) + assert.are_same({horizontal_dirs.left, -1.25}, + {pc.horizontal_dir, pc.ground_speed}) end) - -- bugfix history: missing tests that check the change of sign of ground speed - it('_ should decelerate and change sign when character has low ground speed < 0 and move intention x > 0 '.. + it('should decelerate and change sign when character has low ground speed < 0 and move intention x > 0 '.. 'but the ground speed is high enough so that the new speed wouldn\'t be over the max ground speed', function () + pc.horizontal_dir = horizontal_dirs.right -- start with speed <= ground_accel_frame2 - ground_decel_frame2 pc.ground_speed = -0.24 pc.move_intention.x = 1 pc:_update_ground_speed_by_intention() + assert.are_equal(horizontal_dirs.right, pc.horizontal_dir) assert.is_true(almost_eq_with_message(0.01, pc.ground_speed, 1e-16)) end) it('should decelerate and clamp to the max ground speed in the opposite sign '.. 'when character has low ground speed < 0 and move intention x > 0', function () + pc.horizontal_dir = horizontal_dirs.right -- start with speed > ground_accel_frame2 - ground_decel_frame2 pc.ground_speed = -0.12 pc.move_intention.x = 1 pc:_update_ground_speed_by_intention() - assert.are_equal(pc_data.ground_accel_frame2, pc.ground_speed) + assert.are_same({horizontal_dirs.right, pc_data.ground_accel_frame2}, + {pc.horizontal_dir, pc.ground_speed}) end) - it('should apply friction when character has ground speed > 0 and move intention x is 0', function () + it('should apply friction and preserve direction when character has ground speed > 0 and move intention x is 0', function () + pc.horizontal_dir = horizontal_dirs.right pc.ground_speed = 1.5 pc:_update_ground_speed_by_intention() - assert.are_equal(1.5 - pc_data.ground_friction_frame2, pc.ground_speed) + assert.are_same({horizontal_dirs.right, 1.5 - pc_data.ground_friction_frame2}, + {pc.horizontal_dir, pc.ground_speed}) end) -- bugfix history: missing tests that check the change of sign of ground speed - it('_ should apply friction but stop at 0 without changing ground speed sign when character has low ground speed > 0 and move intention x is 0', function () + it('_ should apply friction and preserve direction but stop at 0 without changing ground speed sign when character has low ground speed > 0 and move intention x is 0', function () + pc.horizontal_dir = horizontal_dirs.right -- must be < friction pc.ground_speed = 0.01 pc:_update_ground_speed_by_intention() - assert.are_equal(0, pc.ground_speed) + assert.are_same({horizontal_dirs.right, 0}, + {pc.horizontal_dir, pc.ground_speed}) end) - it('should apply friction when character has ground speed < 0 and move intention x is 0', function () + -- tests below seem symmetrical, but the character is actually running backward + + it('should apply friction and preserive direction when character has ground speed < 0 and move intention x is 0', function () + pc.horizontal_dir = horizontal_dirs.right pc.ground_speed = -1.5 pc:_update_ground_speed_by_intention() - assert.are_equal(-1.5 + pc_data.ground_friction_frame2, pc.ground_speed) + assert.are_same({horizontal_dirs.right, -1.5 + pc_data.ground_friction_frame2}, + {pc.horizontal_dir, pc.ground_speed}) end) -- bugfix history: missing tests that check the change of sign of ground speed it('_ should apply friction but stop at 0 without changing ground speed sign when character has low ground speed < 0 and move intention x is 0', function () + pc.horizontal_dir = horizontal_dirs.right -- must be < friction in abs pc.ground_speed = -0.01 pc:_update_ground_speed_by_intention() - assert.are_equal(0, pc.ground_speed) + assert.are_same({horizontal_dirs.right, 0}, + {pc.horizontal_dir, pc.ground_speed}) end) - it('should not change ground speed when ground speed is 0 and move intention x is 0', function () + it('should not change ground speed nor direction when ground speed is 0 and move intention x is 0', function () + pc.horizontal_dir = horizontal_dirs.left pc:_update_ground_speed_by_intention() - assert.are_equal(0, pc.ground_speed) + assert.are_same({horizontal_dirs.left, 0}, + {pc.horizontal_dir, pc.ground_speed}) end) end) -- _update_ground_speed_by_intention @@ -3718,7 +3769,7 @@ describe('player_char', function () end) - describe('render', function () + describe('#solo render', function () local spr_data_render_stub @@ -3734,11 +3785,25 @@ describe('player_char', function () spr_data_render_stub:clear() end) - it('should call render on sonic sprite data: idle with the character\'s position', function () + it('(when character is facing left) should call render on sonic sprite data: idle with the character\'s position, flipped x', function () + pc.position = vector(12, 8) + pc.horizontal_dir = horizontal_dirs.left + + pc:render() + + assert.spy(spr_data_render_stub).was_called(1) + assert.spy(spr_data_render_stub).was_called_with(match.ref(pc_data.sonic_sprite_data["idle"]), vector(12, 8), true) + end) + it('(when character is facing right) should call render on sonic sprite data: idle with the character\'s position, not flipped x', function () + pc.position = vector(12, 8) + pc.horizontal_dir = horizontal_dirs.right + pc:render() + assert.spy(spr_data_render_stub).was_called(1) - assert.spy(spr_data_render_stub).was_called_with(match.ref(pc_data.sonic_sprite_data["idle"]), pc.position) + assert.spy(spr_data_render_stub).was_called_with(match.ref(pc_data.sonic_sprite_data["idle"]), vector(12, 8), false) end) + end) end) From c0f9c35145094ded98be875d8508482023a318a6 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 19 Apr 2019 18:32:00 +0100 Subject: [PATCH 090/140] [RENDER] Added render itest for airborne flip (currently failing for human eyes) --- src/game/itests/itestplayercharacter.lua | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/game/itests/itestplayercharacter.lua b/src/game/itests/itestplayercharacter.lua index ef7423e1..d8448453 100644 --- a/src/game/itests/itestplayercharacter.lua +++ b/src/game/itests/itestplayercharacter.lua @@ -629,5 +629,28 @@ wait 1 stop ]]) +itest_dsl_parser.register( + 'pc render airborne right', [[ +@stage # +. +# + +warp 4 7 +]]) + +itest_dsl_parser.register( + 'pc render airborne left', [[ +@stage # +. +# + +warp 4 7 +move left +wait 1 +stop +]]) +-- note: due to flooring, character will go 1px to the left in only 1 frame, +-- so it will look offset compared to the previous test with right + --#pico8]=] From 312880e0d8c28fa0cd0c5d68bd7199f1584a43bd Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 19 Apr 2019 18:44:48 +0100 Subject: [PATCH 091/140] [RENDER] Added test for spin sprite render --- src/game/ingame/playercharacter.lua | 4 ++-- tests/bustedhelper.lua | 6 ++++++ tests/utestplayercharacter.lua | 28 +++++++++++++++++++++++++++- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/game/ingame/playercharacter.lua b/src/game/ingame/playercharacter.lua index f33d83cc..897ae6f4 100644 --- a/src/game/ingame/playercharacter.lua +++ b/src/game/ingame/playercharacter.lua @@ -1045,8 +1045,8 @@ end -- render the player character sprite at its current position function player_char:render() -local flip_x = self.horizontal_dir == horizontal_dirs.left - self.spr_data[self.current_sprite]:render(self.position, flip_x) + local flip_x = self.horizontal_dir == horizontal_dirs.left + self.spr_data[self.current_sprite]:render(self.position, flip_x) end return player_char diff --git a/tests/bustedhelper.lua b/tests/bustedhelper.lua index a8cd95da..61a98e54 100644 --- a/tests/bustedhelper.lua +++ b/tests/bustedhelper.lua @@ -47,3 +47,9 @@ end -- + the test helped me identify potential bugs and tricky cases during implementation of the function using the test -- ! the test failed, revealing a bug hidden in another function indirectly used by the test but not developed at the same time -- * the test revealed a regression/feature change later during development (very useful) + +-- Note about testing with was_called and was_called_with +-- I reported this issue: assert.spy().was_called_with(...) doesn't provide helpful information on failure #578 +-- on https://github.com/Olivine-Labs/busted/issues/578 +-- When debugging arguments actually called, use this workaround: +-- print(nice_dump(spy/stub.calls[i].refs/vals)), e.g. print(nice_dump(stub.calls[1].vals)) diff --git a/tests/utestplayercharacter.lua b/tests/utestplayercharacter.lua index 34572ae5..d6458e00 100644 --- a/tests/utestplayercharacter.lua +++ b/tests/utestplayercharacter.lua @@ -3774,7 +3774,8 @@ describe('player_char', function () local spr_data_render_stub setup(function () - spr_data_render_stub = stub(pc_data.sonic_sprite_data["idle"], "render") + -- create a generic stub at struct level so it works with any particular sprite + spr_data_render_stub = stub(sprite_data, "render") end) teardown(function () @@ -3794,6 +3795,7 @@ describe('player_char', function () assert.spy(spr_data_render_stub).was_called(1) assert.spy(spr_data_render_stub).was_called_with(match.ref(pc_data.sonic_sprite_data["idle"]), vector(12, 8), true) end) + it('(when character is facing right) should call render on sonic sprite data: idle with the character\'s position, not flipped x', function () pc.position = vector(12, 8) pc.horizontal_dir = horizontal_dirs.right @@ -3804,6 +3806,30 @@ describe('player_char', function () assert.spy(spr_data_render_stub).was_called_with(match.ref(pc_data.sonic_sprite_data["idle"]), vector(12, 8), false) end) + it('(when character is airborne, facing left) should call render on sonic sprite data: spin with the character\'s position, flipped x', function () + pc.motion_state = motion_states.airborne -- optional, just to be consistent with current_sprite + pc.current_sprite = "spin" + pc.position = vector(12, 8) + pc.horizontal_dir = horizontal_dirs.left + + pc:render() + + assert.spy(spr_data_render_stub).was_called(1) + assert.spy(spr_data_render_stub).was_called_with(match.ref(pc_data.sonic_sprite_data["spin"]), vector(12, 8), true) + end) + + it('#solo (when character is airborne, facing right) should call render on sonic sprite data: spin with the character\'s position, not flipped x', function () + pc.motion_state = motion_states.airborne -- optional, just to be consistent with current_sprite + pc.current_sprite = "spin" + pc.position = vector(12, 8) + pc.horizontal_dir = horizontal_dirs.right + + pc:render() + + assert.spy(spr_data_render_stub).was_called(1) + assert.spy(spr_data_render_stub).was_called_with(match.ref(pc_data.sonic_sprite_data["spin"]), vector(12, 8), false) + end) + end) end) From 7ba3089e882a36e5b918a071bdfce18d9aed1440 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 19 Apr 2019 18:51:29 +0100 Subject: [PATCH 092/140] [MOTION] Set horizontal direction to intended motion direction in the air --- src/game/ingame/playercharacter.lua | 9 +++++++-- tests/utestplayercharacter.lua | 24 ++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/game/ingame/playercharacter.lua b/src/game/ingame/playercharacter.lua index 897ae6f4..4a07cffc 100644 --- a/src/game/ingame/playercharacter.lua +++ b/src/game/ingame/playercharacter.lua @@ -757,8 +757,13 @@ function player_char:_update_platformer_motion_airborne() -- this means gravity will *not* be applied during the hop/interrupt jump frame self:_check_hold_jump() - -- apply x acceleration via intention (if not 0) - self.velocity.x = self.velocity.x + self.move_intention.x * pc_data.air_accel_x_frame2 + if self.move_intention.x ~= 0 then + -- apply x acceleration via intention (if not 0) + self.velocity.x = self.velocity.x + self.move_intention.x * pc_data.air_accel_x_frame2 + + -- in the air, apply intended motion to direction immediately + self.horizontal_dir = signed_speed_to_dir(self.move_intention.x) + end -- apply air motion diff --git a/tests/utestplayercharacter.lua b/tests/utestplayercharacter.lua index d6458e00..44ec0cc0 100644 --- a/tests/utestplayercharacter.lua +++ b/tests/utestplayercharacter.lua @@ -2976,6 +2976,26 @@ describe('player_char', function () assert.are_equal(4 - pc_data.air_accel_x_frame2, pc.velocity.x) end) + it('should set horizontal direction to intended motion direction: left', function () + pc.horizontal_dir = horizontal_dirs.right + pc.velocity.x = 4 + pc.move_intention.x = -1 + + pc:_update_platformer_motion_airborne() + + assert.are_equal(horizontal_dirs.left, pc.horizontal_dir) + end) + + it('should set horizontal direction to intended motion direction: right', function () + pc.horizontal_dir = horizontal_dirs.left + pc.velocity.x = 4 + pc.move_intention.x = 1 + + pc:_update_platformer_motion_airborne() + + assert.are_equal(horizontal_dirs.right, pc.horizontal_dir) + end) + -- bugfix history: -- . it('should update position with air motion result position', function () @@ -3769,7 +3789,7 @@ describe('player_char', function () end) - describe('#solo render', function () + describe('render', function () local spr_data_render_stub @@ -3818,7 +3838,7 @@ describe('player_char', function () assert.spy(spr_data_render_stub).was_called_with(match.ref(pc_data.sonic_sprite_data["spin"]), vector(12, 8), true) end) - it('#solo (when character is airborne, facing right) should call render on sonic sprite data: spin with the character\'s position, not flipped x', function () + it('(when character is airborne, facing right) should call render on sonic sprite data: spin with the character\'s position, not flipped x', function () pc.motion_state = motion_states.airborne -- optional, just to be consistent with current_sprite pc.current_sprite = "spin" pc.position = vector(12, 8) From a8cda9811cee22a6dcb0a25e33f46cb37eb5e02c Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 19 Apr 2019 19:24:41 +0100 Subject: [PATCH 093/140] [README] Added note on modding sprites, improved copyright/trademark mention --- README.md | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 20b07574..43fbf368 100644 --- a/README.md +++ b/README.md @@ -117,9 +117,23 @@ Those tests need to be built with picotool and run with PICO-8. We recommend the * `./build.sh itestplayercharacter itest_light` * `./run.sh itestplayercharacter itest_light` -### New project +## Modding -If you use the scripts of this project to create a new game, in order to use build command *p8tool: edit data* you need to create a pico8 file at data/data.p8 first. To do this, open PICO-8, type *save data*, then copy the boilerplate file to data/data.p8. +You can modify the spritesheet used in the build pipeline by running the custom Sublime Text build command `p8tool: edit data`, or in the shell: `pico8 -run data/data.p8`. + +This will open the cartridge `data.p8` in PICO-8. This cartridge contains only assets, and no code at all. Make your changes, save the cartridge, then build the project to see your result. + +For fast iterations, you can also directly modify assets while running the built game, but remember your changes are temporary and will be overwritten by the next build. To preserve your changes, you must save the cartridge, open it and copy the data parts (`__gfx__`, `__gff__`, `__map__`, `__sfx__` and `__music__`) and replace them in `data.p8` manually. + +Alternatively, to edit the spritesheet in your favorite editor: + +1. Export it from PICO-8 with the PICO-8 command `export spritesheet.png` +2. Edit it in your favorite editor +3. Import it back to PICO-8 with the PICO-8 command `import spritesheet.png` + +## New project + +If you use the scripts of this project to create a new game, in order to use build command `p8tool: edit data` you need to create a pico8 file at data/data.p8 first. To do this, open PICO-8, type `save data`, then copy the boilerplate file to data/data.p8. ## Runtime third-party libraries @@ -165,6 +179,10 @@ The `npm` folder has its own MIT license because I adapted a script from the `lu ### Assets -Most assets are derivative works of Sonic the Hedgehog, SEGA, especially the Master System and Mega Drive games. They have been created, either manually or with a conversion tool, for demonstration purpose. BGMs have been converted from Master System midi rips to PICO-8 format with [midi2pico](https://github.com/gamax92/midi2pico). I only retain copyright for the manual work of adaptation. +Most assets are derivative works of Sonic the Hedgehog (SEGA), especially the Master System and Mega Drive games. They have been created, either manually or with a conversion tool, for demonstration purpose. BGMs have been converted from Master System midi rips to PICO-8 format with [midi2pico](https://github.com/gamax92/midi2pico), an automated music format converter. + +SEGA owns the Sonic the Hedgehog trademark and retains all copyrights on the original assets. + +I only retain copyright for the manual work of adaptation (i.e. pixel art, but not music). Assets that are not derivative works are under CC BY 4.0. From a28555de7c7b12b6dadf76ff6814dbfdbc32412c Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 19 Apr 2019 19:25:23 +0100 Subject: [PATCH 094/140] [SPRITE] Fixed spin sprite gloss to match facing direction --- data/data.p8 | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/data/data.p8 b/data/data.p8 index b42bfbe7..598d24f5 100644 --- a/data/data.p8 +++ b/data/data.p8 @@ -21,10 +21,10 @@ eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeccccccee1eeeeecccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeceeccccccceeeecccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeccffccccceeeccc7cccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeecccfcc7ccceecc777ccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeecccccc770cceeccc777cccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeecee1cc770ceeeccc7cccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeccffccccceeeccccccc7cceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeecccfcc7ccceecccccc777ccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeecccccc770cceeccccccc777cceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeecee1cc770ceeeccccccc7cccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeccccf77f0eecccccccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeececccffeeeecccccccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeee1fccf7eeecccccccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee @@ -83,14 +83,14 @@ eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee010101010101eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee01eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee01010101eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee01eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee01010101eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee01eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee01010101eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee01eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee From 45329ae25d2caf96120afb8643b8a1d24e0abfe0 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 19 Apr 2019 19:26:22 +0100 Subject: [PATCH 095/140] [TEST] Disable trace log in itest so Travis CI log is not spammed --- src/engine/test/integrationtest.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/test/integrationtest.lua b/src/engine/test/integrationtest.lua index 414857a5..fab255e5 100644 --- a/src/engine/test/integrationtest.lua +++ b/src/engine/test/integrationtest.lua @@ -252,7 +252,7 @@ function integration_test_runner:_initialize() logging.logger.active_categories["itest"] = true --#pico8]] - logging.logger.active_categories["trace"] = true + logging.logger.active_categories["trace"] = false --#endif self.initialized = true From d04db9f8bec7d4d0d5973730adf9e4957ef9587e Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 19 Apr 2019 19:49:06 +0100 Subject: [PATCH 096/140] [SPRITE] Reduced spin sprite size by 2 in both directions to match collider better Adapted pivot --- data/data.p8 | 24 ++++++++++++------------ src/game/data/playercharacter_data.lua | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/data/data.p8 b/data/data.p8 index 598d24f5..4af83cc1 100644 --- a/data/data.p8 +++ b/data/data.p8 @@ -19,18 +19,18 @@ eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeccccccee1eeeeecccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeceeccccccceeeecccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeccffccccceeeccccccc7cceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeecccfcc7ccceecccccc777ccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeecccccc770cceeccccccc777cceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeecee1cc770ceeeccccccc7cccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeccccf77f0eecccccccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeececccffeeeecccccccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeee1fccf7eeecccccccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeee777cf77eeecccccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeee7777c77eeeecccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeee77eceeeeeeecccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeccccccee1eeeeecccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeceeccccccceeecccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeccffccccceeeccccc7cceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeecccfcc7ccceeccccc777cceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeecccccc770cceecccccc7ccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeecee1cc770ceeecccccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeccccf77f0eecccccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeececccffeeeeecccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeee1fccf7eeeecccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeee777cf77eeeeecccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeee7777c77eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeee77eceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeececeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeee7e7eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeee0872088eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee diff --git a/src/game/data/playercharacter_data.lua b/src/game/data/playercharacter_data.lua index 5c21c909..20440cb5 100644 --- a/src/game/data/playercharacter_data.lua +++ b/src/game/data/playercharacter_data.lua @@ -70,7 +70,7 @@ return { -- stand right sonic_sprite_data = { idle = sprite_data(sprite_id_location(0, 2), tile_vector(2, 2), vector(12, 8)), - spin = sprite_data(sprite_id_location(2, 2), tile_vector(2, 2), vector(6, 6)), + spin = sprite_data(sprite_id_location(2, 2), tile_vector(2, 2), vector(5, 5)), } } From e96d5b21e4ea19319ab7cf041aeda27ac2e3cb9c Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 19 Apr 2019 20:33:30 +0100 Subject: [PATCH 097/140] [TEST] Removed old tests on struct equality, since it is now integrated in struct definition --- src/engine/core/datastruct.lua | 4 +++- src/engine/debug/codetuner.lua | 10 +++------- tests/utestcodetuner.lua | 18 ------------------ tests/utestintegrationtest.lua | 9 --------- tests/utestsprite.lua | 16 ---------------- tests/utestui.lua | 14 -------------- 6 files changed, 6 insertions(+), 65 deletions(-) diff --git a/src/engine/core/datastruct.lua b/src/engine/core/datastruct.lua index 219be748..a099be35 100644 --- a/src/engine/core/datastruct.lua +++ b/src/engine/core/datastruct.lua @@ -1,5 +1,7 @@ -- circular buffer implementation. useful to represent fixed size queue --- adapted from https://gist.github.com/johndgiese/3e1c6d6e0535d4536692 +-- adapted from https://gist.github.com/johndgiese/3e1c6d6e0535d4536692 +-- we are defining our own __eq, don't need copy and may contain reference +-- to fully-fledged objects, so we don't use a struct circular_buffer = new_class() -- params diff --git a/src/engine/debug/codetuner.lua b/src/engine/debug/codetuner.lua index 3e5a9d11..d95a9cd9 100644 --- a/src/engine/debug/codetuner.lua +++ b/src/engine/debug/codetuner.lua @@ -39,10 +39,11 @@ function codetuner.below(w, dist) return w.x, w.y+w.h+(dist or 2) end --- tuned variable class, represents a variable to tune in the code tuner +-- todo: use this struct for easier variable handling +-- tuned variable struct, represents a variable to tune in the code tuner -- currently unused, it will replace the free vars in codetuner.tuned_vars -- to provide better information (type, range, default value) -codetuner.tuned_variable = new_class() +codetuner.tuned_variable = new_struct() -- name string tuned variable identifier -- default_value any value used for tuned variable if codetuner is inactive @@ -56,11 +57,6 @@ function codetuner.tuned_variable:_tostring(name, default_value) return "tuned_variable \""..self.name.."\" (default: "..self.default_value..")" end --- return true if both tuned vars have the same name *and* default -function codetuner.tuned_variable:__eq(other) - return self.name == other.name and self.default_value == other.default_value -end - -- return a function callback for the spinner, that sets the corresponding tuned variable -- exposed via codetuner for testing function codetuner:get_spinner_callback(tuned_var_name) diff --git a/tests/utestcodetuner.lua b/tests/utestcodetuner.lua index a6483c77..b56bfe4c 100644 --- a/tests/utestcodetuner.lua +++ b/tests/utestcodetuner.lua @@ -50,24 +50,6 @@ describe('codetuner', function () end) end) - describe('__eq', function () - it('should return true if tile vectors have the same name and default value', function () - local tuned_var1 = codetuner.tuned_variable("offset", 6) - local tuned_var2 = codetuner.tuned_variable("offset", 6) - assert.is_true(tuned_var2:__eq(tuned_var1)) - end) - it('should return false if tile vectors have different names', function () - local tuned_var1 = codetuner.tuned_variable("offset", 6) - local tuned_var2 = codetuner.tuned_variable("offset2", 6) - assert.is_false(tuned_var2:__eq(tuned_var1)) - end) - it('should return false if tile vectors have different default values', function () - local tuned_var1 = codetuner.tuned_variable("offset", 6) - local tuned_var2 = codetuner.tuned_variable("offset", 12) - assert.is_false(tuned_var2:__eq(tuned_var1)) - end) - end) - end) describe('(codetuner active)', function () diff --git a/tests/utestintegrationtest.lua b/tests/utestintegrationtest.lua index 03900e05..c80ba2de 100644 --- a/tests/utestintegrationtest.lua +++ b/tests/utestintegrationtest.lua @@ -1069,15 +1069,6 @@ describe('time_trigger', function () end) end) - describe('__eq', function () - it('should return true if times are equal', function () - assert.is_true(time_trigger(2.0) == time_trigger(2.0)) - end) - it('should return false if times are not equal', function () - assert.is_true(time_trigger(1.0) ~= time_trigger(2.0)) - end) - end) - describe('_check', function () it('should return true if elapsed time is equal to {self.frames}', function () assert.is_true(time_trigger(2.0):_check(120)) diff --git a/tests/utestsprite.lua b/tests/utestsprite.lua index 8dc30875..b429a105 100644 --- a/tests/utestsprite.lua +++ b/tests/utestsprite.lua @@ -39,22 +39,6 @@ describe('sprite', function () end) - describe('__eq', function () - - local spr_data = sprite_data(sprite_id_location(1, 3), tile_vector(2, 3), vector(2, 4)) - local spr_data2 = sprite_data(sprite_id_location(1, 3), tile_vector(2, 3), vector(2, 4)) - local spr_data3 = sprite_data(sprite_id_location(1, 5), tile_vector(2, 3), vector(2, 4)) - - it('sprite_data((1 3) ...) == sprite_data((1 3) ...)', function () - assert.are_equal(spr_data2, spr_data) - end) - - it('sprite_data((1 3) ...) == sprite_data((1 5), ...)', function () - assert.are_not_equal(spr_data3, spr_data) - end) - - end) - describe('render', function () local spr_data = sprite_data(sprite_id_location(1, 3), tile_vector(2, 3), vector(11, 10)) diff --git a/tests/utestui.lua b/tests/utestui.lua index a30e8790..9cf87f68 100644 --- a/tests/utestui.lua +++ b/tests/utestui.lua @@ -27,20 +27,6 @@ describe('ui', function () end) - describe('__eq', function () - - it('should return true for label with same text and position', function () - assert.are_equal(label("good", vector(22, 62), colors.orange), label("good", vector(22, 62), colors.orange)) - end) - - it('should return false for label with different text or position', function () - assert.are_not_equal(label("good", vector(22, 62), colors.orange), label("bad", vector(22, 62), colors.orange)) - assert.are_not_equal(label("good", vector(23, 62), colors.orange), label("good", vector(22, 62), colors.orange)) - assert.are_not_equal(label("good", vector(23, 62), colors.orange), label("good", vector(23, 62), colors.peach)) - end) - - end) - end) describe('overlay', function () From 471153f4c90fa5bf4ab738894947dd9adf53deb0 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 19 Apr 2019 20:47:59 +0100 Subject: [PATCH 098/140] [SPRITE] Animated sprite stub --- src/engine/render/animated_sprite_data.lua | 21 +++++++++++++++ src/engine/render/sprite.lua | 5 ++-- tests/utestanimated_sprite.lua | 31 ++++++++++++++++++++++ tests/utestsprite.lua | 2 +- 4 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 src/engine/render/animated_sprite_data.lua create mode 100644 tests/utestanimated_sprite.lua diff --git a/src/engine/render/animated_sprite_data.lua b/src/engine/render/animated_sprite_data.lua new file mode 100644 index 00000000..7c711ab5 --- /dev/null +++ b/src/engine/render/animated_sprite_data.lua @@ -0,0 +1,21 @@ +require("engine/render/sprite") + +-- struct containing data on animated sprite: sprite references and timing +animated_sprite_data = new_struct() + +-- sprites {sprite_data} sequence of sprites to play in order +-- step_frames int how long a single sprite (step) is displayed, in frames +-- looping bool true iff animation should loop +function animated_sprite_data:_init(sprites, step_frames, looping) + self.sprites = sprites + self.step_frames = step_frames + self.looping = looping or false +end + +--#if log +function animated_sprite_data:_tostring() + return "animated_sprite_data("..joinstr(", ", "["..#self.sprites.." sprites]", self.step_frames, self.looping)..")" +end +--#endif + +return animated_sprite_data diff --git a/src/engine/render/sprite.lua b/src/engine/render/sprite.lua index a34bbb79..7343739a 100644 --- a/src/engine/render/sprite.lua +++ b/src/engine/render/sprite.lua @@ -2,7 +2,7 @@ require("engine/application/constants") require("engine/core/math") require("engine/render/color") --- sprite class +-- sprite struct sprite_data = new_struct() -- id_loc sprite_id_location sprite location on the spritesheet @@ -16,8 +16,7 @@ end --#if log function sprite_data:_tostring() - return "sprite_data("..(self.id_loc:_tostring())..", "..(self.span:_tostring())..", ".. - (self.pivot:_tostring())..")" + return "sprite_data("..joinstr(", ", self.id_loc, self.span, self.pivot)..")" end --#endif diff --git a/tests/utestanimated_sprite.lua b/tests/utestanimated_sprite.lua new file mode 100644 index 00000000..34c76f85 --- /dev/null +++ b/tests/utestanimated_sprite.lua @@ -0,0 +1,31 @@ +require("bustedhelper") +require("engine/render/sprite") +animated_sprite_data = require("engine/render/animated_sprite_data") + +describe('animated_sprite', function () + + local spr_data1 = sprite_data(sprite_id_location(1, 0), tile_vector(1, 2), vector(4, 6)) + local spr_data2 = sprite_data(sprite_id_location(2, 0), tile_vector(1, 2), vector(4, 6)) + + describe('_init', function () + it('should init a sprite with all parameters', function () + local anim_spr_data = animated_sprite_data({spr_data1, spr_data2}, 2, true) + assert.are_same({{spr_data1, spr_data2}, 2, true}, + {anim_spr_data.sprites, anim_spr_data.step_frames, anim_spr_data.looping}) + end) + it('should init a sprite with looping false by default', function () + local anim_spr_data = animated_sprite_data({spr_data1, spr_data2}, 2) + assert.is_false(anim_spr_data.looping) + end) + end) + + describe('_tostring', function () + + it('sprite_data((1, 3) ...) => "sprite_data(sprite_id_location(1, 3) ...)"', function () + local anim_spr_data = animated_sprite_data({spr_data1, spr_data2}, 2, true) + assert.are_equal("animated_sprite_data([2 sprites], 2, true)", anim_spr_data:_tostring()) + end) + + end) + +end) diff --git a/tests/utestsprite.lua b/tests/utestsprite.lua index b429a105..1e4e2d68 100644 --- a/tests/utestsprite.lua +++ b/tests/utestsprite.lua @@ -1,5 +1,5 @@ require("bustedhelper") -sprite = require("engine/render/sprite") +require("engine/render/sprite") describe('sprite', function () From febd168cce0348a312d6176a0f185e18a50b1e31 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 19 Apr 2019 20:51:48 +0100 Subject: [PATCH 099/140] [PROJECT] Renamed sprite module -> sprite_data returning sprite_data struct for consistency --- src/engine/render/animated_sprite_data.lua | 2 +- src/engine/render/{sprite.lua => sprite_data.lua} | 2 ++ src/game/ingame/playercharacter.lua | 1 - src/game/resources/visual.lua | 2 +- tests/utestanimated_sprite.lua | 2 +- tests/{utestsprite.lua => utestsprite_data.lua} | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) rename src/engine/render/{sprite.lua => sprite_data.lua} (98%) rename tests/{utestsprite.lua => utestsprite_data.lua} (98%) diff --git a/src/engine/render/animated_sprite_data.lua b/src/engine/render/animated_sprite_data.lua index 7c711ab5..66fd8be7 100644 --- a/src/engine/render/animated_sprite_data.lua +++ b/src/engine/render/animated_sprite_data.lua @@ -1,4 +1,4 @@ -require("engine/render/sprite") +local sprite_data = require("engine/render/sprite_data") -- struct containing data on animated sprite: sprite references and timing animated_sprite_data = new_struct() diff --git a/src/engine/render/sprite.lua b/src/engine/render/sprite_data.lua similarity index 98% rename from src/engine/render/sprite.lua rename to src/engine/render/sprite_data.lua index 7343739a..f0436e08 100644 --- a/src/engine/render/sprite.lua +++ b/src/engine/render/sprite_data.lua @@ -48,3 +48,5 @@ function sprite_data:render(position, flip_x, flip_y) self.span.i, self.span.j, flip_x, flip_y) end + +return sprite_data diff --git a/src/game/ingame/playercharacter.lua b/src/game/ingame/playercharacter.lua index 4a07cffc..e4da2088 100644 --- a/src/game/ingame/playercharacter.lua +++ b/src/game/ingame/playercharacter.lua @@ -2,7 +2,6 @@ require("engine/application/constants") require("engine/core/class") require("engine/core/helper") require("engine/core/math") -require("engine/render/sprite") local input = require("engine/input/input") local collision = require("engine/physics/collision") local world = require("engine/physics/world") diff --git a/src/game/resources/visual.lua b/src/game/resources/visual.lua index 13ef15d1..b8ad0526 100644 --- a/src/game/resources/visual.lua +++ b/src/game/resources/visual.lua @@ -1,4 +1,4 @@ -require("engine/render/sprite") +local sprite_data = require("engine/render/sprite_data") local visual = {} diff --git a/tests/utestanimated_sprite.lua b/tests/utestanimated_sprite.lua index 34c76f85..4bfab265 100644 --- a/tests/utestanimated_sprite.lua +++ b/tests/utestanimated_sprite.lua @@ -1,5 +1,5 @@ require("bustedhelper") -require("engine/render/sprite") +local sprite_data = require("engine/render/sprite_data") animated_sprite_data = require("engine/render/animated_sprite_data") describe('animated_sprite', function () diff --git a/tests/utestsprite.lua b/tests/utestsprite_data.lua similarity index 98% rename from tests/utestsprite.lua rename to tests/utestsprite_data.lua index 1e4e2d68..8b205e29 100644 --- a/tests/utestsprite.lua +++ b/tests/utestsprite_data.lua @@ -1,5 +1,5 @@ require("bustedhelper") -require("engine/render/sprite") +local sprite_data = require("engine/render/sprite_data") describe('sprite', function () From 164882e9cdfae4102ade5e23cbe3154c7910ea02 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 19 Apr 2019 21:10:40 +0100 Subject: [PATCH 100/140] [SPRITE] Added stateful animated sprite, fixed some test descriptions --- src/engine/render/animated_sprite.lua | 20 ++++++++++++++ src/engine/render/animated_sprite_data.lua | 2 -- tests/utestanimated_sprite.lua | 18 ++++++++----- tests/utestanimated_sprite_data.lua | 31 ++++++++++++++++++++++ 4 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 src/engine/render/animated_sprite.lua create mode 100644 tests/utestanimated_sprite_data.lua diff --git a/src/engine/render/animated_sprite.lua b/src/engine/render/animated_sprite.lua new file mode 100644 index 00000000..ff13e137 --- /dev/null +++ b/src/engine/render/animated_sprite.lua @@ -0,0 +1,20 @@ +-- stateful animated sprite compounded of an animated_sprite_data and an animation state +-- can be used as component of an object rendered with some animation +animated_sprite = new_class() + +-- data animated_sprite_data associated data +-- current_step int index of the current sprite shown in the animation sequence, starting at 1 +-- local_frame int current frame inside the current step, starting at 0 +function animated_sprite:_init(data) + self.data = data + self.current_step = 1 + self.local_frame = 0 +end + +--#if log +function animated_sprite:_tostring() + return "animated_sprite("..joinstr(", ", self.data, self.current_step, self.local_frame)..")" +end +--#endif + +return animated_sprite diff --git a/src/engine/render/animated_sprite_data.lua b/src/engine/render/animated_sprite_data.lua index 66fd8be7..5c31b74d 100644 --- a/src/engine/render/animated_sprite_data.lua +++ b/src/engine/render/animated_sprite_data.lua @@ -1,5 +1,3 @@ -local sprite_data = require("engine/render/sprite_data") - -- struct containing data on animated sprite: sprite references and timing animated_sprite_data = new_struct() diff --git a/tests/utestanimated_sprite.lua b/tests/utestanimated_sprite.lua index 4bfab265..0dd546ca 100644 --- a/tests/utestanimated_sprite.lua +++ b/tests/utestanimated_sprite.lua @@ -1,17 +1,19 @@ require("bustedhelper") local sprite_data = require("engine/render/sprite_data") -animated_sprite_data = require("engine/render/animated_sprite_data") +local animated_sprite_data = require("engine/render/animated_sprite_data") +local animated_sprite = require("engine/render/animated_sprite") describe('animated_sprite', function () local spr_data1 = sprite_data(sprite_id_location(1, 0), tile_vector(1, 2), vector(4, 6)) local spr_data2 = sprite_data(sprite_id_location(2, 0), tile_vector(1, 2), vector(4, 6)) + local anim_spr_data = animated_sprite_data({spr_data1, spr_data2}, 10, true) describe('_init', function () it('should init a sprite with all parameters', function () - local anim_spr_data = animated_sprite_data({spr_data1, spr_data2}, 2, true) - assert.are_same({{spr_data1, spr_data2}, 2, true}, - {anim_spr_data.sprites, anim_spr_data.step_frames, anim_spr_data.looping}) + local anim_spr = animated_sprite(anim_spr_data) + assert.are_same({anim_spr_data, 1, 0}, + {anim_spr.data, anim_spr.current_step, anim_spr.local_frame}) end) it('should init a sprite with looping false by default', function () local anim_spr_data = animated_sprite_data({spr_data1, spr_data2}, 2) @@ -21,9 +23,11 @@ describe('animated_sprite', function () describe('_tostring', function () - it('sprite_data((1, 3) ...) => "sprite_data(sprite_id_location(1, 3) ...)"', function () - local anim_spr_data = animated_sprite_data({spr_data1, spr_data2}, 2, true) - assert.are_equal("animated_sprite_data([2 sprites], 2, true)", anim_spr_data:_tostring()) + it('should return a string describing data, current step and local frame', function () + local anim_spr = animated_sprite(anim_spr_data) + anim_spr.current_step = 2 + anim_spr.local_frame = 5 + assert.are_equal("animated_sprite(animated_sprite_data([2 sprites], 10, true), 2, 5)", anim_spr:_tostring()) end) end) diff --git a/tests/utestanimated_sprite_data.lua b/tests/utestanimated_sprite_data.lua new file mode 100644 index 00000000..2647da2c --- /dev/null +++ b/tests/utestanimated_sprite_data.lua @@ -0,0 +1,31 @@ +require("bustedhelper") +local sprite_data = require("engine/render/sprite_data") +local animated_sprite_data = require("engine/render/animated_sprite_data") + +describe('animated_sprite_data', function () + + local spr_data1 = sprite_data(sprite_id_location(1, 0), tile_vector(1, 2), vector(4, 6)) + local spr_data2 = sprite_data(sprite_id_location(2, 0), tile_vector(1, 2), vector(4, 6)) + + describe('_init', function () + it('should init a sprite with all parameters', function () + local anim_spr_data = animated_sprite_data({spr_data1, spr_data2}, 2, true) + assert.are_same({{spr_data1, spr_data2}, 2, true}, + {anim_spr_data.sprites, anim_spr_data.step_frames, anim_spr_data.looping}) + end) + it('should init a sprite with looping false by default', function () + local anim_spr_data = animated_sprite_data({spr_data1, spr_data2}, 2) + assert.is_false(anim_spr_data.looping) + end) + end) + + describe('_tostring', function () + + it('should return a string describing the number of sprites, step duration in frames and whether it loops', function () + local anim_spr_data = animated_sprite_data({spr_data1, spr_data2}, 2, true) + assert.are_equal("animated_sprite_data([2 sprites], 2, true)", anim_spr_data:_tostring()) + end) + + end) + +end) From 39e073d3975ba6e27e210fd56c0f00ad62a3c3e2 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 19 Apr 2019 21:55:46 +0100 Subject: [PATCH 101/140] [SPRITE] Animated sprite system with optional looping --- src/engine/render/animated_sprite.lua | 45 ++++++++++- src/engine/render/animated_sprite_data.lua | 2 + tests/utestanimated_sprite.lua | 93 ++++++++++++++++++++-- tests/utestsprite_data.lua | 5 ++ 4 files changed, 133 insertions(+), 12 deletions(-) diff --git a/src/engine/render/animated_sprite.lua b/src/engine/render/animated_sprite.lua index ff13e137..79e248c8 100644 --- a/src/engine/render/animated_sprite.lua +++ b/src/engine/render/animated_sprite.lua @@ -1,20 +1,57 @@ -- stateful animated sprite compounded of an animated_sprite_data and an animation state -- can be used as component of an object rendered with some animation +-- data can be swept for another data to easily switch sprite animation animated_sprite = new_class() --- data animated_sprite_data associated data --- current_step int index of the current sprite shown in the animation sequence, starting at 1 --- local_frame int current frame inside the current step, starting at 0 +-- data animated_sprite_data associated data +-- playing bool is the animation playing? +-- current_step int index of the current sprite shown in the animation sequence, starting at 1 +-- local_frame int current frame inside the current step, starting at 0 function animated_sprite:_init(data) self.data = data + self.playing = true self.current_step = 1 self.local_frame = 0 end --#if log function animated_sprite:_tostring() - return "animated_sprite("..joinstr(", ", self.data, self.current_step, self.local_frame)..")" + return "animated_sprite("..joinstr(", ", self.data, self.playing, self.current_step, self.local_frame)..")" end --#endif +-- update the sprite animation +-- this must be called once per update at 60 fps, before the render phase +function animated_sprite:update() + local next_local_frame = self.local_frame + 1 + -- self.local_frame = self.local_frame + 1 + -- if self.local_frame >= self.data.step_frames then + if next_local_frame >= self.data.step_frames then + local next_step = self.current_step + 1 + -- self.current_step = self.current_step + 1 + -- if self.current_step > #self.data.sprites then + if next_step > #self.data.sprites then + if self.data.looping then + self.current_step = 1 + self.local_frame = 0 + else + self.playing = false + end + else + self.current_step = next_step + self.local_frame = 0 + end + else + self.local_frame = next_local_frame + end +end + +-- render the current sprite data with passed arguments +function animated_sprite:render(position, flip_x, flip_y) + local current_sprite_data = self.data.sprites[self.current_step] + current_sprite_data:render(position, flip_x, flip_y) +end + + + return animated_sprite diff --git a/src/engine/render/animated_sprite_data.lua b/src/engine/render/animated_sprite_data.lua index 5c31b74d..91c2e2ed 100644 --- a/src/engine/render/animated_sprite_data.lua +++ b/src/engine/render/animated_sprite_data.lua @@ -5,6 +5,8 @@ animated_sprite_data = new_struct() -- step_frames int how long a single sprite (step) is displayed, in frames -- looping bool true iff animation should loop function animated_sprite_data:_init(sprites, step_frames, looping) + assert(#sprites > 0) + assert(step_frames > 0) self.sprites = sprites self.step_frames = step_frames self.looping = looping or false diff --git a/tests/utestanimated_sprite.lua b/tests/utestanimated_sprite.lua index 0dd546ca..b5944542 100644 --- a/tests/utestanimated_sprite.lua +++ b/tests/utestanimated_sprite.lua @@ -8,16 +8,13 @@ describe('animated_sprite', function () local spr_data1 = sprite_data(sprite_id_location(1, 0), tile_vector(1, 2), vector(4, 6)) local spr_data2 = sprite_data(sprite_id_location(2, 0), tile_vector(1, 2), vector(4, 6)) local anim_spr_data = animated_sprite_data({spr_data1, spr_data2}, 10, true) + local anim_spr_data_once = animated_sprite_data({spr_data1, spr_data2}, 10, false) describe('_init', function () - it('should init a sprite with all parameters', function () + it('should init a sprite with data, automatically playing from step 1, frame 0', function () local anim_spr = animated_sprite(anim_spr_data) - assert.are_same({anim_spr_data, 1, 0}, - {anim_spr.data, anim_spr.current_step, anim_spr.local_frame}) - end) - it('should init a sprite with looping false by default', function () - local anim_spr_data = animated_sprite_data({spr_data1, spr_data2}, 2) - assert.is_false(anim_spr_data.looping) + assert.are_same({anim_spr_data, true, 1, 0}, + {anim_spr.data, anim_spr.playing, anim_spr.current_step, anim_spr.local_frame}) end) end) @@ -27,7 +24,87 @@ describe('animated_sprite', function () local anim_spr = animated_sprite(anim_spr_data) anim_spr.current_step = 2 anim_spr.local_frame = 5 - assert.are_equal("animated_sprite(animated_sprite_data([2 sprites], 10, true), 2, 5)", anim_spr:_tostring()) + assert.are_equal("animated_sprite(animated_sprite_data([2 sprites], 10, true), true, 2, 5)", anim_spr:_tostring()) + end) + + end) + + describe('update', function () + + it('should increment the local frame if under the animation step_frames', function () + local anim_spr = animated_sprite(anim_spr_data) + anim_spr.playing = true + anim_spr.current_step = 1 + anim_spr.local_frame = 8 -- data.step_frames is 10, so frames play from 0 to 9 + + anim_spr:update() + + assert.are_same({1, 9}, + {anim_spr.current_step, anim_spr.local_frame}) + end) + + it('should reset local frame and enter next step when step_frames is reached', function () + local anim_spr = animated_sprite(anim_spr_data) + anim_spr.playing = true + anim_spr.current_step = 1 + anim_spr.local_frame = 9 -- data.step_frames - 1 + + anim_spr:update() + + assert.are_same({2, 0}, + {anim_spr.current_step, anim_spr.local_frame}) + end) + + it('should continue playing from the start when looping and end of animation has been reached', function () + local anim_spr = animated_sprite(anim_spr_data) + anim_spr.playing = true + anim_spr.current_step = 2 + anim_spr.local_frame = 9 -- data.step_frames - 1 + + anim_spr:update() + + assert.are_same({true, 1, 0}, + {anim_spr.playing, anim_spr.current_step, anim_spr.local_frame}) + end) + + it('should stop playing when not looping and end of animation has been reached', function () + local anim_spr = animated_sprite(anim_spr_data_once) + anim_spr.playing = true + anim_spr.current_step = 2 + anim_spr.local_frame = 9 -- data.step_frames - 1 + + anim_spr:update() + + assert.are_same({false, 2, 9}, + {anim_spr.playing, anim_spr.current_step, anim_spr.local_frame}) + end) + + end) + + describe('render', function () + + local anim_spr = animated_sprite(anim_spr_data) + + setup(function () + sprite_data_render = stub(sprite_data, "render") + + anim_spr.current_step = 2 + anim_spr.local_frame = 5 + end) + + teardown(function () + sprite_data_render:revert() + end) + + after_each(function () + sprite_data_render:clear() + end) + + it('should render the sprite from the id location, at the draw position minus pivot, with correct span when not flipping', function () + anim_spr:render(vector(41, 80), false, true) + + assert.spy(sprite_data_render).was_called(1) + assert.spy(sprite_data_render).was_called_with(match.ref(spr_data2), vector(41, 80), false, true) end) end) diff --git a/tests/utestsprite_data.lua b/tests/utestsprite_data.lua index 8b205e29..684a2c8d 100644 --- a/tests/utestsprite_data.lua +++ b/tests/utestsprite_data.lua @@ -59,12 +59,14 @@ describe('sprite', function () it('should render the sprite from the id location, at the draw position minus pivot, with correct span when not flipping', function () spr_data:render(vector(41, 80), false, false) + assert.spy(spr_stub).was_called(1) assert.spy(spr_stub).was_called_with(49, 30, 70, 2, 3, false, false) end) it('should render the sprite from the id location, at the draw position minus pivot itself flipped on x, with correct span when flipping x', function () spr_data:render(vector(41, 80), true, false) + assert.spy(spr_stub).was_called(1) -- flip pivot (11, 10) around center x axis which is at 8 * span.x / 2 = 8 -> flipped pivot (5, 10) assert.spy(spr_stub).was_called_with(49, 36, 70, 2, 3, true, false) @@ -72,6 +74,7 @@ describe('sprite', function () it('should render the sprite from the id location, at the draw position minus pivot itself flipped on y, with correct span when flipping y', function () spr_data:render(vector(41, 80), false, true) + assert.spy(spr_stub).was_called(1) -- flip pivot (11, 10) around center y axis which is at 8 * span.y / 2 = 12 -> flipped pivot (11, 14) assert.spy(spr_stub).was_called_with(49, 30, 66, 2, 3, false, true) @@ -79,12 +82,14 @@ describe('sprite', function () it('should render the sprite from the id location, at the draw position minus pivot itself flipped on x and y, with correct span when flipping x and y', function () spr_data:render(vector(41, 80), true, true) + assert.spy(spr_stub).was_called(1) assert.spy(spr_stub).was_called_with(49, 36, 66, 2, 3, true, true) end) it('should render the sprite from the id location, at the draw position minus pivot located at center, with correct span when flipping x and y', function () spr_data2:render(vector(8, 4), true, true) + assert.spy(spr_stub).was_called(1) -- pivot is already at center, so flip has no effect on it -- and since position == pivot, it draws at the origin From 5ae5d7871bcdedde9ed7c48aee28f87602abf84b Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 19 Apr 2019 22:03:38 +0100 Subject: [PATCH 102/140] [SPRITE] Added missing require in playercharacter_data --- src/engine/render/animated_sprite.lua | 27 +++++++++++++------------- src/game/data/playercharacter_data.lua | 2 ++ tests/utestanimated_sprite.lua | 2 +- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/engine/render/animated_sprite.lua b/src/engine/render/animated_sprite.lua index 79e248c8..67feefce 100644 --- a/src/engine/render/animated_sprite.lua +++ b/src/engine/render/animated_sprite.lua @@ -23,26 +23,27 @@ end -- update the sprite animation -- this must be called once per update at 60 fps, before the render phase function animated_sprite:update() - local next_local_frame = self.local_frame + 1 - -- self.local_frame = self.local_frame + 1 - -- if self.local_frame >= self.data.step_frames then - if next_local_frame >= self.data.step_frames then - local next_step = self.current_step + 1 - -- self.current_step = self.current_step + 1 - -- if self.current_step > #self.data.sprites then - if next_step > #self.data.sprites then + -- check if we have reached the end of this step + if self.local_frame + 1 < self.data.step_frames then + -- keep same sprite and increment local frame counter + self.local_frame = self.local_frame + 1 + else + -- end of step reached, check if there is another sprite afterward + if self.current_step < #self.data.sprites then + -- show next sprite and reset local frame counter + self.current_step = self.current_step + 1 + self.local_frame = 0 + else + -- end of last step reached, should we loop? if self.data.looping then + -- continue playing from start self.current_step = 1 self.local_frame = 0 else + -- stop playing self.playing = false end - else - self.current_step = next_step - self.local_frame = 0 end - else - self.local_frame = next_local_frame end end diff --git a/src/game/data/playercharacter_data.lua b/src/game/data/playercharacter_data.lua index 20440cb5..de6d2a89 100644 --- a/src/game/data/playercharacter_data.lua +++ b/src/game/data/playercharacter_data.lua @@ -1,3 +1,5 @@ +local sprite_data = require("engine/render/sprite_data") + return { -- platformer motion diff --git a/tests/utestanimated_sprite.lua b/tests/utestanimated_sprite.lua index b5944542..1b085512 100644 --- a/tests/utestanimated_sprite.lua +++ b/tests/utestanimated_sprite.lua @@ -11,7 +11,7 @@ describe('animated_sprite', function () local anim_spr_data_once = animated_sprite_data({spr_data1, spr_data2}, 10, false) describe('_init', function () - it('should init a sprite with data, automatically playing from step 1, frame 0', function () + it('should init an animated sprite with data, automatically playing from step 1, frame 0', function () local anim_spr = animated_sprite(anim_spr_data) assert.are_same({anim_spr_data, true, 1, 0}, {anim_spr.data, anim_spr.playing, anim_spr.current_step, anim_spr.local_frame}) From 701e5e014777f12f273bb2fdb933f9553fa077df Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 19 Apr 2019 22:13:46 +0100 Subject: [PATCH 103/140] [SPRITE] Added factory function animated_sprite_data.create for easier setup --- src/engine/render/animated_sprite_data.lua | 10 ++++++++++ tests/utestanimated_sprite_data.lua | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/src/engine/render/animated_sprite_data.lua b/src/engine/render/animated_sprite_data.lua index 91c2e2ed..26c4886f 100644 --- a/src/engine/render/animated_sprite_data.lua +++ b/src/engine/render/animated_sprite_data.lua @@ -12,6 +12,16 @@ function animated_sprite_data:_init(sprites, step_frames, looping) self.looping = looping or false end +-- factory function to create animated sprite data from a table +-- of sprite data, and a sequence of keys +function animated_sprite_data.create(sprite_data_table, sprite_keys, step_frames, looping) + local sprites = {} + for sprite_key in all(sprite_keys) do + add(sprites, sprite_data_table[sprite_key]) + end + return animated_sprite_data(sprites, step_frames, looping) +end + --#if log function animated_sprite_data:_tostring() return "animated_sprite_data("..joinstr(", ", "["..#self.sprites.." sprites]", self.step_frames, self.looping)..")" diff --git a/tests/utestanimated_sprite_data.lua b/tests/utestanimated_sprite_data.lua index 2647da2c..3c6aae78 100644 --- a/tests/utestanimated_sprite_data.lua +++ b/tests/utestanimated_sprite_data.lua @@ -6,6 +6,7 @@ describe('animated_sprite_data', function () local spr_data1 = sprite_data(sprite_id_location(1, 0), tile_vector(1, 2), vector(4, 6)) local spr_data2 = sprite_data(sprite_id_location(2, 0), tile_vector(1, 2), vector(4, 6)) + local spr_data_table = {step1 = spr_data1, step2 = spr_data2} describe('_init', function () it('should init a sprite with all parameters', function () @@ -19,6 +20,14 @@ describe('animated_sprite_data', function () end) end) + describe('create', function () + it('should create an animated sprite data from a sprite data table and sprite keys, with step_frames and looping', function () + local anim_spr_data = animated_sprite_data.create(spr_data_table, {"step1", "step2"}, 4, false) + assert.are_same({{spr_data1, spr_data2}, 4, false}, + {anim_spr_data.sprites, anim_spr_data.step_frames, anim_spr_data.looping}) + end) + end) + describe('_tostring', function () it('should return a string describing the number of sprites, step duration in frames and whether it loops', function () From 14076be78d212f540e49866f7871f0a7a0377fd1 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 19 Apr 2019 23:29:42 +0100 Subject: [PATCH 104/140] [BUILD] Added note on tonum fixed in pico8 0.1.12 --- src/engine/core/helper.lua | 2 ++ tests/pico8api.lua | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/src/engine/core/helper.lua b/src/engine/core/helper.lua index b3b344bf..b75acf19 100644 --- a/src/engine/core/helper.lua +++ b/src/engine/core/helper.lua @@ -123,6 +123,8 @@ end -- alternative to tonum that only works with strings (and numbers -- thanks to sub converting them implicitly) -- it fixes the 0x0000.0001 issue on negative number strings +-- UPDATE: expect native tonum to be fixed in 0.1.12 +-- https://www.lexaloffle.com/bbs/?pid=63583 function string_tonum(val) -- inspired by cheepicus's workaround in -- https://www.lexaloffle.com/bbs/?tid=3780 diff --git a/tests/pico8api.lua b/tests/pico8api.lua index b1a2c1bb..2e30ad48 100644 --- a/tests/pico8api.lua +++ b/tests/pico8api.lua @@ -117,6 +117,8 @@ end -- https://www.lexaloffle.com/bbs/?tid=3780 -- for this reason, we recommend not using tonum in actual pico8 code -- and to use helper's string_tonum instead +-- UPDATE: expect native tonum to be fixed in 0.1.12 +-- https://www.lexaloffle.com/bbs/?pid=63583 function tonum(val) return tonumber(val) -- not a direct assignment to prevent usage of the radix argument end @@ -456,6 +458,11 @@ function rotr(x, y) end function time() + -- starting pico8 0.1.12, time() returns time in seconds, + -- dividing by the appropriate fps (30 or 60 if using _update/_update60 resp.) + -- in this engine, we only use 60 fps updates, so we just hardcoded the result + -- note that _draw may still be called at 30fps, so using time() in draw would + -- give different results in busted utests return pico8.frames/60 end t=time From 9d748440bd79f273e3f75bce26bcc183143e8d9a Mon Sep 17 00:00:00 2001 From: hsandt Date: Sat, 20 Apr 2019 02:47:54 +0100 Subject: [PATCH 105/140] [LOG] Added sorted_keys parameter to dump/nice_dump to allow deterministic strings when keys are comparable --- src/engine/debug/logging.lua | 96 +++++++++++++++++++++++++++++++----- tests/utestlogging.lua | 43 +++++++++++++--- 2 files changed, 119 insertions(+), 20 deletions(-) diff --git a/src/engine/debug/logging.lua b/src/engine/debug/logging.lua index b946198c..b1c28c73 100644 --- a/src/engine/debug/logging.lua +++ b/src/engine/debug/logging.lua @@ -152,17 +152,86 @@ function err(content, category) logger:_generic_log(logging.level.error, category, content) end --- return a precise variable content, including table entries --- for sequence containing nils, nil is not shown but nil's index will be skipped --- if as_key is true and t is not a string, surround it with [] --- by default table recursion will stop at a call depth of logger.dump_max_recursion_level --- however, you can pass a custom number of remaining levels to see more --- if use_tostring is true, use any implemented _tostring method for tables --- you can also use dump on strings just to surround them with quotes -function dump(dumped_value, as_key, level, use_tostring) +--[[ +Ordered table iterator, allow to iterate on the natural order of the keys of a +table. + +This is only here to allow dump and nice_dump functions to be deterministic +by dumping elements with sorted keys (with an optional argument, as this is only possible +if the keys are comparable), hence easier to debug and test. + +Source: http://lua-users.org/wiki/SortedIteration +Modification: +- updated API for modern Lua (# instead of getn) +]] + +local function __genOrderedIndex( t ) + local orderedIndex = {} + for key in pairs(t) do + table.insert(orderedIndex, key) + end + table.sort(orderedIndex) + return orderedIndex +end + +local function orderedNext(t, state) + -- Equivalent of the next function, but returns the keys in the alphabetic + -- order. We use a temporary ordered key table that is stored in the + -- table being iterated. + + local key = nil + if state == nil then + -- the first time, generate the index + t.__orderedIndex = __genOrderedIndex(t) + key = t.__orderedIndex[1] + else + -- fetch the next value + for i = 1, #t.__orderedIndex do + if t.__orderedIndex[i] == state then + key = t.__orderedIndex[i+1] + end + end + end + + if key then + return key, t[key] + end + + -- no more value to return, cleanup + t.__orderedIndex = nil + return +end + +local function orderedPairs(t) + -- Equivalent of the pairs() function on tables. Allows to iterate + -- in order + return orderedNext, t, nil +end + + +--[[ +return a precise variable content, including table entries. + +for sequence containing nils, nil is not shown but nil's index will be skipped + +if as_key is true and t is not a string, surround it with [] + +by default, table recursion will stop at a call depth of logger.dump_max_recursion_level +however, you can pass a custom number of remaining levels to see more + +if use_tostring is true, use any implemented _tostring method for tables +you can also use dump on strings just to surround them with quotes + + +if sorted_keys is true, dump will try to sort the entries by key +only use this if you are sure that all the keys are comparable +(e.g. only numeric or only strings) +--]] +function dump(dumped_value, as_key, level, use_tostring, sorted_keys) as_key = as_key or false level = level or logger.dump_max_recursion_level use_tostring = use_tostring or false + sorted_keys = sorted_keys or false local repr @@ -172,9 +241,10 @@ function dump(dumped_value, as_key, level, use_tostring) else if level > 0 then local entries = {} - for key, value in pairs(dumped_value) do - local key_repr = dump(key, true, level - 1, use_tostring) - local value_repr = dump(value, false, level - 1, use_tostring) + local pairs_callback = sorted_keys and orderedPairs or pairs + for key, value in pairs_callback(dumped_value) do + local key_repr = dump(key, true, level - 1, use_tostring, sorted_keys) + local value_repr = dump(value, false, level - 1, use_tostring, sorted_keys) add(entries, key_repr.." = "..value_repr) end repr = "{"..joinstr_table(", ", entries).."}" @@ -199,8 +269,8 @@ function dump(dumped_value, as_key, level, use_tostring) end -- dump using _tostring method when possible -function nice_dump(value) - return dump(value, false, nil, true) +function nice_dump(value, sorted_keys) + return dump(value, false, nil, true, sorted_keys) end return logging diff --git a/tests/utestlogging.lua b/tests/utestlogging.lua index 6f7f1bb7..b4d0e5bb 100644 --- a/tests/utestlogging.lua +++ b/tests/utestlogging.lua @@ -828,7 +828,8 @@ describe('logging', function () describe('dump', function () - setup(function () + before_each(function () + -- no need to teardown, we are inside the 'logger' block that reinit logger after_each logger.dump_max_recursion_level = 2 end) @@ -853,7 +854,7 @@ describe('logging', function () assert.are_equal("56.2", dump(56.2)) end) - -- as key + -- as_key: used to mimic key representation in lua tables it('"string" => "string"', function () assert.are_equal("string", dump("string", true)) @@ -865,12 +866,14 @@ describe('logging', function () assert.are_equal("[56.2]", dump(56.2, true)) end) - -- tables + -- sequence of mixed values it('{1 nil "string"} => "{[1] = 1 [3] = "string"}"', function () assert.are_equal("{[1] = 1, [3] = \"string\"}", dump({1, nil, "string"})) end) + -- mix of non-comparable keys (cannot use sorted_keys here) + it('{[7] = 5 string = "other"} => "{[7] = 5, string = "other"}" or "{string = "other", [7] = 5}"', function () -- matchers are difficult to use outside of called_with, so we can't use match.any_of directly -- instead we test the alternative with a simple assert.is_true and a custom message to debug if false @@ -878,6 +881,8 @@ describe('logging', function () dump({[7] = 5, string = "other"}))) end) + -- mix of sequence of and indexed values + it('{5 "text" string = "other"} => "{[1] = 5 [2] = "text" string = "other"}', function () assert.are_equal("{[1] = 5, [2] = \"text\", string = \"other\"}", dump({5, "text", string = "other"})) end) @@ -886,6 +891,8 @@ describe('logging', function () assert.are_equal("{[1] = 2, mytable = {[1] = 1, [2] = 3, key = \"value\"}}", dump({2, mytable = {1, 3, key = "value"}})) end) + -- tables as values + it('{...} => "{{[1] = 1 [2] = 3 key = "value"} = 11}', function () assert.are_equal("{[{[1] = 1, [2] = 3, key = \"value\"}] = 11}", dump({[{1, 3, key = "value"}] = 11})) end) @@ -894,14 +901,18 @@ describe('logging', function () assert.are_equal("{[{[1] = 1, [2] = 3, key = \"value\"}] = {[1] = true, [2] = false}}", dump({[{1, 3, key = "value"}] = {true, false}})) end) - -- tables with tostring + -- sequences with table elements implementing _tostring it('{1, "text", vector(2, 4)} => "{[1] = 1, [2] = "text", [3] = vector(2, 4)}"', function () assert.are_equal("{[1] = 1, [2] = \"text\", [3] = vector(2, 4)}", dump({1, "text", vector(2, 4)}, false, 1, true)) end) - it('{1, "text", vector(2, 4)} => "{[1] = 1, [2] = "text", [3] = vector(2, 4)}"', function () - assert.are_equal("{[1] = 1, [2] = \"text\", [3] = vector(2, 4)}", nice_dump({1, "text", vector(2, 4)})) + -- non-sequence tables where ambiguous representation can be made deterministic with sorted_keys + -- as long as the keys are comparable + -- note that we are not testing __genOrderedIndex, orderedNext and orderedPairs, so we test them via dump with sorted_keys: true instead + + it('{f = 4, ["0"] = "a", b = -100} => "{[0] = "a", b = -100, f = 4}"', function () + assert.are_equal("{0 = \"a\", b = -100, f = 4}", dump({f = 4, ["0"] = "a", b = -100}, false, nil, true, --[[sorted_keys:]] true)) end) -- infinite recursion prevention @@ -930,6 +941,24 @@ describe('logging', function () end) - end) + describe('nice_dump', function () + + + before_each(function () + -- no need to teardown, we are inside the 'logger' block that reinit logger after_each + logger.dump_max_recursion_level = 2 + end) + + it('{1, "text", vector(2, 4)} => "{[1] = 1, [2] = "text", [3] = vector(2, 4)}"', function () + assert.are_equal("{[1] = 1, [2] = \"text\", [3] = vector(2, 4)}", nice_dump({1, "text", vector(2, 4)})) + end) + + it('{[10.5] = "b", [-22] = "a", [34.7] = "c"} => "{[-22] = "a", [10.5] = "b", [34.7] = "c"}"', function () + assert.are_equal("{[-22] = \"a\", [10.5] = \"b\", [34.7] = \"c\"}", nice_dump({[10.5] = "b", [-22] = "a", [34.7] = "c"}, true)) + end) + + end) + + end) -- logger end) From c9fd99d1ef4278a41b04a100a43ebafd4db39909 Mon Sep 17 00:00:00 2001 From: hsandt Date: Sat, 20 Apr 2019 02:48:48 +0100 Subject: [PATCH 106/140] [SPRITE] Use animation data table instead of single animation for animated_sprite --- src/engine/render/animated_sprite.lua | 83 ++++++++++++++++----------- tests/utestanimated_sprite.lua | 83 +++++++++++++++++++++------ 2 files changed, 117 insertions(+), 49 deletions(-) diff --git a/src/engine/render/animated_sprite.lua b/src/engine/render/animated_sprite.lua index 67feefce..c2b73155 100644 --- a/src/engine/render/animated_sprite.lua +++ b/src/engine/render/animated_sprite.lua @@ -1,47 +1,61 @@ --- stateful animated sprite compounded of an animated_sprite_data and an animation state --- can be used as component of an object rendered with some animation --- data can be swept for another data to easily switch sprite animation +-- stateful animated sprite compounded of an animated_sprite_data table and an animation state +-- it can be used as component of an object rendered with some animation +-- for objects with a single animation, use a data table containing a single element animated_sprite = new_class() --- data animated_sprite_data associated data --- playing bool is the animation playing? --- current_step int index of the current sprite shown in the animation sequence, starting at 1 --- local_frame int current frame inside the current step, starting at 0 -function animated_sprite:_init(data) - self.data = data - self.playing = true - self.current_step = 1 - self.local_frame = 0 +-- data_table {string: animated_sprite_data} table of animated sprite data, indexed by animation key (unique name) +-- playing bool is the animation playing? false if the animation has reached the end and stopped +-- current_anim_key string|nil key in data_table of animation currently played / paused, or nil if no animation is set at all +-- current_step int|nil index of the current sprite shown in the animation sequence, starting at 1, or nil if no animation is set at all +-- local_frame int|nil current frame inside the current step, starting at 0, or nil if no animation is set at all +function animated_sprite:_init(data_table) + self.data_table = data_table + self.playing = false + self.current_anim_key = nil -- the sprite will be invisible until we start an animation + self.current_step = nil + self.local_frame = nil end --#if log function animated_sprite:_tostring() - return "animated_sprite("..joinstr(", ", self.data, self.playing, self.current_step, self.local_frame)..")" + return "animated_sprite("..joinstr(", ", nice_dump(self.data_table, true), self.playing, self.current_anim_key, self.current_step, self.local_frame)..")" end --#endif +-- start animation with given key: string +function animated_sprite:play(anim_key) + assert(self.data_table[anim_key] ~= nil, "animated_sprite:play: self.data_table['"..anim_key.."'] doesn't exist") + self.playing = true + self.current_anim_key = anim_key + self.current_step = 1 + self.local_frame = 0 +end + -- update the sprite animation -- this must be called once per update at 60 fps, before the render phase function animated_sprite:update() - -- check if we have reached the end of this step - if self.local_frame + 1 < self.data.step_frames then - -- keep same sprite and increment local frame counter - self.local_frame = self.local_frame + 1 - else - -- end of step reached, check if there is another sprite afterward - if self.current_step < #self.data.sprites then - -- show next sprite and reset local frame counter - self.current_step = self.current_step + 1 - self.local_frame = 0 + if self.playing then + local anim_spr_data = self.data_table[self.current_anim_key] + -- check if we have reached the end of this step + if self.local_frame + 1 < anim_spr_data.step_frames then + -- keep same sprite and increment local frame counter + self.local_frame = self.local_frame + 1 else - -- end of last step reached, should we loop? - if self.data.looping then - -- continue playing from start - self.current_step = 1 + -- end of step reached, check if there is another sprite afterward + if self.current_step < #anim_spr_data.sprites then + -- show next sprite and reset local frame counter + self.current_step = self.current_step + 1 self.local_frame = 0 else - -- stop playing - self.playing = false + -- end of last step reached, should we loop? + if anim_spr_data.looping then + -- continue playing from start + self.current_step = 1 + self.local_frame = 0 + else + -- stop playing + self.playing = false + end end end end @@ -49,10 +63,13 @@ end -- render the current sprite data with passed arguments function animated_sprite:render(position, flip_x, flip_y) - local current_sprite_data = self.data.sprites[self.current_step] - current_sprite_data:render(position, flip_x, flip_y) + if self.current_anim_key then + -- an animation is set, render even if not playing since we want to show the last frame + -- of a non-looped anim as a still frame + local anim_spr_data = self.data_table[self.current_anim_key] + local current_sprite_data = anim_spr_data.sprites[self.current_step] + current_sprite_data:render(position, flip_x, flip_y) + end end - - return animated_sprite diff --git a/tests/utestanimated_sprite.lua b/tests/utestanimated_sprite.lua index 1b085512..815695e1 100644 --- a/tests/utestanimated_sprite.lua +++ b/tests/utestanimated_sprite.lua @@ -8,32 +8,70 @@ describe('animated_sprite', function () local spr_data1 = sprite_data(sprite_id_location(1, 0), tile_vector(1, 2), vector(4, 6)) local spr_data2 = sprite_data(sprite_id_location(2, 0), tile_vector(1, 2), vector(4, 6)) local anim_spr_data = animated_sprite_data({spr_data1, spr_data2}, 10, true) - local anim_spr_data_once = animated_sprite_data({spr_data1, spr_data2}, 10, false) + local anim_spr_data_no_loop = animated_sprite_data({spr_data1, spr_data2}, 10, false) + local anim_spr_data_table = { + loop = anim_spr_data, + no_loop = anim_spr_data_no_loop + } describe('_init', function () it('should init an animated sprite with data, automatically playing from step 1, frame 0', function () - local anim_spr = animated_sprite(anim_spr_data) - assert.are_same({anim_spr_data, true, 1, 0}, - {anim_spr.data, anim_spr.playing, anim_spr.current_step, anim_spr.local_frame}) + local anim_spr = animated_sprite(anim_spr_data_table) + assert.are_same({anim_spr_data_table, false, nil, nil, nil}, + {anim_spr.data_table, anim_spr.playing, anim_spr.current_anim_key, anim_spr.current_step, anim_spr.local_frame}) end) end) describe('_tostring', function () it('should return a string describing data, current step and local frame', function () - local anim_spr = animated_sprite(anim_spr_data) + local anim_spr = animated_sprite(anim_spr_data_table) + anim_spr.playing = true + anim_spr.current_anim_key = "idle" anim_spr.current_step = 2 anim_spr.local_frame = 5 - assert.are_equal("animated_sprite(animated_sprite_data([2 sprites], 10, true), true, 2, 5)", anim_spr:_tostring()) + assert.are_equal("animated_sprite({loop = animated_sprite_data([2 sprites], 10, true), no_loop = animated_sprite_data([2 sprites], 10, false)}, true, idle, 2, 5)", anim_spr:_tostring()) + end) + + end) + + describe('play', function () + + it('should assert if the anim_key is not found', function () + local anim_spr = animated_sprite(anim_spr_data_table) + + assert.has_error(function() anim_spr:play("unknown") end, + "animated_sprite:play: self.data_table['unknown'] doesn't exist") + end) + + it('should start playing the anim from the first step, first frame', function () + local anim_spr = animated_sprite(anim_spr_data_table) + anim_spr:play("loop") + + assert.are_same({true, "loop", 1, 0}, + {anim_spr.playing, anim_spr.current_anim_key, anim_spr.current_step, anim_spr.local_frame}) end) end) describe('update', function () + it('should do nothing when not playing', function () + local anim_spr = animated_sprite(anim_spr_data_table) + anim_spr.playing = false + anim_spr.current_step = 9 + anim_spr.local_frame = 99 + + anim_spr:update() + + assert.are_same({false, 9, 99}, + {anim_spr.playing, anim_spr.current_step, anim_spr.local_frame}) + end) + it('should increment the local frame if under the animation step_frames', function () - local anim_spr = animated_sprite(anim_spr_data) + local anim_spr = animated_sprite(anim_spr_data_table) anim_spr.playing = true + anim_spr.current_anim_key = "loop" anim_spr.current_step = 1 anim_spr.local_frame = 8 -- data.step_frames is 10, so frames play from 0 to 9 @@ -44,8 +82,9 @@ describe('animated_sprite', function () end) it('should reset local frame and enter next step when step_frames is reached', function () - local anim_spr = animated_sprite(anim_spr_data) + local anim_spr = animated_sprite(anim_spr_data_table) anim_spr.playing = true + anim_spr.current_anim_key = "loop" anim_spr.current_step = 1 anim_spr.local_frame = 9 -- data.step_frames - 1 @@ -56,8 +95,9 @@ describe('animated_sprite', function () end) it('should continue playing from the start when looping and end of animation has been reached', function () - local anim_spr = animated_sprite(anim_spr_data) + local anim_spr = animated_sprite(anim_spr_data_table) anim_spr.playing = true + anim_spr.current_anim_key = "loop" anim_spr.current_step = 2 anim_spr.local_frame = 9 -- data.step_frames - 1 @@ -68,7 +108,8 @@ describe('animated_sprite', function () end) it('should stop playing when not looping and end of animation has been reached', function () - local anim_spr = animated_sprite(anim_spr_data_once) + local anim_spr = animated_sprite(anim_spr_data_table) + anim_spr.current_anim_key = "no_loop" anim_spr.playing = true anim_spr.current_step = 2 anim_spr.local_frame = 9 -- data.step_frames - 1 @@ -83,13 +124,8 @@ describe('animated_sprite', function () describe('render', function () - local anim_spr = animated_sprite(anim_spr_data) - setup(function () sprite_data_render = stub(sprite_data, "render") - - anim_spr.current_step = 2 - anim_spr.local_frame = 5 end) teardown(function () @@ -100,7 +136,22 @@ describe('animated_sprite', function () sprite_data_render:clear() end) - it('should render the sprite from the id location, at the draw position minus pivot, with correct span when not flipping', function () + it('should not render the sprite when not playing', function () + local anim_spr = animated_sprite(anim_spr_data_table) + anim_spr.current_anim_key = nil + + anim_spr:render(vector(41, 80), false, true) + + assert.spy(sprite_data_render).was_called(0) + end) + + it('should render the sprite for current animation and step, with passed arguments', function () + + local anim_spr = animated_sprite(anim_spr_data_table) + anim_spr.current_anim_key = "no_loop" + anim_spr.current_step = 2 + anim_spr.local_frame = 5 + anim_spr:render(vector(41, 80), false, true) assert.spy(sprite_data_render).was_called(1) From be6fbb97f37f2038d674fa9f5fa2030094ad83a3 Mon Sep 17 00:00:00 2001 From: hsandt Date: Sat, 20 Apr 2019 03:28:29 +0100 Subject: [PATCH 107/140] [SPRITE] Player character uses animated sprite, added run1 sprite and 2 anims --- data/data.p8 | 84 ++++++++++++------------ src/game/data/playercharacter_data.lua | 21 ++++-- src/game/ingame/playercharacter.lua | 19 ++++-- src/game/itests/itestplayercharacter.lua | 36 +++++----- tests/utestplayercharacter.lua | 83 +++++++++++------------ 5 files changed, 134 insertions(+), 109 deletions(-) diff --git a/data/data.p8 b/data/data.p8 index 4af83cc1..9e0e708d 100644 --- a/data/data.p8 +++ b/data/data.p8 @@ -19,48 +19,6 @@ eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeccccccee1eeeeecccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeceeccccccceeecccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeccffccccceeeccccc7cceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeecccfcc7ccceeccccc777cceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeecccccc770cceecccccc7ccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeecee1cc770ceeecccccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeccccf77f0eecccccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeececccffeeeeecccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeee1fccf7eeeecccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeee777cf77eeeeecccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeee7777c77eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeee77eceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeececeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeee7e7eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeee0872088eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeee27888028eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -99994444eeeeeee49eeeeeeeeeeeeeee99eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -99994444eeeeee4499eeeeeeeeeeeeee999ee4e4eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -99994444eeeee444999eeeeeeeeeeeee99994444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -99994444eeee44449999eeeeeeeeee4499994444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -44449999eee4999944449eeeeeee999944449999eeeeeeee44449999eeee9999eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -44449999ee449999444499eeee44999944449999eeeeeeee44449999eeee9999eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -44449999e44499994444999e4444999944449999eeeeeeee44449999eeee999944449999eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -4444999944449999444499994444999944449999eeeeeeee44449999eeee999944449999eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -77777777000000077000000000000000770000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -77777777000000777700000000000000777007070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -77777777000007777770000000000000777777770000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -77777777000077777777000000000077777777770000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -77777777000777777777700000007777777777770000000077777777000077770000000000000000000000000000000000000000000000000000000000000000 -77777777007777777777770000777777777777770000000077777777000077770000000000000000000000000000000000000000000000000000000000000000 -77777777077777777777777077777777777777770000000077777777000077777777777700000000000000000000000000000000000000000000000000000000 -77777777777777777777777777777777777777770000000077777777000077777777777700000000000000000000000000000000000000000000000000000000 -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee @@ -77,6 +35,22 @@ eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +99994444eeeeeee49eeeeeeeeeeeeeee99eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +99994444eeeeee4499eeeeeeeeeeeeee999ee4e4eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +99994444eeeee444999eeeeeeeeeeeee99994444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +99994444eeee44449999eeeeeeeeee4499994444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +44449999eee4999944449eeeeeee999944449999eeeeeeee44449999eeee9999eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +44449999ee449999444499eeee44999944449999eeeeeeee44449999eeee9999eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +44449999e44499994444999e4444999944449999eeeeeeee44449999eeee999944449999eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +4444999944449999444499994444999944449999eeeeeeee44449999eeee999944449999eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +77777777000000077000000000000000770000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +77777777000000777700000000000000777007070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +77777777000007777770000000000000777777770000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +77777777000077777777000000000077777777770000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +77777777000777777777700000007777777777770000000077777777000077770000000000000000000000000000000000000000000000000000000000000000 +77777777007777777777770000777777777777770000000077777777000077770000000000000000000000000000000000000000000000000000000000000000 +77777777077777777777777077777777777777770000000077777777000077777777777700000000000000000000000000000000000000000000000000000000 +77777777777777777777777777777777777777770000000077777777000077777777777700000000000000000000000000000000000000000000000000000000 eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee @@ -93,6 +67,32 @@ eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeccccccee1eeeeeeeeccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeceeccccccceeeeeeececccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeccffccccceeeeeeeeccffcccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeecccfcc7ccceeeeeeecccfcc7ccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeecccccc770cceeeeeeccccccc70cceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeecee1cc770ceeeeeeeceecccc70ceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeccccf77f0eeeeeeeeccccf7ff0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeececccffeeeeeeeeeceecccffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeee1fccf7eeeeeeeeeecfcffe77eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeee777cf77eeeeeeee77eccff77eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeee7777c77eeeeeeee77ecccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeee77eceeeeeeeeeeeeccee7ceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeeeececeeeeeeeeee7ccee888eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeeee7e7eeeeeeeee877eeee877eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeee0872088eeeeee88878eee88eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeee27888028eeeeee87888eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeecccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +ecccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eccccc7cceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +ccccc777cceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +cccccc7ccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +cccccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +cccccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +ecccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +ecccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeecccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee diff --git a/src/game/data/playercharacter_data.lua b/src/game/data/playercharacter_data.lua index de6d2a89..002ada83 100644 --- a/src/game/data/playercharacter_data.lua +++ b/src/game/data/playercharacter_data.lua @@ -1,6 +1,7 @@ local sprite_data = require("engine/render/sprite_data") +local animated_sprite_data = require("engine/render/animated_sprite_data") -return { +local playercharacter_data = { -- platformer motion @@ -70,9 +71,19 @@ return { -- sprite -- stand right - sonic_sprite_data = { - idle = sprite_data(sprite_id_location(0, 2), tile_vector(2, 2), vector(12, 8)), - spin = sprite_data(sprite_id_location(2, 2), tile_vector(2, 2), vector(5, 5)), - } + sonic_sprite_data_table = { + idle = sprite_data(sprite_id_location(0, 8), tile_vector(2, 2), vector(12, 8)), + run1 = sprite_data(sprite_id_location(2, 8), tile_vector(2, 2), vector(12, 8)), + spin = sprite_data(sprite_id_location(0, 10), tile_vector(2, 2), vector(5, 5)), + }, } + +-- define animated sprite data in a second step, as it needs sprite data to be defined first +playercharacter_data.sonic_animated_sprite_data_table = { + idle = animated_sprite_data.create(playercharacter_data.sonic_sprite_data_table, {"idle"}, 2, true), + run = animated_sprite_data.create(playercharacter_data.sonic_sprite_data_table, {"idle", "run1"}, 2, true), + spin = animated_sprite_data.create(playercharacter_data.sonic_sprite_data_table, {"spin"}, 2, true), +} + +return playercharacter_data diff --git a/src/game/ingame/playercharacter.lua b/src/game/ingame/playercharacter.lua index e4da2088..bc92a749 100644 --- a/src/game/ingame/playercharacter.lua +++ b/src/game/ingame/playercharacter.lua @@ -5,6 +5,7 @@ require("engine/core/math") local input = require("engine/input/input") local collision = require("engine/physics/collision") local world = require("engine/physics/world") +local animated_sprite = require("engine/render/animated_sprite") local pc_data = require("game/data/playercharacter_data") @@ -44,6 +45,12 @@ local player_char = new_class() -- debug_move_accel float move acceleration in debug mode -- debug_move_decel float move deceleration in debug mode + +-- components + +-- anim_spr animated_sprite controls sprite animation, responsible for sprite rendering + + -- state vars -- control_mode control_modes control mode: human (default) or ai @@ -68,6 +75,8 @@ function player_char:_init() self.debug_move_accel = pc_data.debug_move_accel self.debug_move_decel = pc_data.debug_move_decel + self.anim_spr = animated_sprite(pc_data.sonic_animated_sprite_data_table) + self:_setup() end @@ -83,7 +92,7 @@ function player_char:_setup() self.ground_speed = 0. self.velocity = vector.zero() self.debug_velocity = vector.zero() - self.slope_angle = 0 + self.slope_angle = 0. self.move_intention = vector.zero() self.jump_intention = false @@ -92,7 +101,7 @@ function player_char:_setup() self.has_jumped_this_frame = false self.has_interrupted_jump = false - self.current_sprite = "idle" + self.anim_spr:play("idle") end -- spawn character at given position, and escape from ground / enter airborne state if needed @@ -337,14 +346,14 @@ function player_char:_enter_motion_state(next_motion_state) -- and since ground speed is now unused, reset it for clarity self.ground_speed = 0 self.should_jump = false - self.current_sprite = "spin" + self.anim_spr:play("spin") elseif next_motion_state == motion_states.grounded then -- we have just reached the ground (and possibly escaped), -- reset values airborne vars self.velocity.y = 0 -- no velocity retain yet on y self.has_jumped_this_frame = false -- optional since consumed immediately in _update_platformer_motion_airborne self.has_interrupted_jump = false - self.current_sprite = "idle" + self.anim_spr:play("idle") end end @@ -1050,7 +1059,7 @@ end -- render the player character sprite at its current position function player_char:render() local flip_x = self.horizontal_dir == horizontal_dirs.left - self.spr_data[self.current_sprite]:render(self.position, flip_x) + self.anim_spr:render(self.position, flip_x) end return player_char diff --git a/src/game/itests/itestplayercharacter.lua b/src/game/itests/itestplayercharacter.lua index d8448453..c434e051 100644 --- a/src/game/itests/itestplayercharacter.lua +++ b/src/game/itests/itestplayercharacter.lua @@ -609,16 +609,17 @@ expect pc_velocity 0 0 -- = fixed character pivot computed from drawn sprite topleft (with some gap above character's head) -- and not actual sprite topleft in the spritesheet itest_dsl_parser.register( - 'pc render idle right', [[ + 'pc anim idle right', [[ @stage # . # warp 4 8 +wait 60 ]]) itest_dsl_parser.register( - 'pc render idle left', [[ + 'pc anim idle left', [[ @stage # . # @@ -627,30 +628,33 @@ warp 4 8 move left wait 1 stop +wait 59 ]]) +-- note: due to flooring, character will go 1px to the left in only 1 frame, +-- so it will look offset compared to the previous test with right + itest_dsl_parser.register( - 'pc render airborne right', [[ + 'pc anim run left and fall', [[ @stage # -. -# +... +### -warp 4 7 +warp 20 8 +move left +wait 60 ]]) + itest_dsl_parser.register( - 'pc render airborne left', [[ + 'pc anim run right and fall', [[ @stage # -. -# +... +### -warp 4 7 -move left -wait 1 -stop +warp 4 8 +move right +wait 60 ]]) --- note: due to flooring, character will go 1px to the left in only 1 frame, --- so it will look offset compared to the previous test with right - --#pico8]=] diff --git a/tests/utestplayercharacter.lua b/tests/utestplayercharacter.lua index 44ec0cc0..c1f6de7e 100644 --- a/tests/utestplayercharacter.lua +++ b/tests/utestplayercharacter.lua @@ -115,6 +115,14 @@ describe('player_char', function () describe('_setup', function () + setup(function () + spy.on(animated_sprite, "play") + end) + + teardown(function () + animated_sprite.play:revert() + end) + it('should reset the character state vars', function () local pc = player_char() assert.is_not_nil(pc) @@ -135,7 +143,6 @@ describe('player_char', function () false, false, false, - "idle" }, { pc.control_mode, @@ -155,9 +162,10 @@ describe('player_char', function () pc.should_jump, pc.has_jumped_this_frame, pc.has_interrupted_jump, - pc.current_sprite } ) + assert.spy(animated_sprite.play).was_called(1) + assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "idle") end) end) @@ -1222,41 +1230,58 @@ describe('player_char', function () describe('_enter_motion_state', function () + setup(function () + spy.on(animated_sprite, "play") + end) + + teardown(function () + animated_sprite.play:revert() + end) + + -- since pc is _init in before_each and _init calls _setup + -- which calls pc.anim_spr:play, we must clear call count just after that + before_each(function () + animated_sprite.play:clear() + end) + it('should enter passed state: airborne and reset ground-specific state vars', function () -- character starts grounded pc:_enter_motion_state(motion_states.airborne) + assert.are_same({ motion_states.airborne, 0, - false, - "spin" + false }, { pc.motion_state, pc.ground_speed, - pc.should_jump, - pc.current_sprite + pc.should_jump }) + assert.spy(animated_sprite.play).was_called(1) + assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "spin") end) - it('. should enter passed state: grounded and reset speed y and has_interrupted_jump', function () + -- bugfix history: . + it('should enter passed state: grounded and reset speed y and has_interrupted_jump', function () pc.motion_state = motion_states.airborne pc:_enter_motion_state(motion_states.grounded) + assert.are_same({ motion_states.grounded, 0, false, false, - "idle" }, { pc.motion_state, pc.velocity.y, pc.has_jumped_this_frame, pc.has_interrupted_jump, - pc.current_sprite }) + assert.spy(animated_sprite.play).was_called(1) + assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "idle") end) end) @@ -3791,19 +3816,19 @@ describe('player_char', function () describe('render', function () - local spr_data_render_stub + local anim_spr_render_stub setup(function () -- create a generic stub at struct level so it works with any particular sprite - spr_data_render_stub = stub(sprite_data, "render") + anim_spr_render_stub = stub(animated_sprite, "render") end) teardown(function () - spr_data_render_stub:revert() + anim_spr_render_stub:revert() end) after_each(function () - spr_data_render_stub:clear() + anim_spr_render_stub:clear() end) it('(when character is facing left) should call render on sonic sprite data: idle with the character\'s position, flipped x', function () @@ -3812,8 +3837,8 @@ describe('player_char', function () pc:render() - assert.spy(spr_data_render_stub).was_called(1) - assert.spy(spr_data_render_stub).was_called_with(match.ref(pc_data.sonic_sprite_data["idle"]), vector(12, 8), true) + assert.spy(anim_spr_render_stub).was_called(1) + assert.spy(anim_spr_render_stub).was_called_with(match.ref(pc.anim_spr), vector(12, 8), true) end) it('(when character is facing right) should call render on sonic sprite data: idle with the character\'s position, not flipped x', function () @@ -3822,32 +3847,8 @@ describe('player_char', function () pc:render() - assert.spy(spr_data_render_stub).was_called(1) - assert.spy(spr_data_render_stub).was_called_with(match.ref(pc_data.sonic_sprite_data["idle"]), vector(12, 8), false) - end) - - it('(when character is airborne, facing left) should call render on sonic sprite data: spin with the character\'s position, flipped x', function () - pc.motion_state = motion_states.airborne -- optional, just to be consistent with current_sprite - pc.current_sprite = "spin" - pc.position = vector(12, 8) - pc.horizontal_dir = horizontal_dirs.left - - pc:render() - - assert.spy(spr_data_render_stub).was_called(1) - assert.spy(spr_data_render_stub).was_called_with(match.ref(pc_data.sonic_sprite_data["spin"]), vector(12, 8), true) - end) - - it('(when character is airborne, facing right) should call render on sonic sprite data: spin with the character\'s position, not flipped x', function () - pc.motion_state = motion_states.airborne -- optional, just to be consistent with current_sprite - pc.current_sprite = "spin" - pc.position = vector(12, 8) - pc.horizontal_dir = horizontal_dirs.right - - pc:render() - - assert.spy(spr_data_render_stub).was_called(1) - assert.spy(spr_data_render_stub).was_called_with(match.ref(pc_data.sonic_sprite_data["spin"]), vector(12, 8), false) + assert.spy(anim_spr_render_stub).was_called(1) + assert.spy(anim_spr_render_stub).was_called_with(match.ref(pc.anim_spr), vector(12, 8), false) end) end) From ab45b791fc14994f3b4e1964f1835943c8c083a9 Mon Sep 17 00:00:00 2001 From: hsandt Date: Sat, 20 Apr 2019 03:43:35 +0100 Subject: [PATCH 108/140] [CODE] Replaced "or false" default argument pattern with clean nil check in case we switch to "or true" later, which would not work when passing false --- src/engine/debug/logging.lua | 15 ++++++++++++--- src/engine/render/animated_sprite_data.lua | 5 ++++- src/engine/test/unittest_helper.lua | 4 +++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/engine/debug/logging.lua b/src/engine/debug/logging.lua index b1c28c73..3534ef94 100644 --- a/src/engine/debug/logging.lua +++ b/src/engine/debug/logging.lua @@ -228,10 +228,19 @@ only use this if you are sure that all the keys are comparable (e.g. only numeric or only strings) --]] function dump(dumped_value, as_key, level, use_tostring, sorted_keys) - as_key = as_key or false + if as_key == nil then + as_key = false + end + level = level or logger.dump_max_recursion_level - use_tostring = use_tostring or false - sorted_keys = sorted_keys or false + + if use_tostring == nil then + use_tostring = false + end + + if sorted_keys == nil then + sorted_keys = false + end local repr diff --git a/src/engine/render/animated_sprite_data.lua b/src/engine/render/animated_sprite_data.lua index 26c4886f..1311278f 100644 --- a/src/engine/render/animated_sprite_data.lua +++ b/src/engine/render/animated_sprite_data.lua @@ -9,7 +9,10 @@ function animated_sprite_data:_init(sprites, step_frames, looping) assert(step_frames > 0) self.sprites = sprites self.step_frames = step_frames - self.looping = looping or false + if looping == nil then + looping = false + end + self.looping = looping end -- factory function to create animated sprite data from a table diff --git a/src/engine/test/unittest_helper.lua b/src/engine/test/unittest_helper.lua index d54ff69f..01de040f 100644 --- a/src/engine/test/unittest_helper.lua +++ b/src/engine/test/unittest_helper.lua @@ -3,7 +3,9 @@ require("engine/core/helper") -- helper for unitests executed in pico8, that miss busted assertions function are_same_with_message(t, passed, compare_raw_content) - compare_raw_content = compare_raw_content or false + if compare_raw_content == nil then + compare_raw_content = false + end local result = are_same(t, passed, compare_raw_content) if result then -- passed is not same as t, return false with does_not_contain message (will appear when using assert(not are_same(...))) From 6fc29e5804dad66893f5e6056c6a5315fd7f867a Mon Sep 17 00:00:00 2001 From: hsandt Date: Sat, 20 Apr 2019 03:57:17 +0100 Subject: [PATCH 109/140] [SPRITE] Added parameter `from_start` to animated_sprite:play --- src/engine/render/animated_sprite.lua | 25 ++++++++++++---- tests/utestanimated_sprite.lua | 42 ++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/src/engine/render/animated_sprite.lua b/src/engine/render/animated_sprite.lua index c2b73155..a96d6b68 100644 --- a/src/engine/render/animated_sprite.lua +++ b/src/engine/render/animated_sprite.lua @@ -22,13 +22,26 @@ function animated_sprite:_tostring() end --#endif --- start animation with given key: string -function animated_sprite:play(anim_key) +-- play animation with given key: string +-- if this animation is not already set, play it from start +-- if this animation is already set, check from_start: +-- - if true, play it from start +-- - if false, do nothing (if playing, it means continuing to play; if not playing (e.g. stopped at the end), do not replay from start) +-- note that even if the animation is paused, it won't be resumed in this case (because we don't have a flag has_ended to distinguish pause and end) +-- by default, continue animation already playing +function animated_sprite:play(anim_key, from_start) assert(self.data_table[anim_key] ~= nil, "animated_sprite:play: self.data_table['"..anim_key.."'] doesn't exist") - self.playing = true - self.current_anim_key = anim_key - self.current_step = 1 - self.local_frame = 0 + + if from_start == nil then + from_start = false + end + + if self.current_anim_key ~= anim_key or from_start then + self.playing = true -- this will do nothing if forcing replay from start during play + self.current_anim_key = anim_key -- this will do nothing if this animation is already set + self.current_step = 1 + self.local_frame = 0 + end end -- update the sprite animation diff --git a/tests/utestanimated_sprite.lua b/tests/utestanimated_sprite.lua index 815695e1..1f08b9c9 100644 --- a/tests/utestanimated_sprite.lua +++ b/tests/utestanimated_sprite.lua @@ -44,14 +44,54 @@ describe('animated_sprite', function () "animated_sprite:play: self.data_table['unknown'] doesn't exist") end) - it('should start playing the anim from the first step, first frame', function () + it('should start playing a new anim from the first step, first frame', function () local anim_spr = animated_sprite(anim_spr_data_table) + anim_spr:play("loop") assert.are_same({true, "loop", 1, 0}, {anim_spr.playing, anim_spr.current_anim_key, anim_spr.current_step, anim_spr.local_frame}) end) + it('should start playing the current anim from the first step, first frame if passing the current anim and from_start is true', function () + local anim_spr = animated_sprite(anim_spr_data_table) + anim_spr.playing = true + anim_spr.current_anim_key = "loop" + anim_spr.current_step = 2 + anim_spr.local_frame = 5 + + anim_spr:play("loop", true) + + assert.are_same({true, "loop", 1, 0}, + {anim_spr.playing, anim_spr.current_anim_key, anim_spr.current_step, anim_spr.local_frame}) + end) + + it('should continue playing the current anim if passing the current anim and from_start is false', function () + local anim_spr = animated_sprite(anim_spr_data_table) + anim_spr.playing = true + anim_spr.current_anim_key = "no_loop" + anim_spr.current_step = 2 + anim_spr.local_frame = 5 + + anim_spr:play("no_loop", false) + + assert.are_same({true, "no_loop", 2, 5}, + {anim_spr.playing, anim_spr.current_anim_key, anim_spr.current_step, anim_spr.local_frame}) + end) + + it('should not resume the current anim if paused, passing the current anim and from_start is false', function () + local anim_spr = animated_sprite(anim_spr_data_table) + anim_spr.playing = false + anim_spr.current_anim_key = "no_loop" + anim_spr.current_step = 2 + anim_spr.local_frame = 5 + + anim_spr:play("no_loop", false) + + assert.are_same({false, "no_loop", 2, 5}, + {anim_spr.playing, anim_spr.current_anim_key, anim_spr.current_step, anim_spr.local_frame}) + end) + end) describe('update', function () From 68b11f6ade47390e6d0afae6268d29851038c424 Mon Sep 17 00:00:00 2001 From: hsandt Date: Sat, 20 Apr 2019 14:36:57 +0100 Subject: [PATCH 110/140] [SPRITE] Play idle/run anim based on character ground speed --- src/game/ingame/playercharacter.lua | 11 ++ tests/utestplayercharacter.lua | 166 ++++++++++++++++++++++++++-- 2 files changed, 166 insertions(+), 11 deletions(-) diff --git a/src/game/ingame/playercharacter.lua b/src/game/ingame/playercharacter.lua index bc92a749..70163acd 100644 --- a/src/game/ingame/playercharacter.lua +++ b/src/game/ingame/playercharacter.lua @@ -92,6 +92,7 @@ function player_char:_setup() self.ground_speed = 0. self.velocity = vector.zero() self.debug_velocity = vector.zero() + -- slope_angle starts at 0 instead of nil to match grounded state above (first spawn will set this anyway) self.slope_angle = 0. self.move_intention = vector.zero() @@ -345,6 +346,7 @@ function player_char:_enter_motion_state(next_motion_state) -- we have just left the ground, enter airborne state -- and since ground speed is now unused, reset it for clarity self.ground_speed = 0 + self.slope_angle = nil self.should_jump = false self.anim_spr:play("spin") elseif next_motion_state == motion_states.grounded then @@ -399,8 +401,17 @@ function player_char:_update_platformer_motion_grounded() if ground_motion_result.is_falling then self:_enter_motion_state(motion_states.airborne) else + -- we are still grounded, so: + -- only allow jump preparation for next frame if not already falling self:_check_jump_intention() + + -- update ground animation based on speed + if self.ground_speed == 0 then + self.anim_spr:play("idle") + else + self.anim_spr:play("run") + end end log("self.position: "..self.position, "trace") diff --git a/tests/utestplayercharacter.lua b/tests/utestplayercharacter.lua index c1f6de7e..6fa44cb0 100644 --- a/tests/utestplayercharacter.lua +++ b/tests/utestplayercharacter.lua @@ -1451,6 +1451,8 @@ describe('player_char', function () local compute_ground_motion_result_mock setup(function () + spy.on(animated_sprite, "play") + update_ground_speed_mock = stub(player_char, "_update_ground_speed", function (self) self.ground_speed = -2.5 -- use fractional speed to check that fractions are preserved end) @@ -1459,11 +1461,19 @@ describe('player_char', function () end) teardown(function () + animated_sprite.play:revert() + update_ground_speed_mock:revert() enter_motion_state_stub:revert() check_jump_intention_stub:revert() end) + -- since pc is _init in before_each and _init calls _setup + -- which calls pc.anim_spr:play, we must clear call count just after that + before_each(function () + animated_sprite.play:clear() + end) + after_each(function () update_ground_speed_mock:clear() enter_motion_state_stub:clear() @@ -1478,12 +1488,13 @@ describe('player_char', function () assert.spy(update_ground_speed_mock).was_called_with(match.ref(pc)) end) - describe('(when _compute_ground_motion_result returns a motion result with position vector(3, 4), is_blocked: false, is_falling: false)', function () + describe('(when _compute_ground_motion_result returns a motion result with position vector(3, 4), slope_angle: 0.25, is_blocked: false, is_falling: false)', function () setup(function () compute_ground_motion_result_mock = stub(player_char, "_compute_ground_motion_result", function (self) return collision.ground_motion_result( vector(3, 4), + 0.25, false, false ) @@ -1498,22 +1509,30 @@ describe('player_char', function () compute_ground_motion_result_mock:clear() end) - it('should set the position to vector(3, 4)', function () - pc:_update_platformer_motion_grounded() - assert.are_equal(vector(3, 4), pc.position) - end) - it('should keep updated ground speed and set velocity frame according to ground speed (not blocked)', function () pc:_update_platformer_motion_grounded() + -- interface: relying on _update_ground_speed implementation assert.are_same({-2.5, vector(-2.5, 0)}, {pc.ground_speed, pc.velocity}) end) it('should keep updated ground speed and set velocity frame according to ground speed and slope if not flat (not blocked)', function () pc.slope_angle = -1/6 -- cos = 1/2, sin = -sqrt(3)/2, but use the formula directly to support floating errors pc:_update_platformer_motion_grounded() + -- interface: relying on _update_ground_speed implementation assert.are_same({-2.5, vector(-2.5*cos(1/6), 2.5*sqrt(3)/2)}, {pc.ground_speed, pc.velocity}) end) + it('should set the position to vector(3, 4)', function () + pc:_update_platformer_motion_grounded() + assert.are_equal(vector(3, 4), pc.position) + end) + + it('should set the slope angle to 0.25', function () + pc.slope_angle = -0.25 + pc:_update_platformer_motion_grounded() + assert.are_equal(0.25, pc.slope_angle) + end) + it('should call _check_jump_intention, not _enter_motion_state (not falling)', function () pc:_update_platformer_motion_grounded() @@ -1523,9 +1542,17 @@ describe('player_char', function () assert.spy(enter_motion_state_stub).was_not_called() end) + it('should play the run animation (ground speed ~= 0)', function () + pc:_update_platformer_motion_grounded() + + -- implementation + assert.spy(animated_sprite.play).was_called(1) + assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "run") + end) + end) - describe('(when _compute_ground_motion_result returns a motion result with position vector(3, 4))', function () + describe('(when _compute_ground_motion_result returns a motion result with position vector(3, 4), slope_angle: 0.5, is_blocked: true, is_falling: false)', function () local compute_ground_motion_result_mock @@ -1533,8 +1560,66 @@ describe('player_char', function () compute_ground_motion_result_mock = stub(player_char, "_compute_ground_motion_result", function (self) return collision.ground_motion_result( vector(3, 4), - -0.25, + 0.5, true, + false + ) + end) + end) + + teardown(function () + compute_ground_motion_result_mock:revert() + end) + + after_each(function () + compute_ground_motion_result_mock:clear() + end) + + it('should reset ground speed and velocity frame to zero (blocked)', function () + pc:_update_platformer_motion_grounded() + assert.are_same({0, vector.zero()}, {pc.ground_speed, pc.velocity}) + end) + + it('should call _check_jump_intention, not _enter_motion_state (not falling)', function () + pc:_update_platformer_motion_grounded() + + -- implementation + assert.spy(check_jump_intention_stub).was_called(1) + assert.spy(check_jump_intention_stub).was_called_with(match.ref(pc)) + assert.spy(enter_motion_state_stub).was_not_called() + end) + + it('should set the position to vector(3, 4)', function () + pc:_update_platformer_motion_grounded() + assert.are_equal(vector(3, 4), pc.position) + end) + + it('should set the slope angle to 0.5', function () + pc.slope_angle = -0.25 + pc:_update_platformer_motion_grounded() + assert.are_equal(0.5, pc.slope_angle) + end) + + it('should play the idle animation (ground speed ~= 0)', function () + pc:_update_platformer_motion_grounded() + + -- implementation + assert.spy(animated_sprite.play).was_called(1) + assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "idle") + end) + + end) + + describe('(when _compute_ground_motion_result returns a motion result with position vector(3, 4), slope_angle: nil, is_blocked: false, is_falling: true)', function () + + local compute_ground_motion_result_mock + + setup(function () + compute_ground_motion_result_mock = stub(player_char, "_compute_ground_motion_result", function (self) + return collision.ground_motion_result( + vector(3, 4), + nil, + false, true ) end) @@ -1548,14 +1633,62 @@ describe('player_char', function () compute_ground_motion_result_mock:clear() end) + it('should keep updated ground speed and set velocity frame according to ground speed (not blocked)', function () + pc:_update_platformer_motion_grounded() + -- interface: relying on _update_ground_speed implementation + assert.are_same({-2.5, vector(-2.5, 0)}, {pc.ground_speed, pc.velocity}) + end) + + it('should keep updated ground speed and set velocity frame according to ground speed and slope if not flat (not blocked)', function () + pc.slope_angle = -1/6 -- cos = 1/2, sin = -sqrt(3)/2, but use the formula directly to support floating errors + pc:_update_platformer_motion_grounded() + -- interface: relying on _update_ground_speed implementation + assert.are_same({-2.5, vector(-2.5*cos(1/6), 2.5*sqrt(3)/2)}, {pc.ground_speed, pc.velocity}) + end) + + it('should call _enter_motion_state with airborne state, not call _check_jump_intention nor anim_spr:play (falling)', function () + pc:_update_platformer_motion_grounded() + + -- implementation + assert.spy(enter_motion_state_stub).was_called(1) + assert.spy(enter_motion_state_stub).was_called_with(match.ref(pc), motion_states.airborne) + assert.spy(check_jump_intention_stub).was_not_called() + assert.spy(animated_sprite.play).was_not_called() + end) + it('should set the position to vector(3, 4)', function () pc:_update_platformer_motion_grounded() assert.are_equal(vector(3, 4), pc.position) end) - it('should set the slope angle to -0.25', function () + it('should set the slope angle to nil', function () pc:_update_platformer_motion_grounded() - assert.are_equal(-0.25, pc.slope_angle) + assert.is_nil(pc.slope_angle) + end) + + end) + + describe('(when _compute_ground_motion_result returns a motion result with position vector(3, 4), slope_angle: nil, is_blocked: true, is_falling: true)', function () + + local compute_ground_motion_result_mock + + setup(function () + compute_ground_motion_result_mock = stub(player_char, "_compute_ground_motion_result", function (self) + return collision.ground_motion_result( + vector(3, 4), + nil, + true, + true + ) + end) + end) + + teardown(function () + compute_ground_motion_result_mock:revert() + end) + + after_each(function () + compute_ground_motion_result_mock:clear() end) it('should reset ground speed and velocity frame to zero (blocked)', function () @@ -1563,13 +1696,24 @@ describe('player_char', function () assert.are_same({0, vector.zero()}, {pc.ground_speed, pc.velocity}) end) - it('should call _enter_motion_state with airborne state, not call _check_jump_intention (falling)', function () + it('should call _enter_motion_state with airborne state, not call _check_jump_intention nor anim_spr:play (falling)', function () pc:_update_platformer_motion_grounded() -- implementation assert.spy(enter_motion_state_stub).was_called(1) assert.spy(enter_motion_state_stub).was_called_with(match.ref(pc), motion_states.airborne) assert.spy(check_jump_intention_stub).was_not_called() + assert.spy(animated_sprite.play).was_not_called() + end) + + it('should set the position to vector(3, 4)', function () + pc:_update_platformer_motion_grounded() + assert.are_equal(vector(3, 4), pc.position) + end) + + it('should set the slope angle to nil', function () + pc:_update_platformer_motion_grounded() + assert.is_nil(pc.slope_angle) end) end) From a1d7e1e7e4f00923a433dc4606db8fcdd703833e Mon Sep 17 00:00:00 2001 From: hsandt Date: Sat, 20 Apr 2019 15:41:42 +0100 Subject: [PATCH 111/140] [TEST] Added -r / --render option to render during itests (for debug) --- build.sh | 2 +- sonic-2d-tech-demo.sublime-project | 8 +++++ test.sh | 46 +++++++++++++++++++++++---- tests/itests/utestheadless_itests.lua | 9 ++++++ 4 files changed, 58 insertions(+), 7 deletions(-) diff --git a/build.sh b/build.sh index 6477eeaa..a9dafa01 100755 --- a/build.sh +++ b/build.sh @@ -24,7 +24,7 @@ minify=true while [ "$1" != "" ]; do case $1 in - -m | --no-minify ) shift + -m | --no-minify ) shift minify=false ;; * ) echo "unknown option: $1" diff --git a/sonic-2d-tech-demo.sublime-project b/sonic-2d-tech-demo.sublime-project index 9f91701e..0196424c 100644 --- a/sonic-2d-tech-demo.sublime-project +++ b/sonic-2d-tech-demo.sublime-project @@ -428,10 +428,18 @@ "name": "headless_itests", "cmd": ["./test.sh headless_itests"], }, + { + "name": "headless_itests (render)", + "cmd": ["./test.sh headless_itests --render"], + }, { "name": "headless_itests - solo", "cmd": ["./test.sh headless_itests solo"], }, + { + "name": "headless_itests - solo (render)", + "cmd": ["./test.sh headless_itests solo --render"], + }, ], }, ], diff --git a/test.sh b/test.sh index d86d004d..e07c6893 100755 --- a/test.sh +++ b/test.sh @@ -2,18 +2,46 @@ # $1: test name (module name) if [[ $# -lt 1 ]] ; then - echo "test.sh takes 1 mandatory param and 1 optional param, provided $#: + echo "test.sh takes 1 mandatory param, 1 optional param and 1 option, provided $#: $1: test file pattern - $2: test filter mode: (default 'standard') 'standard' to filter out all #mute, 'solo' to filter #solo, 'all' to include #mute" + $2: test filter mode: (default 'standard') 'standard' to filter out all #mute, 'solo' to filter #solo, 'all' to include #mute + -r or --render to enable rendering in the itest loop (used for $1=headless_itests only)" exit 1 fi +# https://stackoverflow.com/questions/192249/how-do-i-parse-command-line-arguments-in-bash + if [[ ${1::5} = "utest" ]] ; then MODULE=${1:5} else MODULE=$1 fi +# shift arguments 1 position so we start reading options at $1 +shift + +TEST_FILTER_MODE="standard" + +# if second argument is not an option, it means it's the positional argument "test filter mode" +# remember to check for "-" not "--" as the shortcut options only use a single hyphen +if [[ "${1::1}" != "-" ]] ; then + TEST_FILTER_MODE=$1 # should be "all" or "solo" + shift +fi + +RENDER=false + +while [[ "$1" != "" ]]; do + case $1 in + -r | --render ) shift + RENDER=true + ;; + * ) echo "unknown option: $1" + exit 1 + esac + shift +done + if [[ $MODULE = "all" || -z $MODULE ]] ; then TEST_FILE_PATTERN="utest" # all unit tests COVERAGE_OPTIONS="-c .luacov_all" # we cannot just use default .luacov since it would also affect specific module tests @@ -23,11 +51,11 @@ else COVERAGE_OPTIONS="-c .luacov_current \"/$MODULE\"" fi -if [[ $2 = "all" ]] ; then +if [[ $TEST_FILTER_MODE = "all" ]] ; then FILTER="" FILTER_OUT="" USE_COVERAGE=true -elif [[ $2 = "solo" ]]; then +elif [[ $TEST_FILTER_MODE = "solo" ]]; then FILTER="--filter \"#solo\"" FILTER_OUT="" # coverage on a file is not relevant when testing one or two functions @@ -38,7 +66,7 @@ else USE_COVERAGE=true fi -if [[ "$USE_COVERAGE" = true ]]; then +if [[ $USE_COVERAGE = true ]]; then PRE_TEST="rm -f luacov.stats.out luacov.report.out &&" POST_TEST="&& luacov $COVERAGE_OPTIONS && grep -C 3 -P \"(?:(?:^|[ *])\*0|\d+%)\" luacov.report.out" else @@ -46,8 +74,14 @@ else POST_TEST="" fi +EXTRA_ARGS="" + +if [[ $RENDER = true ]]; then + EXTRA_ARGS+="--render" +fi + LUA_PATH="src/?.lua;tests/?.lua" -CORE_TEST="busted tests --lpath=\"$LUA_PATH\" -p \"$TEST_FILE_PATTERN\" $FILTER $FILTER_OUT -c -v" +CORE_TEST="busted tests --lpath=\"$LUA_PATH\" -p \"$TEST_FILE_PATTERN\" $FILTER $FILTER_OUT -c -v -- $EXTRA_ARGS" TEST_COMMAND="$PRE_TEST $CORE_TEST $POST_TEST" echo "Testing $1..." diff --git a/tests/itests/utestheadless_itests.lua b/tests/itests/utestheadless_itests.lua index 65d1b28f..7313ba83 100644 --- a/tests/itests/utestheadless_itests.lua +++ b/tests/itests/utestheadless_itests.lua @@ -3,6 +3,12 @@ require("engine/test/integrationtest") local gameapp = require("game/application/gameapp") local gamestate_proxy = require("game/application/gamestate_proxy") +-- check options +local should_render = false +if contains(arg, "--render") then + should_render = true +end + local function find_all_scripts(dir) local files = {} local p = io.popen('find "'..dir..'" -type f -name *.lua') @@ -39,6 +45,9 @@ describe('headless itest', function () itest_manager:init_game_and_start_by_index(i) while integration_test_runner.current_state == test_states.running do integration_test_runner:update_game_and_test() + if should_render then + integration_test_runner:draw_game_and_test() + end end local itest_fail_message = nil From e95b3f3e259f713e3b440a82b117d23c80be9a16 Mon Sep 17 00:00:00 2001 From: hsandt Date: Sat, 20 Apr 2019 15:42:04 +0100 Subject: [PATCH 112/140] [SPRITE] Update animated sprite on player character update --- src/game/ingame/playercharacter.lua | 1 + tests/utestplayercharacter.lua | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/game/ingame/playercharacter.lua b/src/game/ingame/playercharacter.lua index 70163acd..b6e28008 100644 --- a/src/game/ingame/playercharacter.lua +++ b/src/game/ingame/playercharacter.lua @@ -152,6 +152,7 @@ end function player_char:update() self:_handle_input() self:_update_motion() + self.anim_spr:update() end -- update intention based on current input diff --git a/tests/utestplayercharacter.lua b/tests/utestplayercharacter.lua index 6fa44cb0..098b6643 100644 --- a/tests/utestplayercharacter.lua +++ b/tests/utestplayercharacter.lua @@ -344,21 +344,24 @@ describe('player_char', function () end) end) - describe('_handle_input', function () + describe('update', function () setup(function () stub(player_char, "_handle_input") stub(player_char, "_update_motion") + stub(animated_sprite, "update") end) teardown(function () player_char._handle_input:revert() player_char._update_motion:revert() + animated_sprite.update:revert() end) after_each(function () player_char._handle_input:clear() player_char._update_motion:clear() + animated_sprite.update:clear() end) it('should call _handle_input and _update_motion', function () @@ -369,6 +372,8 @@ describe('player_char', function () assert.spy(pc._handle_input).was_called_with(match.ref(pc)) assert.spy(pc._update_motion).was_called(1) assert.spy(pc._update_motion).was_called_with(match.ref(pc)) + assert.spy(animated_sprite.update).was_called(1) + assert.spy(animated_sprite.update).was_called_with(match.ref(pc.anim_spr)) end) end) From 3626ac6f5dc147c106e05b90346ba3e156d3028c Mon Sep 17 00:00:00 2001 From: hsandt Date: Sun, 21 Apr 2019 00:38:17 +0100 Subject: [PATCH 113/140] [BUILD] Shorter local module table name "mod" in integration test --- src/engine/test/integrationtest.lua | 54 +++++++++++++---------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/src/engine/test/integrationtest.lua b/src/engine/test/integrationtest.lua index fab255e5..8be9fe17 100644 --- a/src/engine/test/integrationtest.lua +++ b/src/engine/test/integrationtest.lua @@ -10,7 +10,7 @@ local logging = require("engine/debug/logging") local gameapp = require("game/application/gameapp") local input = require("engine/input/input") -local integrationtest = {} +local mod = {} test_states = { none = 'none', -- no test started @@ -25,7 +25,7 @@ test_states = { itest_manager = singleton(function (self) self.itests = {} end) -integrationtest.itest_manager = itest_manager +mod.itest_manager = itest_manager -- all-in-one utility function that creates and register a new itest, -- defining setup, actions and final assertion inside a contextual callback, @@ -50,7 +50,7 @@ integrationtest.itest_manager = itest_manager -- end) -- end) function itest_manager:register_itest(name, states, definition) - local itest = integrationtest.integration_test(name, states) + local itest = mod.integration_test(name, states) self:register(itest) -- context @@ -78,7 +78,7 @@ function itest_manager:register_itest(name, states, definition) -- we were already waiting, so finish last wait with empty action itest:add_action(last_time_trigger, nil) end - last_time_trigger = integrationtest.time_trigger(time, use_frame_unit) + last_time_trigger = mod.time_trigger(time, use_frame_unit) end function act(callback) @@ -87,7 +87,7 @@ function itest_manager:register_itest(name, states, definition) last_time_trigger = nil -- consume so we know no final wait-action is needed else -- no wait since last action (or this is the first action), so use immediate trigger - itest:add_action(integrationtest.immediate_trigger, callback) + itest:add_action(mod.immediate_trigger, callback) end end @@ -114,14 +114,14 @@ end function itest_manager:init_game_and_start_by_index(index) local itest = self.itests[index] assert(itest, "itest_manager:init_game_and_start_by_index: index is "..tostr(index).." but only "..tostr(#self.itests).." were registered.") - integration_test_runner:init_game_and_start(itest) + itest_runner:init_game_and_start(itest) end -- integration test runner singleton -- test lifetime: -- none -> running -> success/failure/timeout (still alive, but not updated) -- -> stopped when a another test starts running -integration_test_runner = singleton(function (self) +itest_runner = singleton(function (self) self.initialized = false self.current_test = nil self.current_frame = 0 @@ -132,7 +132,7 @@ integration_test_runner = singleton(function (self) end) -- helper method to use in rendered itest _init -function integration_test_runner:init_game_and_start(test) +function itest_runner:init_game_and_start(test) -- if there was a previous test, gameapp was already initialized, -- so reset it now (we could also just keep it and change the gamestate -- to void, if we are sure that all the itests have the same required modules) @@ -141,11 +141,11 @@ function integration_test_runner:init_game_and_start(test) end gameapp.init(test.active_gamestates) - integration_test_runner:start(test) + itest_runner:start(test) end -- helper method to use in rendered itest _update60 -function integration_test_runner:update_game_and_test() +function itest_runner:update_game_and_test() if self.current_state == test_states.running then -- update gameapp, then test runner -- updating test runner 2nd allows us to check the actual game state at final frame f, @@ -165,12 +165,12 @@ function integration_test_runner:update_game_and_test() end -- helper method to use in rendered itest _draw -function integration_test_runner:draw_game_and_test() +function itest_runner:draw_game_and_test() gameapp.draw() self:draw() end -function integration_test_runner:start(test) +function itest_runner:start(test) -- lazy initialization if not self.initialized then self:_initialize() @@ -197,8 +197,8 @@ function integration_test_runner:start(test) end end -function integration_test_runner:update() - assert(self.current_test, "integration_test_runner:update: current_test is not set") +function itest_runner:update() + assert(self.current_test, "itest_runner:update: current_test is not set") if self.current_state ~= test_states.running then -- the current test is over and we already got the result -- do nothing and fail silently (to avoid crashing @@ -217,7 +217,7 @@ function integration_test_runner:update() end end -function integration_test_runner:draw() +function itest_runner:draw() if self.current_test then api.print(self.current_test.name, 2, 2, colors.yellow) api.print(self.current_state, 2, 9, self:_get_test_state_color(self.current_state)) @@ -226,7 +226,7 @@ function integration_test_runner:draw() end end -function integration_test_runner:_get_test_state_color(test_state) +function itest_runner:_get_test_state_color(test_state) if test_state == test_states.none then return colors.white elseif test_state == test_states.running then @@ -240,25 +240,21 @@ function integration_test_runner:_get_test_state_color(test_state) end end -function integration_test_runner:_initialize() +function itest_runner:_initialize() -- use simulated input during itests input.mode = input_modes.simulated --#if log -- all itests should only print itest logs, and maybe trace if you want logging.logger:deactivate_all_categories() - ---[[#pico8 logging.logger.active_categories["itest"] = true ---#pico8]] - logging.logger.active_categories["trace"] = false --#endif self.initialized = true end -function integration_test_runner:_check_next_action() +function itest_runner:_check_next_action() assert(self._next_action_index <= #self.current_test.action_sequence, "self._next_action_index ("..self._next_action_index..") is out of bounds for self.current_test.action_sequence (size "..#self.current_test.action_sequence..")") -- test: chain actions with no intervals between them @@ -281,7 +277,7 @@ function integration_test_runner:_check_next_action() until not should_trigger_next_action end -function integration_test_runner:_check_end() +function itest_runner:_check_end() -- check if last action was applied, end now -- this means you can define an 'end' action just by adding an empty action at the end if self.current_test.action_sequence[1] then @@ -293,7 +289,7 @@ function integration_test_runner:_check_end() return false end -function integration_test_runner:_end_with_final_assertion() +function itest_runner:_end_with_final_assertion() -- check the final assertion so we know if we should end with success or failure result, message = self.current_test:_check_final_assertion() if result then @@ -307,7 +303,7 @@ end -- stop the current test, tear it down and reset all values -- this is only called when starting a new test, not when it finished, -- so we can still access info on the current test while the user examines its result -function integration_test_runner:stop() +function itest_runner:stop() if self.current_test.teardown then self.current_test.teardown() end @@ -321,7 +317,7 @@ end -- time trigger struct local time_trigger = new_struct() -integrationtest.time_trigger = time_trigger +mod.time_trigger = time_trigger -- non-member parameters -- time float time to wait before running callback after last trigger (in seconds by default, in frames if use_frame_unit is true) @@ -350,7 +346,7 @@ function time_trigger:_check(elapsed_frames) end -- helper triggers -integrationtest.immediate_trigger = time_trigger(0, true) +mod.immediate_trigger = time_trigger(0, true) -- scripted action struct (but we use class because comparing functions only work by reference) @@ -375,7 +371,7 @@ end -- integration test class local integration_test = new_class() -integrationtest.integration_test = integration_test +mod.integration_test = integration_test -- parameters -- name string test name @@ -431,4 +427,4 @@ function integration_test:_check_final_assertion() end end -return integrationtest +return mod From a3e76bad0500a0768e6b88fff794fb8490453efb Mon Sep 17 00:00:00 2001 From: hsandt Date: Sun, 21 Apr 2019 00:38:42 +0100 Subject: [PATCH 114/140] [DEBUG] Gamestate proxy has better assert --- src/game/application/gamestate_proxy.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/application/gamestate_proxy.lua b/src/game/application/gamestate_proxy.lua index 6fbded28..ef294dc3 100644 --- a/src/game/application/gamestate_proxy.lua +++ b/src/game/application/gamestate_proxy.lua @@ -53,7 +53,7 @@ end -- with a lightweight dummy state function gamestate_proxy:get(module_name) assert(type(module_name) == "string") - assert(self._state_modules[module_name] ~= nil, "gamestate_proxy:get: self._state_modules[module_name] is nil, make sure you have called gamestate_proxy:require_gamestates before") + assert(self._state_modules[module_name] ~= nil, "gamestate_proxy:get: self._state_modules['"..module_name.."'] is nil, make sure you have called gamestate_proxy:require_gamestates before") assert(type(self._state_modules[module_name]) == "table" and self._state_modules[module_name].state, "gamestate_proxy:get: self._state_modules[module_name] is not a function with a 'state' member") return self._state_modules[module_name].state end From 0e6b355b356685dad02144f07e773dc646ea3514 Mon Sep 17 00:00:00 2001 From: hsandt Date: Sun, 21 Apr 2019 00:40:04 +0100 Subject: [PATCH 115/140] [TEST] Render itests are now in busted to debug trace --- src/game/itests/itestplayercharacter.lua | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/game/itests/itestplayercharacter.lua b/src/game/itests/itestplayercharacter.lua index c434e051..78531ffe 100644 --- a/src/game/itests/itestplayercharacter.lua +++ b/src/game/itests/itestplayercharacter.lua @@ -598,12 +598,9 @@ expect pc_velocity 0 0 -- 7 (4, 32 - 16) (0, 0) airborne hit ceiling ---[=[#pico8 - - --- human tests --- pico8 only, since human must check rendering --- they have no final assertion, let the user check if result is correct or not (note it will display success whatever) +-- human tests: let human check rendering (until I find a way to automate this) +-- they have no final assertion, and will always succeed +-- although it's about rendering, we don't strip them from busted headless itests so we can debug the trace without having to run pico8 -- bugfix history: -- = fixed character pivot computed from drawn sprite topleft (with some gap above character's head) @@ -656,5 +653,3 @@ warp 4 8 move right wait 60 ]]) - ---#pico8]=] From deb145146807b56717b59a73de5ead7ccf9ec1ef Mon Sep 17 00:00:00 2001 From: hsandt Date: Sun, 21 Apr 2019 00:42:35 +0100 Subject: [PATCH 116/140] [TEST] Fixed tests for gamestate_proxy and integrationtest after last commits --- tests/utestgamestate_proxy.lua | 2 +- tests/utestintegrationtest.lua | 382 ++++++++++++++++----------------- 2 files changed, 192 insertions(+), 192 deletions(-) diff --git a/tests/utestgamestate_proxy.lua b/tests/utestgamestate_proxy.lua index 0684d91f..8bfe88be 100644 --- a/tests/utestgamestate_proxy.lua +++ b/tests/utestgamestate_proxy.lua @@ -30,7 +30,7 @@ describe('gamestate_proxy', function () assert.has_error(function () gamestate_proxy:get("invalid") end, - "gamestate_proxy:get: self._state_modules[module_name] is nil, make sure you have called gamestate_proxy:require_gamestates before") + "gamestate_proxy:get: self._state_modules['invalid'] is nil, make sure you have called gamestate_proxy:require_gamestates before") end) describe('(when modules have been required)', function () diff --git a/tests/utestintegrationtest.lua b/tests/utestintegrationtest.lua index c80ba2de..b11f9b19 100644 --- a/tests/utestintegrationtest.lua +++ b/tests/utestintegrationtest.lua @@ -91,7 +91,7 @@ describe('itest_manager', function () describe('init_game_and_start_by_index', function () setup(function () - itest_runner_own_method = stub(integration_test_runner, "init_game_and_start") + itest_runner_own_method = stub(itest_runner, "init_game_and_start") end) teardown(function () @@ -107,7 +107,7 @@ describe('itest_manager', function () itest_manager:register(itest) itest_manager:init_game_and_start_by_index(1) assert.spy(itest_runner_own_method).was_called(1) - assert.spy(itest_runner_own_method).was_called_with(match.ref(integration_test_runner), itest) + assert.spy(itest_runner_own_method).was_called_with(match.ref(itest_runner), itest) end) it('should assert if the index is invalid', function () @@ -124,7 +124,7 @@ describe('itest_manager', function () end) -describe('integration_test_runner', function () +describe('itest_runner', function () local test @@ -134,7 +134,7 @@ describe('integration_test_runner', function () after_each(function () -- full reset - integration_test_runner:init() + itest_runner:init() input.mode = input_modes.native logging.logger:init() end) @@ -144,7 +144,7 @@ describe('integration_test_runner', function () setup(function () gameapp_init_stub = stub(gameapp, "init") gameapp_reinit_modules_stub = stub(gameapp, "reinit_modules") - itest_runner_start_stub = stub(integration_test_runner, "start") + itest_runner_start_stub = stub(itest_runner, "start") end) teardown(function () @@ -160,19 +160,19 @@ describe('integration_test_runner', function () end) it('should init the gameapp and the passed test', function () - integration_test_runner:init_game_and_start(test) + itest_runner:init_game_and_start(test) assert.spy(gameapp_init_stub).was_called(1) assert.spy(gameapp_init_stub).was_called_with({'stage'}) assert.spy(itest_runner_start_stub).was_called(1) - assert.spy(itest_runner_start_stub).was_called_with(match.ref(integration_test_runner), test) + assert.spy(itest_runner_start_stub).was_called_with(match.ref(itest_runner), test) end) describe('(when another test was running)', function () it('should reinit the gameapp modules first', function () - integration_test_runner.current_test = integration_test('previous test', {}) + itest_runner.current_test = integration_test('previous test', {}) - integration_test_runner:init_game_and_start(test) + itest_runner:init_game_and_start(test) assert.spy(gameapp_reinit_modules_stub).was_called(1) assert.spy(gameapp_reinit_modules_stub).was_called_with() @@ -186,25 +186,25 @@ describe('integration_test_runner', function () setup(function () gameapp_update_stub = stub(gameapp, "update") - spy.on(integration_test_runner, "update") + spy.on(itest_runner, "update") end) teardown(function () gameapp_update_stub:revert() - integration_test_runner.update:revert() + itest_runner.update:revert() end) after_each(function () gameapp_update_stub:clear() - integration_test_runner.update:clear() + itest_runner.update:clear() end) describe('(when state is not running)', function () it('should do nothing', function () - integration_test_runner:update_game_and_test() + itest_runner:update_game_and_test() assert.spy(gameapp_update_stub).was_not_called() - assert.spy(integration_test_runner.update).was_not_called() + assert.spy(itest_runner.update).was_not_called() end) end) @@ -213,15 +213,15 @@ describe('integration_test_runner', function () before_each(function () test:add_action(time_trigger(1.0), function () end, 'some_action') - integration_test_runner:start(test) + itest_runner:start(test) end) it('should update the gameapp and the passed test', function () - integration_test_runner:update_game_and_test() + itest_runner:update_game_and_test() assert.spy(gameapp_update_stub).was_called(1) assert.spy(gameapp_update_stub).was_called_with() - assert.spy(integration_test_runner.update).was_called(1) - assert.spy(integration_test_runner.update).was_called_with(match.ref(integration_test_runner)) + assert.spy(itest_runner.update).was_called(1) + assert.spy(itest_runner.update).was_called_with(match.ref(itest_runner)) end) end) @@ -232,7 +232,7 @@ describe('integration_test_runner', function () before_each(function () test:add_action(time_trigger(0.017), function () end, 'some_action') - integration_test_runner:start(test) + itest_runner:start(test) end) setup(function () @@ -248,7 +248,7 @@ describe('integration_test_runner', function () end) it('should only log the result', function () - integration_test_runner:update_game_and_test() + itest_runner:update_game_and_test() assert.spy(log_stub).was_called() -- we only want 1 call, but we check "at least once" because there are other unrelated logs assert.spy(log_stub).was_called_with("itest 'character walks' ended with success", "itest") end) @@ -264,7 +264,7 @@ describe('integration_test_runner', function () test.final_assertion = function () return false, "character walks failed" end - integration_test_runner:start(test) + itest_runner:start(test) end) setup(function () @@ -280,7 +280,7 @@ describe('integration_test_runner', function () end) it('should log the result and failure message', function () - integration_test_runner:update_game_and_test() + itest_runner:update_game_and_test() assert.spy(log_stub).was_called() -- we only want 2 calls, but we check "at least twice" because there are other unrelated logs assert.spy(log_stub).was_called_with("itest 'character walks' ended with failure", "itest") assert.spy(log_stub).was_called_with("failed: character walks failed", "itest") @@ -294,7 +294,7 @@ describe('integration_test_runner', function () setup(function () gameapp_draw_stub = stub(gameapp, "draw") - itest_runner_draw_stub = stub(integration_test_runner, "draw") + itest_runner_draw_stub = stub(itest_runner, "draw") end) teardown(function () @@ -308,11 +308,11 @@ describe('integration_test_runner', function () end) it('should draw the gameapp and the passed test information', function () - integration_test_runner:draw_game_and_test() + itest_runner:draw_game_and_test() assert.spy(gameapp_draw_stub).was_called(1) assert.spy(gameapp_draw_stub).was_called_with() assert.spy(itest_runner_draw_stub).was_called(1) - assert.spy(itest_runner_draw_stub).was_called_with(match.ref(integration_test_runner)) + assert.spy(itest_runner_draw_stub).was_called_with(match.ref(itest_runner)) end) end) @@ -320,15 +320,15 @@ describe('integration_test_runner', function () describe('start', function () setup(function () - spy.on(integration_test_runner, "_initialize") - spy.on(integration_test_runner, "_check_end") - spy.on(integration_test_runner, "_check_next_action") + spy.on(itest_runner, "_initialize") + spy.on(itest_runner, "_check_end") + spy.on(itest_runner, "_check_next_action") end) teardown(function () - integration_test_runner._initialize:revert() - integration_test_runner._check_end:revert() - integration_test_runner._check_next_action:revert() + itest_runner._initialize:revert() + itest_runner._check_end:revert() + itest_runner._check_next_action:revert() end) before_each(function () @@ -336,53 +336,53 @@ describe('integration_test_runner', function () end) after_each(function () - integration_test_runner._initialize:clear() - integration_test_runner._check_end:clear() - integration_test_runner._check_next_action:clear() + itest_runner._initialize:clear() + itest_runner._check_end:clear() + itest_runner._check_next_action:clear() end) it('should set the current test to the passed test', function () - integration_test_runner:start(test) - assert.are_equal(test, integration_test_runner.current_test) + itest_runner:start(test) + assert.are_equal(test, itest_runner.current_test) end) it('should initialize state vars', function () - integration_test_runner:start(test) + itest_runner:start(test) assert.are_same({0, 0, 1}, { - integration_test_runner.current_frame, - integration_test_runner._last_trigger_frame, - integration_test_runner._next_action_index + itest_runner.current_frame, + itest_runner._last_trigger_frame, + itest_runner._next_action_index }) end) it('should call the test setup callback', function () - integration_test_runner:start(test) + itest_runner:start(test) assert.spy(test.setup).was_called(1) assert.spy(test.setup).was_called_with() end) it('should call _initialize the first time', function () - integration_test_runner:start(test) - assert.spy(integration_test_runner._initialize).was_called(1) - assert.spy(integration_test_runner._initialize).was_called_with(match.ref(integration_test_runner)) + itest_runner:start(test) + assert.spy(itest_runner._initialize).was_called(1) + assert.spy(itest_runner._initialize).was_called_with(match.ref(itest_runner)) end) it('should call _check_end', function () - integration_test_runner:start(test) - assert.spy(integration_test_runner._check_end).was_called(1) - assert.spy(integration_test_runner._check_end).was_called_with(match.ref(integration_test_runner)) + itest_runner:start(test) + assert.spy(itest_runner._check_end).was_called(1) + assert.spy(itest_runner._check_end).was_called_with(match.ref(itest_runner)) end) describe('(when no actions)', function () it('should not check the next action', function () - integration_test_runner:start(test) - assert.spy(integration_test_runner._check_next_action).was_not_called() + itest_runner:start(test) + assert.spy(itest_runner._check_next_action).was_not_called() end) it('should immediately end the run (result depends on final assertion)', function () - integration_test_runner:start(test) - assert.are_not_equal(test_states.running, integration_test_runner.current_state) + itest_runner:start(test) + assert.are_not_equal(test_states.running, itest_runner.current_state) end) end) @@ -394,14 +394,14 @@ describe('integration_test_runner', function () end) it('should check the next action immediately (if at time 0, will also call it)', function () - integration_test_runner:start(test) - assert.spy(integration_test_runner._check_next_action).was_called(1) - assert.spy(integration_test_runner._check_next_action).was_called_with(match.ref(integration_test_runner)) + itest_runner:start(test) + assert.spy(itest_runner._check_next_action).was_called(1) + assert.spy(itest_runner._check_next_action).was_called_with(match.ref(itest_runner)) end) it('should enter running state', function () - integration_test_runner:start(test) - assert.are_equal(test_states.running, integration_test_runner.current_state) + itest_runner:start(test) + assert.are_equal(test_states.running, itest_runner.current_state) end) end) @@ -411,28 +411,28 @@ describe('integration_test_runner', function () before_each(function () test:add_action(time_trigger(1.0), function () end, 'restart_action') -- some progress - integration_test_runner:start(test) + itest_runner:start(test) repeat_callback(1.0, function () - integration_test_runner:update() + itest_runner:update() end) end) it('should automatically stop before restarting, effectively resetting state vars but the current test and state', function () - integration_test_runner:start(test) + itest_runner:start(test) assert.are_same({0, 0, 1, test_states.running}, { - integration_test_runner.current_frame, - integration_test_runner._last_trigger_frame, - integration_test_runner._next_action_index, - integration_test_runner.current_state + itest_runner.current_frame, + itest_runner._last_trigger_frame, + itest_runner._next_action_index, + itest_runner.current_state }) end) it('should not call _initialize the second time', function () -- in this specific case, start was called in before_each so we need to clear manually -- just before we call start ourselves to have the correct count - integration_test_runner._initialize:clear() - integration_test_runner:start(test) - assert.spy(integration_test_runner._initialize).was_not_called() + itest_runner._initialize:clear() + itest_runner:start(test) + assert.spy(itest_runner._initialize).was_not_called() end) end) @@ -443,9 +443,9 @@ describe('integration_test_runner', function () it('should assert when no test has been started', function () assert.has_error(function() - integration_test_runner:update() + itest_runner:update() end, - "integration_test_runner:update: current_test is not set") + "itest_runner:update: current_test is not set") end) describe('(after test started)', function () @@ -462,7 +462,7 @@ describe('integration_test_runner', function () end) before_each(function () - integration_test_runner:start(test) + itest_runner:start(test) end) after_each(function () @@ -470,41 +470,41 @@ describe('integration_test_runner', function () end) it('should advance the current time by 1', function () - integration_test_runner:update() - assert.are_equal(1, integration_test_runner.current_frame) + itest_runner:update() + assert.are_equal(1, itest_runner.current_frame) end) it('should call an initial action (t=0.) immediately, preserving last trigger time to 0 and incrementing the _next_action_index', function () - integration_test_runner:update() + itest_runner:update() assert.spy(action_callback).was_not_called() - assert.are_equal(0., integration_test_runner._last_trigger_frame) - assert.are_equal(1, integration_test_runner._next_action_index) + assert.are_equal(0., itest_runner._last_trigger_frame) + assert.are_equal(1, itest_runner._next_action_index) end) it('should not call a later action (t=1.02) before the expected time (1.0s)', function () repeat_callback(1.0, function () - integration_test_runner:update() + itest_runner:update() end) assert.spy(action_callback).was_not_called() - assert.are_equal(0., integration_test_runner._last_trigger_frame) - assert.are_equal(1, integration_test_runner._next_action_index) + assert.are_equal(0., itest_runner._last_trigger_frame) + assert.are_equal(1, itest_runner._next_action_index) end) it('should call a later action (t=1.02) after the action time has been reached', function () repeat_callback(1.02, function () - integration_test_runner:update() + itest_runner:update() end) assert.spy(action_callback).was_called(1) - assert.are_equal(61, integration_test_runner._last_trigger_frame) - assert.are_equal(2, integration_test_runner._next_action_index) + assert.are_equal(61, itest_runner._last_trigger_frame) + assert.are_equal(2, itest_runner._next_action_index) end) it('should end the test once the last action has been applied', function () repeat_callback(1.02, function () - integration_test_runner:update() + itest_runner:update() end) - assert.are_equal(test_states.success, integration_test_runner.current_state) - assert.are_equal(2, integration_test_runner._next_action_index) + assert.are_equal(test_states.success, itest_runner.current_state) + assert.are_equal(2, itest_runner._next_action_index) end) describe('(with timeout set to 2s and more actions after that, usually unmet conditions)', function () @@ -517,12 +517,12 @@ describe('integration_test_runner', function () describe('(when next frame is below 120)', function () before_each(function () - integration_test_runner.current_frame = 118 + itest_runner.current_frame = 118 end) it('should call next action (no time out)', function () - integration_test_runner:update() - assert.are_equal(test_states.running, integration_test_runner.current_state) + itest_runner:update() + assert.are_equal(test_states.running, itest_runner.current_state) assert.spy(action_callback).was_called(1) end) @@ -531,12 +531,12 @@ describe('integration_test_runner', function () describe('(when next frame is 120 or above)', function () before_each(function () - integration_test_runner.current_frame = 119 + itest_runner.current_frame = 119 end) it('should time out without calling next action', function () - integration_test_runner:update() - assert.are_equal(test_states.timeout, integration_test_runner.current_state) + itest_runner:update() + assert.are_equal(test_states.timeout, itest_runner.current_state) assert.spy(action_callback).was_not_called() end) @@ -550,13 +550,13 @@ describe('integration_test_runner', function () before_each(function () -- without any action, start should end the test immediately - integration_test_runner:start(test) + itest_runner:start(test) end) it('should do nothing', function () - assert.are_equal(integration_test_runner.current_state, test_states.success) - assert.has_no_errors(function () integration_test_runner:update() end) - assert.are_equal(integration_test_runner.current_state, test_states.success) + assert.are_equal(itest_runner.current_state, test_states.success) + assert.has_no_errors(function () itest_runner:update() end) + assert.are_equal(itest_runner.current_state, test_states.success) end) end) @@ -580,19 +580,19 @@ describe('integration_test_runner', function () end) it('should draw "no itest running"', function () - integration_test_runner:draw() + itest_runner:draw() assert.spy(api_print_stub).was_called(1) end) describe('(when current test is set)', function () before_each(function () - integration_test_runner.current_test = test - integration_test_runner.current_state = test_states.running + itest_runner.current_test = test + itest_runner.current_state = test_states.running end) it('should draw information on the current test', function () - integration_test_runner:draw() + itest_runner:draw() assert.spy(api_print_stub).was_called(2) end) @@ -605,23 +605,23 @@ describe('integration_test_runner', function () describe('_get_test_state_color', function () it('should return white for none', function () - assert.are_equal(colors.white, integration_test_runner:_get_test_state_color(test_states.none)) + assert.are_equal(colors.white, itest_runner:_get_test_state_color(test_states.none)) end) it('should return white for none', function () - assert.are_equal(colors.white, integration_test_runner:_get_test_state_color(test_states.running)) + assert.are_equal(colors.white, itest_runner:_get_test_state_color(test_states.running)) end) it('should return green for success', function () - assert.are_equal(colors.green, integration_test_runner:_get_test_state_color(test_states.success)) + assert.are_equal(colors.green, itest_runner:_get_test_state_color(test_states.success)) end) it('should return red for failure', function () - assert.are_equal(colors.red, integration_test_runner:_get_test_state_color(test_states.failure)) + assert.are_equal(colors.red, itest_runner:_get_test_state_color(test_states.failure)) end) it('should return dark purple for timeout', function () - assert.are_equal(colors.dark_purple, integration_test_runner:_get_test_state_color(test_states.timeout)) + assert.are_equal(colors.dark_purple, itest_runner:_get_test_state_color(test_states.timeout)) end) end) @@ -629,12 +629,12 @@ describe('integration_test_runner', function () describe('_initialize', function () it('should set the input mode to simulated', function () - integration_test_runner:_initialize() + itest_runner:_initialize() assert.are_equal(input_modes.simulated, input.mode) end) it('should set all logger categories (except itest, but that\'s only visible in pico8 build)', function () - integration_test_runner:_initialize() + itest_runner:_initialize() -- hack until we implement #82 TEST integration-busted-trace-build-system -- since "trace" is not set in data but in code in _initialize, -- it promises to change often during development so we "hide" such tuning in code @@ -645,15 +645,15 @@ describe('integration_test_runner', function () player = false, ui = false, codetuner = false, - itest = false, -- would be true in pico8 itests + itest = true, -- now true for both pico8 and busted tests trace = false -- forced to false for this test }, logging.logger.active_categories) end) it('should set initialized to true', function () - integration_test_runner:_initialize() - assert.is_true(integration_test_runner.initialized) + itest_runner:_initialize() + assert.is_true(itest_runner.initialized) end) end) @@ -668,47 +668,47 @@ describe('integration_test_runner', function () setup(function () -- don't stub a function if the return value matters, as in start - spy.on(integration_test_runner, "_check_end") + spy.on(itest_runner, "_check_end") end) teardown(function () action_callback:revert() action_callback2:revert() - integration_test_runner._check_end:revert() + itest_runner._check_end:revert() end) before_each(function () - integration_test_runner:start(test) + itest_runner:start(test) test:add_action(time_trigger(1.0), action_callback, 'action_callback') end) after_each(function () action_callback:clear() action_callback2:clear() - integration_test_runner._check_end:clear() + itest_runner._check_end:clear() end) describe('(when next action index is 1/1)', function () before_each(function () - integration_test_runner._next_action_index = 1 + itest_runner._next_action_index = 1 end) describe('(when next action time trigger is not reached yet)', function () before_each(function () -- time trigger uses relative frames, so compare the difference since last trigger to 60 - integration_test_runner.current_frame = 158 - integration_test_runner._last_trigger_frame = 100 + itest_runner.current_frame = 158 + itest_runner._last_trigger_frame = 100 end) it('should not call the action nor advance the time/index', function () - integration_test_runner._check_end:clear() -- was called on start in before_each - integration_test_runner:_check_next_action() + itest_runner._check_end:clear() -- was called on start in before_each + itest_runner:_check_next_action() assert.spy(action_callback).was_not_called() - assert.are_equal(100, integration_test_runner._last_trigger_frame) - assert.are_equal(1, integration_test_runner._next_action_index) - assert.spy(integration_test_runner._check_end).was_not_called() + assert.are_equal(100, itest_runner._last_trigger_frame) + assert.are_equal(1, itest_runner._next_action_index) + assert.spy(itest_runner._check_end).was_not_called() end) end) @@ -717,19 +717,19 @@ describe('integration_test_runner', function () before_each(function () -- time trigger uses relative frames, so compare the difference since last trigger to 60 - integration_test_runner.current_frame = 160 - integration_test_runner._last_trigger_frame = 100 + itest_runner.current_frame = 160 + itest_runner._last_trigger_frame = 100 end) it('should call the action and advance the timeindex', function () - integration_test_runner._check_end:clear() -- was called on start in before_each - integration_test_runner:_check_next_action() + itest_runner._check_end:clear() -- was called on start in before_each + itest_runner:_check_next_action() assert.spy(action_callback).was_called(1) assert.spy(action_callback).was_called_with() - assert.are_equal(160, integration_test_runner._last_trigger_frame) - assert.are_equal(2, integration_test_runner._next_action_index) - assert.spy(integration_test_runner._check_end).was_called(1) - assert.spy(integration_test_runner._check_end).was_called_with(match.ref(integration_test_runner)) + assert.are_equal(160, itest_runner._last_trigger_frame) + assert.are_equal(2, itest_runner._next_action_index) + assert.spy(itest_runner._check_end).was_called(1) + assert.spy(itest_runner._check_end).was_called_with(match.ref(itest_runner)) end) end) @@ -740,12 +740,12 @@ describe('integration_test_runner', function () before_each(function () -- we still have the dummy action from the outer scope - integration_test_runner._next_action_index = 2 -- we are now at 2/1 + itest_runner._next_action_index = 2 -- we are now at 2/1 end) it('should assert', function () assert.has_error(function () - integration_test_runner:_check_next_action() + itest_runner:_check_next_action() end, "self._next_action_index (2) is out of bounds for self.current_test.action_sequence (size 1)") end) @@ -757,7 +757,7 @@ describe('integration_test_runner', function () describe('(when next action index is 1/1)', function () before_each(function () - integration_test_runner._next_action_index = 1 + itest_runner._next_action_index = 1 end) describe('(when next action time trigger is not reached yet)', function () @@ -765,18 +765,18 @@ describe('integration_test_runner', function () before_each(function () -- time trigger uses relative frames, so compare the difference since last trigger to 60 test:add_action(time_trigger(0.0), action_callback2, 'action_callback2') - integration_test_runner.current_frame = 158 - integration_test_runner._last_trigger_frame = 100 + itest_runner.current_frame = 158 + itest_runner._last_trigger_frame = 100 end) it('should not call any actions nor advance the time/index', function () - integration_test_runner._check_end:clear() -- was called on start in before_each - integration_test_runner:_check_next_action() + itest_runner._check_end:clear() -- was called on start in before_each + itest_runner:_check_next_action() assert.spy(action_callback).was_not_called() assert.spy(action_callback2).was_not_called() - assert.are_equal(100, integration_test_runner._last_trigger_frame) - assert.are_equal(1, integration_test_runner._next_action_index) - assert.spy(integration_test_runner._check_end).was_not_called() + assert.are_equal(100, itest_runner._last_trigger_frame) + assert.are_equal(1, itest_runner._next_action_index) + assert.spy(itest_runner._check_end).was_not_called() end) end) @@ -786,21 +786,21 @@ describe('integration_test_runner', function () before_each(function () -- time trigger uses relative frames, so compare the difference since last trigger to 60 test:add_action(time_trigger(0.0), action_callback2, 'action_callback2') - integration_test_runner.current_frame = 160 - integration_test_runner._last_trigger_frame = 100 + itest_runner.current_frame = 160 + itest_runner._last_trigger_frame = 100 end) it('should call both actions and advance the timeindex by 2', function () - integration_test_runner._check_end:clear() -- was called on start in before_each - integration_test_runner:_check_next_action() + itest_runner._check_end:clear() -- was called on start in before_each + itest_runner:_check_next_action() assert.spy(action_callback).was_called(1) assert.spy(action_callback).was_called_with() assert.spy(action_callback2).was_called(1) -- thx to action chaining when next action time is 0 assert.spy(action_callback2).was_called_with() - assert.are_equal(160, integration_test_runner._last_trigger_frame) - assert.are_equal(3, integration_test_runner._next_action_index) -- after action 2 - assert.spy(integration_test_runner._check_end).was_called(2) -- checked after each action - assert.spy(integration_test_runner._check_end).was_called_with(match.ref(integration_test_runner)) + assert.are_equal(160, itest_runner._last_trigger_frame) + assert.are_equal(3, itest_runner._next_action_index) -- after action 2 + assert.spy(itest_runner._check_end).was_called(2) -- checked after each action + assert.spy(itest_runner._check_end).was_called_with(match.ref(itest_runner)) end) end) @@ -814,7 +814,7 @@ describe('integration_test_runner', function () describe('(when next action index is 1/1)', function () before_each(function () - integration_test_runner._next_action_index = 1 + itest_runner._next_action_index = 1 end) describe('(when next action time trigger is not reached yet)', function () @@ -822,18 +822,18 @@ describe('integration_test_runner', function () before_each(function () -- time trigger uses relative frames, so compare the difference since last trigger to 60 test:add_action(time_trigger(0.2), action_callback2, 'action_callback2') - integration_test_runner.current_frame = 158 - integration_test_runner._last_trigger_frame = 100 + itest_runner.current_frame = 158 + itest_runner._last_trigger_frame = 100 end) it('should not call any actions nor advance the time/index', function () - integration_test_runner._check_end:clear() -- was called on start in before_each - integration_test_runner:_check_next_action() + itest_runner._check_end:clear() -- was called on start in before_each + itest_runner:_check_next_action() assert.spy(action_callback).was_not_called() assert.spy(action_callback2).was_not_called() - assert.are_equal(100, integration_test_runner._last_trigger_frame) - assert.are_equal(1, integration_test_runner._next_action_index) - assert.spy(integration_test_runner._check_end).was_not_called() + assert.are_equal(100, itest_runner._last_trigger_frame) + assert.are_equal(1, itest_runner._next_action_index) + assert.spy(itest_runner._check_end).was_not_called() end) end) @@ -843,20 +843,20 @@ describe('integration_test_runner', function () before_each(function () -- time trigger uses relative frames, so compare the difference since last trigger to 60 test:add_action(time_trigger(0.2), action_callback2, 'action_callback2') - integration_test_runner.current_frame = 160 - integration_test_runner._last_trigger_frame = 100 + itest_runner.current_frame = 160 + itest_runner._last_trigger_frame = 100 end) it('should call only the first action and advance the timeindex', function () - integration_test_runner._check_end:clear() -- was called on start in before_each - integration_test_runner:_check_next_action() + itest_runner._check_end:clear() -- was called on start in before_each + itest_runner:_check_next_action() assert.spy(action_callback).was_called(1) assert.spy(action_callback).was_called_with() assert.spy(action_callback2).was_not_called() -- at least 1 frame before action2, no action chaining - assert.are_equal(160, integration_test_runner._last_trigger_frame) - assert.are_equal(2, integration_test_runner._next_action_index) - assert.spy(integration_test_runner._check_end).was_called(1) - assert.spy(integration_test_runner._check_end).was_called_with(match.ref(integration_test_runner)) + assert.are_equal(160, itest_runner._last_trigger_frame) + assert.are_equal(2, itest_runner._next_action_index) + assert.spy(itest_runner._check_end).was_called(1) + assert.spy(itest_runner._check_end).was_called_with(match.ref(itest_runner)) end) end) @@ -875,11 +875,11 @@ describe('integration_test_runner', function () end) it('should recognize next empty action and do nothing', function () - integration_test_runner:start(test) - integration_test_runner.current_frame = 2 -- to trigger action to do at end of frame 1 + itest_runner:start(test) + itest_runner.current_frame = 2 -- to trigger action to do at end of frame 1 assert.has_no_errors(function () - integration_test_runner:_check_next_action() + itest_runner:_check_next_action() end) end) @@ -890,7 +890,7 @@ describe('integration_test_runner', function () describe('_check_end', function () before_each(function () - integration_test_runner:start(test) + itest_runner:start(test) end) describe('(when no actions left)', function () @@ -898,10 +898,10 @@ describe('integration_test_runner', function () describe('(when no final assertion)', function () it('should make test end immediately with success and return true', function () - local result = integration_test_runner:_check_end(test) + local result = itest_runner:_check_end(test) assert.is_true(result) assert.are_same({test_states.success, nil}, - {integration_test_runner.current_state, integration_test_runner.current_message}) + {itest_runner.current_state, itest_runner.current_message}) end) end) @@ -915,10 +915,10 @@ describe('integration_test_runner', function () end) it('should check the final assertion immediately, end with success and return true', function () - local result = integration_test_runner:_check_end(test) + local result = itest_runner:_check_end(test) assert.is_true(result) assert.are_same({test_states.success, nil}, - {integration_test_runner.current_state, integration_test_runner.current_message}) + {itest_runner.current_state, itest_runner.current_message}) end) end) @@ -932,9 +932,9 @@ describe('integration_test_runner', function () end) it('should check the final assertion immediately, end with failure and return true', function () - local result = integration_test_runner:_check_end(test) + local result = itest_runner:_check_end(test) assert.is_true(result) - assert.are_equal(test_states.failure, integration_test_runner.current_state) + assert.are_equal(test_states.failure, itest_runner.current_state) end) end) @@ -948,7 +948,7 @@ describe('integration_test_runner', function () end) it('should return false', function () - assert.is_false(integration_test_runner:_check_end(test)) + assert.is_false(itest_runner:_check_end(test)) end) end) @@ -958,19 +958,19 @@ describe('integration_test_runner', function () describe('_end_with_final_assertion', function () before_each(function () - -- inline some parts of integration_test_runner:start(test) + -- inline some parts of itest_runner:start(test) -- to get a boilerplate to test on -- avoid calling start() directly as it would call _check_end, messing the teardown spy count - integration_test_runner:_initialize() - integration_test_runner.current_test = test - integration_test_runner.current_state = test_states.running + itest_runner:_initialize() + itest_runner.current_test = test + itest_runner.current_state = test_states.running end) describe('(when no final assertion)', function () it('should end with success', function () - integration_test_runner:_end_with_final_assertion(test) - assert.are_equal(test_states.success, integration_test_runner.current_state) + itest_runner:_end_with_final_assertion(test) + assert.are_equal(test_states.success, itest_runner.current_state) end) end) @@ -984,8 +984,8 @@ describe('integration_test_runner', function () end) it('should check the final assertion and end with success', function () - integration_test_runner:_end_with_final_assertion(test) - assert.are_equal(test_states.success, integration_test_runner.current_state) + itest_runner:_end_with_final_assertion(test) + assert.are_equal(test_states.success, itest_runner.current_state) end) end) @@ -999,9 +999,9 @@ describe('integration_test_runner', function () end) it('should check the final assertion and end with failure', function () - integration_test_runner:_end_with_final_assertion(test) + itest_runner:_end_with_final_assertion(test) assert.are_same({test_states.failure, "error message"}, - {integration_test_runner.current_state, integration_test_runner.current_message}) + {itest_runner.current_state, itest_runner.current_message}) end) end) @@ -1011,21 +1011,21 @@ describe('integration_test_runner', function () describe('stop', function () before_each(function () - integration_test_runner:start(test) + itest_runner:start(test) end) it('should reset the current test', function () - integration_test_runner:stop(test) - assert.is_nil(integration_test_runner.current_test) + itest_runner:stop(test) + assert.is_nil(itest_runner.current_test) end) it('should reset state vars', function () - integration_test_runner:stop(test) + itest_runner:stop(test) assert.are_same({0, 0, 1, test_states.none}, { - integration_test_runner.current_frame, - integration_test_runner._last_trigger_frame, - integration_test_runner._next_action_index, - integration_test_runner.current_state + itest_runner.current_frame, + itest_runner._last_trigger_frame, + itest_runner._next_action_index, + itest_runner.current_state }) end) @@ -1037,7 +1037,7 @@ describe('integration_test_runner', function () end) it('should call teardown', function () - integration_test_runner:stop(test) + itest_runner:stop(test) assert.spy(test.teardown).was_called(1) assert.spy(test.teardown).was_called_with() end) From e2614f3b0a171bacc29040d4d1575608d6109bfb Mon Sep 17 00:00:00 2001 From: hsandt Date: Sun, 21 Apr 2019 00:42:53 +0100 Subject: [PATCH 117/140] [TEST] Consistently rename to itest_runner --- src/game/itest_main.lua | 10 +++++----- tests/itests/utestheadless_itests.lua | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/game/itest_main.lua b/src/game/itest_main.lua index fdbc460f..4f48aa65 100644 --- a/src/game/itest_main.lua +++ b/src/game/itest_main.lua @@ -33,11 +33,11 @@ end function _update60() handle_input() - integration_test_runner:update_game_and_test() + itest_runner:update_game_and_test() end function _draw() - integration_test_runner:draw_game_and_test() + itest_runner:draw_game_and_test() end function init_game_and_start_next_itest() @@ -76,9 +76,9 @@ function handle_input() return end - if integration_test_runner.current_state == test_states.success or - integration_test_runner.current_state == test_states.failure or - integration_test_runner.current_state == test_states.timeout then + if itest_runner.current_state == test_states.success or + itest_runner.current_state == test_states.failure or + itest_runner.current_state == test_states.timeout then -- previous itest has finished, wait for x press to continue to next itest if btnp(button_ids.x) then init_game_and_start_next_itest() diff --git a/tests/itests/utestheadless_itests.lua b/tests/itests/utestheadless_itests.lua index 7313ba83..539180f3 100644 --- a/tests/itests/utestheadless_itests.lua +++ b/tests/itests/utestheadless_itests.lua @@ -43,19 +43,19 @@ describe('headless itest', function () it(itest.name..' should succeed', function () itest_manager:init_game_and_start_by_index(i) - while integration_test_runner.current_state == test_states.running do - integration_test_runner:update_game_and_test() + while itest_runner.current_state == test_states.running do + itest_runner:update_game_and_test() if should_render then - integration_test_runner:draw_game_and_test() + itest_runner:draw_game_and_test() end end local itest_fail_message = nil - if integration_test_runner.current_message then - itest_fail_message = "itest '"..itest.name.."' ended with "..integration_test_runner.current_state.." due to:\n"..integration_test_runner.current_message + if itest_runner.current_message then + itest_fail_message = "itest '"..itest.name.."' ended with "..itest_runner.current_state.." due to:\n"..itest_runner.current_message end - assert.are_equal(test_states.success, integration_test_runner.current_state, itest_fail_message) + assert.are_equal(test_states.success, itest_runner.current_state, itest_fail_message) end) From a46024d9f215a50e8c79d30d7aadfb2ac54a5237 Mon Sep 17 00:00:00 2001 From: hsandt Date: Sun, 21 Apr 2019 02:07:23 +0100 Subject: [PATCH 118/140] [BUILD] Shorter names to remain under 65536 char limit --- src/engine/core/math.lua | 10 +++++----- src/game/ingame/playercharacter.lua | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/engine/core/math.lua b/src/engine/core/math.lua index a99b76be..ae0152f8 100644 --- a/src/engine/core/math.lua +++ b/src/engine/core/math.lua @@ -315,11 +315,11 @@ function signed_speed_to_dir(signed_speed) return signed_speed < 0 and horizontal_dirs.left or horizontal_dirs.right end -function oppose_direction(direction) +function oppose_dir(direction) return (direction + 2) % 4 end -function mirror_direction_x(direction) +function mirror_dir_x(direction) if direction == directions.left then return directions.right elseif direction == directions.right then @@ -329,7 +329,7 @@ function mirror_direction_x(direction) end end -function mirror_direction_y(direction) +function mirror_dir_y(direction) if direction == directions.up then return directions.down elseif direction == directions.down then @@ -339,10 +339,10 @@ function mirror_direction_y(direction) end end -function rotate_direction_90_cw(direction) +function rotate_dir_90_cw(direction) return (direction + 1) % 4 end -function rotate_direction_90_ccw(direction) +function rotate_dir_90_ccw(direction) return (direction - 1) % 4 end diff --git a/src/game/ingame/playercharacter.lua b/src/game/ingame/playercharacter.lua index b6e28008..4f5e2ff2 100644 --- a/src/game/ingame/playercharacter.lua +++ b/src/game/ingame/playercharacter.lua @@ -895,7 +895,7 @@ function player_char:_advance_in_air_along(ref_motion_result, velocity, coord) direction = directions.down end if velocity[coord] < 0 then - direction = oppose_direction(direction) + direction = oppose_dir(direction) end local pixel_distance_before_step = 0 From 1ed6b0755f673b210155ff1200f70453519fcc2b Mon Sep 17 00:00:00 2001 From: hsandt Date: Sun, 21 Apr 2019 02:13:37 +0100 Subject: [PATCH 119/140] [TEST] Fix utestmath with shorter names --- tests/utestmath.lua | 50 ++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/tests/utestmath.lua b/tests/utestmath.lua index 9b24758a..f236a5c0 100644 --- a/tests/utestmath.lua +++ b/tests/utestmath.lua @@ -481,77 +481,77 @@ describe('signed_speed_to_dir', function () end) end) -describe('oppose_direction', function () +describe('oppose_dir', function () it('should left => right', function () - assert.are_equal(directions.right, oppose_direction(directions.left)) + assert.are_equal(directions.right, oppose_dir(directions.left)) end) it('should right => left', function () - assert.are_equal(directions.left, oppose_direction(directions.right)) + assert.are_equal(directions.left, oppose_dir(directions.right)) end) it('should up => down', function () - assert.are_equal(directions.down, mirror_direction_y(directions.up)) + assert.are_equal(directions.down, mirror_dir_y(directions.up)) end) it('should down => up', function () - assert.are_equal(directions.up, mirror_direction_y(directions.down)) + assert.are_equal(directions.up, mirror_dir_y(directions.down)) end) end) -describe('mirror_direction_x', function () +describe('mirror_dir_x', function () it('should left => right', function () - assert.are_equal(directions.right, mirror_direction_x(directions.left)) + assert.are_equal(directions.right, mirror_dir_x(directions.left)) end) it('should right => left', function () - assert.are_equal(directions.left, mirror_direction_x(directions.right)) + assert.are_equal(directions.left, mirror_dir_x(directions.right)) end) it('should up => up', function () - assert.are_equal(directions.up, mirror_direction_x(directions.up)) + assert.are_equal(directions.up, mirror_dir_x(directions.up)) end) it('should down => down', function () - assert.are_equal(directions.down, mirror_direction_x(directions.down)) + assert.are_equal(directions.down, mirror_dir_x(directions.down)) end) end) -describe('+ mirror_direction_y', function () +describe('+ mirror_dir_y', function () it('should up => down', function () - assert.are_equal(directions.down, mirror_direction_y(directions.up)) + assert.are_equal(directions.down, mirror_dir_y(directions.up)) end) it('should down => up', function () - assert.are_equal(directions.up, mirror_direction_y(directions.down)) + assert.are_equal(directions.up, mirror_dir_y(directions.down)) end) it('should left => left', function () - assert.are_equal(directions.left, mirror_direction_y(directions.left)) + assert.are_equal(directions.left, mirror_dir_y(directions.left)) end) it('should right => right', function () - assert.are_equal(directions.right, mirror_direction_y(directions.right)) + assert.are_equal(directions.right, mirror_dir_y(directions.right)) end) end) -describe('rotate_direction_90_cw', function () +describe('rotate_dir_90_cw', function () it('should left => up', function () - assert.are_equal(directions.up, rotate_direction_90_cw(directions.left)) + assert.are_equal(directions.up, rotate_dir_90_cw(directions.left)) end) it('should up => right', function () - assert.are_equal(directions.right, rotate_direction_90_cw(directions.up)) + assert.are_equal(directions.right, rotate_dir_90_cw(directions.up)) end) it('should right => down', function () - assert.are_equal(directions.down, rotate_direction_90_cw(directions.right)) + assert.are_equal(directions.down, rotate_dir_90_cw(directions.right)) end) it('should down => left', function () - assert.are_equal(directions.left, rotate_direction_90_cw(directions.down)) + assert.are_equal(directions.left, rotate_dir_90_cw(directions.down)) end) end) -describe('rotate_direction_90_ccw', function () +describe('rotate_dir_90_ccw', function () it('should left => down', function () - assert.are_equal(directions.down, rotate_direction_90_ccw(directions.left)) + assert.are_equal(directions.down, rotate_dir_90_ccw(directions.left)) end) it('should down => right', function () - assert.are_equal(directions.right, rotate_direction_90_ccw(directions.down)) + assert.are_equal(directions.right, rotate_dir_90_ccw(directions.down)) end) it('should right => up', function () - assert.are_equal(directions.up, rotate_direction_90_ccw(directions.right)) + assert.are_equal(directions.up, rotate_dir_90_ccw(directions.right)) end) it('should up => left', function () - assert.are_equal(directions.left, rotate_direction_90_ccw(directions.up)) + assert.are_equal(directions.left, rotate_dir_90_ccw(directions.up)) end) end) From 1db206a95c2eb4f83d72ee0be39414afc3eaff81 Mon Sep 17 00:00:00 2001 From: hsandt Date: Sun, 21 Apr 2019 02:16:55 +0100 Subject: [PATCH 120/140] [LOG] Stop tracing in main (slowdown when combined with visual logger) --- src/game/main.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/main.lua b/src/game/main.lua index a08c6295..192be9fc 100644 --- a/src/game/main.lua +++ b/src/game/main.lua @@ -5,7 +5,7 @@ local gamestate_proxy = require("game/application/gamestate_proxy") local logging = require("engine/debug/logging") logging.logger:register_stream(logging.console_log_stream) logging.logger:register_stream(logging.file_log_stream) -logging.logger.active_categories["trace"] = true +logging.logger.active_categories["trace"] = false --#if visual_logger local vlogger = require("engine/debug/visual_logger") From bc7d8d8caa4dfc8622a13369dce57bcfc2a672ce Mon Sep 17 00:00:00 2001 From: hsandt Date: Sun, 21 Apr 2019 02:17:13 +0100 Subject: [PATCH 121/140] [TEST] Fixed more tests with shorter names --- tests/utestcollision.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/utestcollision.lua b/tests/utestcollision.lua index 9a234279..e7f2e008 100644 --- a/tests/utestcollision.lua +++ b/tests/utestcollision.lua @@ -58,7 +58,7 @@ local function describe_all_test_variants(original_bb1, original_bb2, escape_vector:mul_inplace(-1) end if prioritized_escape_direction then - prioritized_escape_direction = oppose_direction(prioritized_escape_direction) + prioritized_escape_direction = oppose_dir(prioritized_escape_direction) end end @@ -72,7 +72,7 @@ local function describe_all_test_variants(original_bb1, original_bb2, escape_vector:mirror_x() end if prioritized_escape_direction then - prioritized_escape_direction = mirror_direction_x(prioritized_escape_direction) + prioritized_escape_direction = mirror_dir_x(prioritized_escape_direction) end end @@ -84,7 +84,7 @@ local function describe_all_test_variants(original_bb1, original_bb2, escape_vector:mirror_y() end if prioritized_escape_direction then - prioritized_escape_direction = mirror_direction_y(prioritized_escape_direction) + prioritized_escape_direction = mirror_dir_y(prioritized_escape_direction) end end @@ -97,7 +97,7 @@ local function describe_all_test_variants(original_bb1, original_bb2, escape_vector:rotate_90_cw_inplace() end if prioritized_escape_direction then - prioritized_escape_direction = rotate_direction_90_cw(prioritized_escape_direction) + prioritized_escape_direction = rotate_dir_90_cw(prioritized_escape_direction) end end From 49f047b35fbd05f32ee5f7f858fdcc3fb937c070 Mon Sep 17 00:00:00 2001 From: hsandt Date: Sun, 21 Apr 2019 02:19:31 +0100 Subject: [PATCH 122/140] [SPRITE] Improved default speed for run animation --- src/game/data/playercharacter_data.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/game/data/playercharacter_data.lua b/src/game/data/playercharacter_data.lua index 002ada83..c9879003 100644 --- a/src/game/data/playercharacter_data.lua +++ b/src/game/data/playercharacter_data.lua @@ -81,9 +81,9 @@ local playercharacter_data = { -- define animated sprite data in a second step, as it needs sprite data to be defined first playercharacter_data.sonic_animated_sprite_data_table = { - idle = animated_sprite_data.create(playercharacter_data.sonic_sprite_data_table, {"idle"}, 2, true), - run = animated_sprite_data.create(playercharacter_data.sonic_sprite_data_table, {"idle", "run1"}, 2, true), - spin = animated_sprite_data.create(playercharacter_data.sonic_sprite_data_table, {"spin"}, 2, true), + idle = animated_sprite_data.create(playercharacter_data.sonic_sprite_data_table, {"idle"}, 10, true), + run = animated_sprite_data.create(playercharacter_data.sonic_sprite_data_table, {"idle", "run1"}, 10, true), + spin = animated_sprite_data.create(playercharacter_data.sonic_sprite_data_table, {"spin"}, 10, true), } return playercharacter_data From d0007e8b82c71463aff3a45e5f9289268090e160 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 26 Apr 2019 17:35:51 +0100 Subject: [PATCH 123/140] [RUN] Improved running message by mentioning extra arguments --- run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run.sh b/run.sh index 1bb9afe9..51c13a57 100755 --- a/run.sh +++ b/run.sh @@ -18,7 +18,7 @@ define_build_vars $1 OUTPUT_FILEPATH="build/${OUTPUT_BASENAME}_$2.p8" -echo "Running ${OUTPUT_FILEPATH} with ${@:3}..." +echo "Running ${OUTPUT_FILEPATH} with extra args: '${@:3}'" # ${@:4} will pass remaining args after the first 2, such as "-x; exec bash # as a trick to run headless and keep terminal open" gnome-terminal -- bash -x -c "pico8 -run ${OUTPUT_FILEPATH} -gif_scale 4 ${@:3}" From e6a41931f5381b97aafca6133b01cd5c1d1f5fdb Mon Sep 17 00:00:00 2001 From: hsandt Date: Sat, 27 Apr 2019 01:38:19 +0100 Subject: [PATCH 124/140] [TEST] Added ".lua" to test file pattern to avoid testing/covering file with similar name --- test.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test.sh b/test.sh index e07c6893..c8b032e0 100755 --- a/test.sh +++ b/test.sh @@ -46,8 +46,9 @@ if [[ $MODULE = "all" || -z $MODULE ]] ; then TEST_FILE_PATTERN="utest" # all unit tests COVERAGE_OPTIONS="-c .luacov_all" # we cannot just use default .luacov since it would also affect specific module tests else - # prepend "utest" again in case a module name contains another one (e.g. logger c visual_logger) - TEST_FILE_PATTERN="utest$MODULE" + # prepend "utest" again, and append ".lua" in case a module name contains another one + # (e.g. logger for visual_logger or tile for tile_data) + TEST_FILE_PATTERN="utest$MODULE.lua" COVERAGE_OPTIONS="-c .luacov_current \"/$MODULE\"" fi From 83ebc118b14807d433696e812feb91cf9f0fb818 Mon Sep 17 00:00:00 2001 From: hsandt Date: Sat, 27 Apr 2019 01:40:55 +0100 Subject: [PATCH 125/140] [ENGINE] Separated engine/game by moving world, motion and tile (last two extracted from collision) to game side --- src/engine/data/tilemap.lua | 16 +- src/engine/physics/collision.lua | 153 ------------ src/engine/test/itest_dsl.lua | 2 +- src/game/data/collision_data.lua | 5 +- src/game/data/tile_data.lua | 12 - src/game/ingame/playercharacter.lua | 22 +- src/game/platformer/motion.lua | 86 +++++++ src/game/platformer/tile.lua | 74 ++++++ .../physics => game/platformer}/world.lua | 5 +- src/game/test_data/tile_test_data.lua | 4 +- src/game/utests/utestdata.lua | 6 +- tests/pico8api.lua | 6 +- tests/utestcollision.lua | 230 ------------------ tests/utestmotion.lua | 124 ++++++++++ tests/utestplayercharacter.lua | 216 ++++++++-------- tests/utesttile.lua | 119 +++++++++ tests/utesttile_data.lua | 11 - tests/utesttilemap.lua | 21 ++ tests/utestworld.lua | 2 +- 19 files changed, 572 insertions(+), 542 deletions(-) create mode 100644 src/game/platformer/motion.lua create mode 100644 src/game/platformer/tile.lua rename src/{engine/physics => game/platformer}/world.lua (95%) create mode 100644 tests/utestmotion.lua create mode 100644 tests/utesttile.lua diff --git a/src/engine/data/tilemap.lua b/src/engine/data/tilemap.lua index a7469198..8ea292e5 100644 --- a/src/engine/data/tilemap.lua +++ b/src/engine/data/tilemap.lua @@ -1,6 +1,4 @@ require("engine/core/class") --- engine > game reference to clean -require("game/data/tile_data") local tilemap = new_struct() @@ -11,7 +9,7 @@ end -- load the content into the current map function tilemap:load(content) - clear_map() + tilemap.clear_map() for i = 1, #self.content do local row = self.content[i] for j = 1, #row do @@ -20,4 +18,16 @@ function tilemap:load(content) end end +-- clear map, using appropriate interface (pico8 or busted pico8api) +function tilemap.clear_map() +--#ifn pico8 + pico8:clear_map() +--#endif + +--[[#pico8 + -- clear map data + memset(0x2000, 0, 0x1000) +--#pico8]] +end + return tilemap diff --git a/src/engine/physics/collision.lua b/src/engine/physics/collision.lua index 7219beba..3a4d68f2 100644 --- a/src/engine/physics/collision.lua +++ b/src/engine/physics/collision.lua @@ -1,12 +1,6 @@ require("engine/application/constants") require("engine/core/math") --- physics notes: collisions use fixed-point floating coordinates --- to support fractional coordinates like classic sonic uses for motion. --- therefore, we don't use pixel perfect collisions, --- e.g. "touching" does not occur when two aabb's pixel representations --- with 1px wide borders touch, but when their exact borders coincide - local collision = {} -- axis-aligned bounding box struct @@ -151,151 +145,4 @@ function aabb:intersects(other) return signed_distance <= 0 end --- struct containing the result of a ground detection test -local ground_query_info = new_struct() -collision.ground_query_info = ground_query_info - --- signed_distance float signed distance to the detected ground (clamped to min-1 amd max+1) --- slope_angle float|nil slope angle of the detected ground (nil if no ground) -function ground_query_info:_init(signed_distance, slope_angle) - self.signed_distance = signed_distance - self.slope_angle = slope_angle -end - ---#if log -function ground_query_info:_tostring() - return "ground_query_info("..joinstr(", ", self.signed_distance, tostr(self.slope_angle))..")" -end ---#endif - - --- struct representing the expected result of a character ground move over a frame, --- computed step by step. similar to a raycast hit info, specialized for ground motion -local ground_motion_result = new_struct() -collision.ground_motion_result = ground_motion_result - --- position vector position at the end of motion --- slope_angle float|nil slope angle of the final position (nil if is_falling is true) --- is_blocked bool was the character blocked during motion? --- is_falling bool should the character fall after this motion? -function ground_motion_result:_init(position, slope_angle, is_blocked, is_falling) - self.position = position - self.slope_angle = slope_angle - self.is_blocked = is_blocked - self.is_falling = is_falling -end - ---#if log -function ground_motion_result:_tostring() - return "ground_motion_result("..joinstr(", ", self.position, self.slope_angle, self.is_blocked, self.is_falling)..")" -end ---#endif - - --- struct representing the expected result of a character air move over a frame, --- computed step by step. similar to a raycast hit info, specialized for air motion -local air_motion_result = new_struct() -collision.air_motion_result = air_motion_result - --- position vector position at the end of motion --- is_blocked_by_wall bool was the character blocked by a left/right wall during motion? --- is_blocked_by_ceiling bool was the character blocked by a ceiling during motion? --- is_landing bool has the character landed at the end of this motion? --- slope_angle float|nil slope angle of the final position (nil unless is_landing is true) -function air_motion_result:_init(position, is_blocked_by_wall, is_blocked_by_ceiling, is_landing, slope_angle) - self.position = position - self.is_blocked_by_wall = is_blocked_by_wall - self.is_blocked_by_ceiling = is_blocked_by_ceiling - self.is_landing = is_landing - self.slope_angle = slope_angle -end - --- return true iff motion result indicates a blocker in the given direction -function air_motion_result:is_blocked_along(direction) - if direction == directions.left or direction == directions.right then - return self.is_blocked_by_wall - elseif direction == directions.up then - return self.is_blocked_by_ceiling - else -- direction == directions.down - return self.is_landing - end -end - ---#if log -function air_motion_result:_tostring() - return "air_motion_result("..joinstr(", ", - self.position, self.is_blocked_by_wall, self.is_blocked_by_ceiling, self.is_landing, self.slope_angle)..")" -end ---#endif - - -local tile_data = new_struct() -collision.tile_data = tile_data - --- id_loc sprite_id_location sprite location on the spritesheet --- slope_angle float slope angle in turn ratio (0.0 to 1.0, positive clockwise) -function tile_data:_init(id_loc, slope_angle) - self.id_loc = id_loc - self.slope_angle = slope_angle -end - ---#if log -function tile_data:_tostring() - return "tile_data("..joinstr(", ", self.id_loc:_tostring(), self.slope_angle)..")" -end ---#endif - - -local height_array = new_struct() -collision.height_array = height_array - --- tile_data_value tile_data tile data to generate the height array from --- _array [int] sequence of heights of a tile collision mask column per index, --- counting index from the left, height from the bottom --- it is filled based on tile_mask_id_location --- slope_angle float slope angle in turn ratio (0.0 to 1.0) -function height_array:_init(tile_data_value) - self._array = {} - self._fill_array(self._array, tile_data_value.id_loc) - self.slope_angle = tile_data_value.slope_angle -end - ---#if log -function height_array:_tostring() - return "height_array("..joinstr(", ", "{"..joinstr_table(", ", self._array).."}", self.slope_angle)..")" -end ---#endif - --- return the height for a column index starting at 0, from left to right -function height_array:get_height(column_index0) - return self._array[column_index0 + 1] -- adapt 0-index to 1-index -end - - --- fill the passed array with height data based on the sprite mask --- located at tile_mask_id_location: sprite_id_location --- pass an empty array so it is only filled with the computed values --- the tile mask must represent the collision mask of a tile, with columns --- of non-transparent (black) pixels filled from the bottom, --- or at least the upper edge of said mask (we don't check what is below --- the edge once we found the first non-transparent pixel from top to bottom) -function height_array._fill_array(array, tile_mask_id_location) - local tile_mask_topleft_position = tile_mask_id_location:to_topleft_position() - -- iterate over columns from left to right, searching for the highest filled pixel - for dx = 0, tile_size - 1 do - -- iterate from the top of the column and stop at the first filled pixel (we assume - -- lower pixels are also filled for readability of the tile mask, but not enforced) - local mask_height = 0 - for dy = 0, tile_size - 1 do - local tile_mask_color = sget(tile_mask_topleft_position.x + dx, tile_mask_topleft_position.y + dy) - -- we use black (0) as transparent mask color - if tile_mask_color ~= 0 then - mask_height = tile_size - dy - break - end - end - add(array, mask_height) - end -end - return collision diff --git a/src/engine/test/itest_dsl.lua b/src/engine/test/itest_dsl.lua index d423b853..01462aa0 100644 --- a/src/engine/test/itest_dsl.lua +++ b/src/engine/test/itest_dsl.lua @@ -549,7 +549,7 @@ function itest_dsl_parser.create_itest(name, dsli) if dsli.gamestate_type == "stage" then if dsli.stage_name == '#' then -- clear tilemap and unload tilemap data - clear_map() + tilemap.clear_map() teardown_map_data() end end diff --git a/src/game/data/collision_data.lua b/src/game/data/collision_data.lua index f7e003a6..84d0eaf9 100644 --- a/src/game/data/collision_data.lua +++ b/src/game/data/collision_data.lua @@ -1,5 +1,6 @@ -local collision = require("engine/physics/collision") -local tile_data = collision.tile_data +require("engine/core/math") +local tile = require("game/platformer/tile") +local tile_data = tile.tile_data sprite_flags = { collision = 0 diff --git a/src/game/data/tile_data.lua b/src/game/data/tile_data.lua index 9b956091..086fbb52 100644 --- a/src/game/data/tile_data.lua +++ b/src/game/data/tile_data.lua @@ -27,15 +27,3 @@ function teardown_map_data() tile_test_data.teardown() --#endif end - --- clear map, using appropriate interface (pico8 or busted pico8api) -function clear_map() ---#ifn pico8 - pico8:clear_map() ---#endif - ---[[#pico8 - -- clear map data - memset(0x2000, 0, 0x1000) ---#pico8]] -end diff --git a/src/game/ingame/playercharacter.lua b/src/game/ingame/playercharacter.lua index 4f5e2ff2..9f62c8a5 100644 --- a/src/game/ingame/playercharacter.lua +++ b/src/game/ingame/playercharacter.lua @@ -3,10 +3,10 @@ require("engine/core/class") require("engine/core/helper") require("engine/core/math") local input = require("engine/input/input") -local collision = require("engine/physics/collision") -local world = require("engine/physics/world") +local world = require("game/platformer/world") local animated_sprite = require("engine/render/animated_sprite") local pc_data = require("game/data/playercharacter_data") +local motion = require("game/platformer/motion") -- enum for character control @@ -254,7 +254,7 @@ function player_char:_compute_ground_sensors_signed_distance(center_position) end - return collision.ground_query_info(min_signed_distance, highest_ground_slope_angle) + return motion.ground_query_info(min_signed_distance, highest_ground_slope_angle) end @@ -302,13 +302,13 @@ function player_char:_compute_signed_distance_to_closest_ground(sensor_position) -- check the presence of a collider pixel from top to bottom, from max step up - 1 to min step up (we don't go until + 1 -- because if we found nothing until min step down, signed distance will be max step down + 1 anyway) - local query_info = collision.ground_query_info(pc_data.max_ground_snap_height + 1, nil) + local query_info = motion.ground_query_info(pc_data.max_ground_snap_height + 1, nil) for offset_y = -pc_data.max_ground_escape_height - 1, pc_data.max_ground_snap_height do local does_collide, slope_angle = world.get_pixel_collision_info(sensor_position.x, initial_y + offset_y) if does_collide then -- signed_distance is just the current offset, minus the initial subpixel fraction that we ignored for the pixel test iteration local fraction_y = sensor_position.y - initial_y - query_info = collision.ground_query_info(offset_y - fraction_y, slope_angle) -- slope_angle may still be nil if we are inside ground + query_info = motion.ground_query_info(offset_y - fraction_y, slope_angle) -- slope_angle may still be nil if we are inside ground break else -- optimization: use extra info from is_collision_pixel to skip pixels that we know are empty already thx to the column system @@ -491,7 +491,7 @@ end function player_char:_compute_ground_motion_result() -- if character is not moving, he is not blocked nor falling (we assume the environment is static) if self.ground_speed == 0 then - return collision.ground_motion_result( + return motion.ground_motion_result( self.position, self.slope_angle, false, @@ -504,7 +504,7 @@ function player_char:_compute_ground_motion_result() -- initialise result with floored x (we will reinject subpixels if character didn't touch a wall) -- note that left and right are not completely symmetrical since floor is asymmetrical local floored_x = flr(self.position.x) - local motion_result = collision.ground_motion_result( + local motion_result = motion.ground_motion_result( vector(floored_x, self.position.y), self.slope_angle, false, @@ -583,7 +583,7 @@ function player_char._compute_max_pixel_distance(initial_position_coord, velocit return abs(flr(initial_position_coord + velocity_coord) - flr(initial_position_coord)) end --- update ref_motion_result: collision.ground_motion_result for a character trying to move +-- update ref_motion_result: motion.ground_motion_result for a character trying to move -- by 1 pixel step in horizontal_dir, taking obstacles into account -- if character is blocked, it doesn't update the position and flag is_blocked -- if character is falling, it updates the position and flag is_falling @@ -833,7 +833,7 @@ end function player_char:_compute_air_motion_result() -- if character is not moving, he is not blocked nor landing (we assume the environment is static) if self.velocity == vector.zero() then - return collision.air_motion_result( + return motion.air_motion_result( self.position, false, false, @@ -843,7 +843,7 @@ function player_char:_compute_air_motion_result() end -- initialize air motion result (do not floor coordinates, _advance_in_air_along will do it) - local motion_result = collision.air_motion_result( + local motion_result = motion.air_motion_result( vector(self.position.x, self.position.y), false, false, @@ -949,7 +949,7 @@ function player_char:_advance_in_air_along(ref_motion_result, velocity, coord) end end --- update ref_motion_result: collision.air_motion_result for a character trying to move +-- update ref_motion_result: motion.air_motion_result for a character trying to move -- by 1 pixel step in direction in the air, taking obstacles into account -- if character is blocked by wall, ceiling or landing when moving toward left/right, up or down resp., -- it doesn't update the position and the corresponding flag is set diff --git a/src/game/platformer/motion.lua b/src/game/platformer/motion.lua new file mode 100644 index 00000000..36b4fdbe --- /dev/null +++ b/src/game/platformer/motion.lua @@ -0,0 +1,86 @@ +local motion = {} + +-- physics notes: collisions use fixed-point floating coordinates +-- to support fractional coordinates like classic sonic uses for motion. +-- therefore, we don't use pixel perfect collisions, +-- e.g. "touching" does not occur when two aabb's pixel representations +-- with 1px wide borders touch, but when their exact borders coincide + +-- struct containing the result of a ground detection test +local ground_query_info = new_struct() +motion.ground_query_info = ground_query_info + +-- signed_distance float signed distance to the detected ground (clamped to min-1 amd max+1) +-- slope_angle float|nil slope angle of the detected ground (nil if no ground) +function ground_query_info:_init(signed_distance, slope_angle) + self.signed_distance = signed_distance + self.slope_angle = slope_angle +end + +--#if log +function ground_query_info:_tostring() + return "ground_query_info("..joinstr(", ", self.signed_distance, tostr(self.slope_angle))..")" +end +--#endif + + +-- struct representing the expected result of a character ground move over a frame, +-- computed step by step. similar to a raycast hit info, specialized for ground motion +local ground_motion_result = new_struct() +motion.ground_motion_result = ground_motion_result + +-- position vector position at the end of motion +-- slope_angle float|nil slope angle of the final position (nil if is_falling is true) +-- is_blocked bool was the character blocked during motion? +-- is_falling bool should the character fall after this motion? +function ground_motion_result:_init(position, slope_angle, is_blocked, is_falling) + self.position = position + self.slope_angle = slope_angle + self.is_blocked = is_blocked + self.is_falling = is_falling +end + +--#if log +function ground_motion_result:_tostring() + return "ground_motion_result("..joinstr(", ", self.position, self.slope_angle, self.is_blocked, self.is_falling)..")" +end +--#endif + + +-- struct representing the expected result of a character air move over a frame, +-- computed step by step. similar to a raycast hit info, specialized for air motion +local air_motion_result = new_struct() +motion.air_motion_result = air_motion_result + +-- position vector position at the end of motion +-- is_blocked_by_wall bool was the character blocked by a left/right wall during motion? +-- is_blocked_by_ceiling bool was the character blocked by a ceiling during motion? +-- is_landing bool has the character landed at the end of this motion? +-- slope_angle float|nil slope angle of the final position (nil unless is_landing is true) +function air_motion_result:_init(position, is_blocked_by_wall, is_blocked_by_ceiling, is_landing, slope_angle) + self.position = position + self.is_blocked_by_wall = is_blocked_by_wall + self.is_blocked_by_ceiling = is_blocked_by_ceiling + self.is_landing = is_landing + self.slope_angle = slope_angle +end + +-- return true iff motion result indicates a blocker in the given direction +function air_motion_result:is_blocked_along(direction) + if direction == directions.left or direction == directions.right then + return self.is_blocked_by_wall + elseif direction == directions.up then + return self.is_blocked_by_ceiling + else -- direction == directions.down + return self.is_landing + end +end + +--#if log +function air_motion_result:_tostring() + return "air_motion_result("..joinstr(", ", + self.position, self.is_blocked_by_wall, self.is_blocked_by_ceiling, self.is_landing, self.slope_angle)..")" +end +--#endif + +return motion diff --git a/src/game/platformer/tile.lua b/src/game/platformer/tile.lua new file mode 100644 index 00000000..c41529bd --- /dev/null +++ b/src/game/platformer/tile.lua @@ -0,0 +1,74 @@ +require("engine/core/class") + +local tile = {} + +local tile_data = new_struct() +tile.tile_data = tile_data + +-- id_loc sprite_id_location sprite location on the spritesheet +-- slope_angle float slope angle in turn ratio (0.0 to 1.0, positive clockwise) +function tile_data:_init(id_loc, slope_angle) + self.id_loc = id_loc + self.slope_angle = slope_angle +end + +--#if log +function tile_data:_tostring() + return "tile_data("..joinstr(", ", self.id_loc:_tostring(), self.slope_angle)..")" +end +--#endif + + +local height_array = new_struct() +tile.height_array = height_array + +-- tile_data_value tile_data tile data to generate the height array from +-- _array [int] sequence of heights of a tile collision mask column per index, +-- counting index from the left, height from the bottom +-- it is filled based on tile_mask_id_location +-- slope_angle float slope angle in turn ratio (0.0 to 1.0) +function height_array:_init(tile_data_value) + self._array = {} + self._fill_array(self._array, tile_data_value.id_loc) + self.slope_angle = tile_data_value.slope_angle +end + +--#if log +function height_array:_tostring() + return "height_array("..joinstr(", ", "{"..joinstr_table(", ", self._array).."}", self.slope_angle)..")" +end +--#endif + +-- return the height for a column index starting at 0, from left to right +function height_array:get_height(column_index0) + return self._array[column_index0 + 1] -- adapt 0-index to 1-index +end + + +-- fill the passed array with height data based on the sprite mask +-- located at tile_mask_id_location: sprite_id_location +-- pass an empty array so it is only filled with the computed values +-- the tile mask must represent the collision mask of a tile, with columns +-- of non-transparent (black) pixels filled from the bottom, +-- or at least the upper edge of said mask (we don't check what is below +-- the edge once we found the first non-transparent pixel from top to bottom) +function height_array._fill_array(array, tile_mask_id_location) + local tile_mask_topleft_position = tile_mask_id_location:to_topleft_position() + -- iterate over columns from left to right, searching for the highest filled pixel + for dx = 0, tile_size - 1 do + -- iterate from the top of the column and stop at the first filled pixel (we assume + -- lower pixels are also filled for readability of the tile mask, but not enforced) + local mask_height = 0 + for dy = 0, tile_size - 1 do + local tile_mask_color = sget(tile_mask_topleft_position.x + dx, tile_mask_topleft_position.y + dy) + -- we use black (0) as transparent mask color + if tile_mask_color ~= 0 then + mask_height = tile_size - dy + break + end + end + add(array, mask_height) + end +end + +return tile diff --git a/src/engine/physics/world.lua b/src/game/platformer/world.lua similarity index 95% rename from src/engine/physics/world.lua rename to src/game/platformer/world.lua index 4f307100..d51a1358 100644 --- a/src/engine/physics/world.lua +++ b/src/game/platformer/world.lua @@ -1,4 +1,5 @@ -local collision = require("engine/physics/collision") +local tile = require("game/platformer/tile") +-- engine > game reference to clean local collision_data = require("game/data/collision_data") local world = {} @@ -24,7 +25,7 @@ function world._compute_column_height_at(tile_location, column_index0) if tile_data_value then -- optimize: cache collision height array on game start (otherwise, we get all the data every time, -- including the unused slope angle) - local h_array = collision.height_array(tile_data_value) + local h_array = tile.height_array(tile_data_value) return h_array:get_height(column_index0), h_array.slope_angle end diff --git a/src/game/test_data/tile_test_data.lua b/src/game/test_data/tile_test_data.lua index 8f445c94..344998b7 100644 --- a/src/game/test_data/tile_test_data.lua +++ b/src/game/test_data/tile_test_data.lua @@ -1,7 +1,7 @@ --#ifn pico8 require("pico8api") -local collision = require("engine/physics/collision") +local tile = require("game/platformer/tile") local collision_data = require("game/data/collision_data") local stub = require("luassert.stub") @@ -23,7 +23,7 @@ function tile_test_data.setup() fset(73, sprite_flags.collision, true) -- high-tile (3/4 filled) -- mock height array _init so it doesn't have to dig in sprite data, inaccessible from busted - height_array_init_mock = stub(collision.height_array, "_init", function (self, tile_data) + height_array_init_mock = stub(tile.height_array, "_init", function (self, tile_data) local tile_mask_id_location = tile_data.id_loc if tile_mask_id_location == collision_data.tiles_data[64].id_loc then self._array = {8, 8, 8, 8, 8, 8, 8, 8} -- full tile diff --git a/src/game/utests/utestdata.lua b/src/game/utests/utestdata.lua index d8d5cf7c..cc701436 100644 --- a/src/game/utests/utestdata.lua +++ b/src/game/utests/utestdata.lua @@ -1,9 +1,9 @@ require("engine/test/unittest") require("engine/core/math") -local collision = require("engine/physics/collision") +local tile = require("game/platformer/tile") local collision_data = require("game/data/collision_data") -local height_array = collision.height_array -local tile_data = collision.tile_data +local height_array = tile.height_array +local tile_data = tile.tile_data check('sprite_id_location(0, 4) should have collision flag set', function () local sprite_id = sprite_id_location(0, 4):to_sprite_id() diff --git a/tests/pico8api.lua b/tests/pico8api.lua index 2e30ad48..f1ab6a21 100644 --- a/tests/pico8api.lua +++ b/tests/pico8api.lua @@ -55,9 +55,9 @@ end -- busted-only helper to clear the map, as memset(0x2000, 0, 0x1000) wouldn't work function pico8:clear_map() for y = 0, 63 do - pico8.map[y] = {} + self.map[y] = {} for x = 0, 127 do - pico8.map[y][x] = 0 + self.map[y][x] = 0 end end end @@ -66,7 +66,7 @@ end -- and we don't really do that in pico8 anyway function pico8:clear_spriteflags() for n = 0, 255 do - pico8.spriteflags[n] = 0 + self.spriteflags[n] = 0 end end diff --git a/tests/utestcollision.lua b/tests/utestcollision.lua index e7f2e008..e501a3c7 100644 --- a/tests/utestcollision.lua +++ b/tests/utestcollision.lua @@ -3,11 +3,6 @@ require("engine/application/constants") require("engine/core/math") local collision = require("engine/physics/collision") local aabb = collision.aabb -local tile_data = collision.tile_data -local height_array = collision.height_array -local ground_query_info = collision.ground_query_info -local ground_motion_result, air_motion_result = get_members(collision, - "ground_motion_result", "air_motion_result") -- retrieve the filter arguments so we can optimize by only generating tests we will need local cli = require('busted.modules.cli')() @@ -681,229 +676,4 @@ describe('collision', function () end) - describe('ground_query_info', function () - - describe('_init', function () - - it('should create a ground_query_info with signed_distance, slope_angle', function () - local info = ground_query_info(-2.0, 0.25) - assert.are_same({-2.0, 0.25}, {info.signed_distance, info.slope_angle}) - end) - - end) - - describe('_tostring', function () - - it('should return "ground_query_info({self.signed_distance}, 0.125)"', function () - local info = ground_query_info(-2.0, 0.25) - assert.are_equal("ground_query_info(-2.0, 0.25)", info:_tostring()) - end) - - it('should return "ground_query_info({self.signed_distance}, [nil])"', function () - local info = ground_query_info(-2.0, nil) - assert.are_equal("ground_query_info(-2.0, [nil])", info:_tostring()) - end) - - end) - - end) - - describe('ground_motion_result', function () - - describe('_init', function () - - it('should create a ground_motion_result with position, slope_angle, is_blocked, is_falling', function () - local gmr = ground_motion_result(vector(2, 3), 0.25, true, false) - assert.are_same({vector(2, 3), 0.25, true, false}, {gmr.position, gmr.slope_angle, gmr.is_blocked, gmr.is_falling}) - end) - - end) - - describe('_tostring', function () - - it('should return "ground_motion_result(vector(2, 3), 0.25, true, false)"', function () - local gmr = ground_motion_result(vector(2, 3), 0.25, true, false) - assert.are_equal("ground_motion_result(vector(2, 3), 0.25, true, false)", gmr:_tostring()) - end) - - end) - - end) - - describe('air_motion_result', function () - - describe('_init', function () - - it('should create a air_motion_result with position, is_blocked_by_wall, is_blocked_by_ceiling, is_landing, slope_angle', function () - local gmr = air_motion_result(vector(2, 3), true, false, true, -0.25) - assert.are_same({vector(2, 3), true, false, true, -0.25}, {gmr.position, gmr.is_blocked_by_wall, gmr.is_blocked_by_ceiling, gmr.is_landing, gmr.slope_angle}) - end) - - end) - - describe('is_blocked_along', function () - - it('return false if direction is left and is_blocked_by_wall is false', function () - local gmr = air_motion_result(vector(2, 3), false, false, false) - assert.is_false(gmr:is_blocked_along(directions.left)) - end) - - it('return true if direction is left and is_blocked_by_wall is true', function () - local gmr = air_motion_result(vector(2, 3), true, false, false) - assert.is_true(gmr:is_blocked_along(directions.left)) - end) - - it('return false if direction is right and is_blocked_by_wall is false', function () - local gmr = air_motion_result(vector(2, 3), false, false, false) - assert.is_false(gmr:is_blocked_along(directions.right)) - end) - - it('return true if direction is right and is_blocked_by_wall is true', function () - local gmr = air_motion_result(vector(2, 3), true, false, false) - assert.is_true(gmr:is_blocked_along(directions.right)) - end) - - it('return false if direction is up and is_blocked_by_ceiling is false', function () - local gmr = air_motion_result(vector(2, 3), false, false, false) - assert.is_false(gmr:is_blocked_along(directions.up)) - end) - - it('return true if direction is up and is_blocked_by_ceiling is true', function () - local gmr = air_motion_result(vector(2, 3), false, true, false) - assert.is_true(gmr:is_blocked_along(directions.up)) - end) - - it('return false if direction is down and is_landing is false', function () - local gmr = air_motion_result(vector(2, 3), false, false, false) - assert.is_false(gmr:is_blocked_along(directions.down)) - end) - - it('return true if direction is down and is_landing is true', function () - local gmr = air_motion_result(vector(2, 3), false, false, true) - assert.is_true(gmr:is_blocked_along(directions.down)) - end) - - end) - - describe('_tostring', function () - - it('should return "air_motion_result(vector(2, 3), true, false, true, -0.25)"', function () - local gmr = air_motion_result(vector(2, 3), true, false, true, -0.25) - assert.are_equal("air_motion_result(vector(2, 3), true, false, true, -0.25)", gmr:_tostring()) - end) - - end) - - end) - - describe('tile_data', function () - - describe('_init', function () - - it('should create a tile data setting the sprite id location and the slope angle', function () - local td = tile_data(sprite_id_location(1, 2), 0.125) - assert.are_same({sprite_id_location(1, 2), 0.125}, {td.id_loc, td.slope_angle}) - end) - - end) - - describe('_tostring', function () - - it('should return "height_array({4, 5, 6, 7, 8, 9, 10, 11}, 0.125)"', function () - local td = tile_data(sprite_id_location(1, 2), 0.125) - assert.are_equal("tile_data(sprite_id_location(1, 2), 0.125)", td:_tostring()) - end) - - end) - - end) - - describe('height_array', function () - - describe("mocking _fill_array", function () - - local fill_array_mock - - setup(function () - fill_array_mock = stub(height_array, "_fill_array", function (array, tile_mask_sprite_id_location) - for i = 1, tile_size do - array[i] = tile_mask_sprite_id_location.i + tile_mask_sprite_id_location.j + i - end - end) - end) - - teardown(function () - fill_array_mock:revert() - end) - - after_each(function () - fill_array_mock:clear() - end) - - describe('_init', function () - - it('should create a height array using fill_array and setting the slope angle', function () - local h_array = height_array(tile_data(sprite_id_location(1, 2), 0.125)) - assert.are_same({{4, 5, 6, 7, 8, 9, 10, 11}, 0.125}, {h_array._array, h_array.slope_angle}) - end) - - end) - - describe('_tostring', function () - - it('should return "height_array({4, 5, 6, 7, 8, 9, 10, 11}, 0.125)"', function () - local h_array = height_array(tile_data(sprite_id_location(1, 2), 0.125)) - assert.are_equal("height_array({4, 5, 6, 7, 8, 9, 10, 11}, 0.125)", h_array:_tostring()) - end) - - end) - - describe('get_height', function () - - it('should return the height at the given column index', function () - local h_array = height_array(tile_data(sprite_id_location(1, 2), 0.125)) - assert.are_equal(6, h_array:get_height(2)) - end) - - end) - - end) - - describe('_fill_array', function () - - local sget_mock - - setup(function () - -- simulate an sget that would return the pixel of a tile mask - -- if coordinates fall in the sprite at location (1, 2), i.e. [8-15] x [16-23], - -- where mock_height_array contains the respective height of the mask columns - -- for each column from left to right - local mock_height_array = {2, 3, 5, 6, 0, 1, 4, 2} - sget_mock = stub(_G, "sget", function (x, y) - if x >= 8 and x <= 15 and y >= 16 and y <= 23 then - -- return filled pixel color iff below mask height on this column - local height = mock_height_array[x - 7] - if y - 16 >= tile_size - height then - return 1 - else - return 0 - end - end - end) - end) - - teardown(function () - sget_mock:revert() - end) - - it('should fill the array with ', function () - local array = {} - height_array._fill_array(array, sprite_id_location(1, 2)) - assert.are_same({2, 3, 5, 6, 0, 1, 4, 2}, array) - end) - - end) - - end) - end) diff --git a/tests/utestmotion.lua b/tests/utestmotion.lua new file mode 100644 index 00000000..d694539d --- /dev/null +++ b/tests/utestmotion.lua @@ -0,0 +1,124 @@ +require("bustedhelper") +local motion = require("game/platformer/motion") +local ground_query_info = motion.ground_query_info +local ground_motion_result, air_motion_result = get_members(motion, + "ground_motion_result", "air_motion_result") + +describe('motion', function () + + describe('ground_query_info', function () + + describe('_init', function () + + it('should create a ground_query_info with signed_distance, slope_angle', function () + local info = ground_query_info(-2.0, 0.25) + assert.are_same({-2.0, 0.25}, {info.signed_distance, info.slope_angle}) + end) + + end) + + describe('_tostring', function () + + it('should return "ground_query_info({self.signed_distance}, 0.125)"', function () + local info = ground_query_info(-2.0, 0.25) + assert.are_equal("ground_query_info(-2.0, 0.25)", info:_tostring()) + end) + + it('should return "ground_query_info({self.signed_distance}, [nil])"', function () + local info = ground_query_info(-2.0, nil) + assert.are_equal("ground_query_info(-2.0, [nil])", info:_tostring()) + end) + + end) + + end) + + describe('ground_motion_result', function () + + describe('_init', function () + + it('should create a ground_motion_result with position, slope_angle, is_blocked, is_falling', function () + local gmr = ground_motion_result(vector(2, 3), 0.25, true, false) + assert.are_same({vector(2, 3), 0.25, true, false}, {gmr.position, gmr.slope_angle, gmr.is_blocked, gmr.is_falling}) + end) + + end) + + describe('_tostring', function () + + it('should return "ground_motion_result(vector(2, 3), 0.25, true, false)"', function () + local gmr = ground_motion_result(vector(2, 3), 0.25, true, false) + assert.are_equal("ground_motion_result(vector(2, 3), 0.25, true, false)", gmr:_tostring()) + end) + + end) + + end) + + describe('air_motion_result', function () + + describe('_init', function () + + it('should create a air_motion_result with position, is_blocked_by_wall, is_blocked_by_ceiling, is_landing, slope_angle', function () + local gmr = air_motion_result(vector(2, 3), true, false, true, -0.25) + assert.are_same({vector(2, 3), true, false, true, -0.25}, {gmr.position, gmr.is_blocked_by_wall, gmr.is_blocked_by_ceiling, gmr.is_landing, gmr.slope_angle}) + end) + + end) + + describe('is_blocked_along', function () + + it('return false if direction is left and is_blocked_by_wall is false', function () + local gmr = air_motion_result(vector(2, 3), false, false, false) + assert.is_false(gmr:is_blocked_along(directions.left)) + end) + + it('return true if direction is left and is_blocked_by_wall is true', function () + local gmr = air_motion_result(vector(2, 3), true, false, false) + assert.is_true(gmr:is_blocked_along(directions.left)) + end) + + it('return false if direction is right and is_blocked_by_wall is false', function () + local gmr = air_motion_result(vector(2, 3), false, false, false) + assert.is_false(gmr:is_blocked_along(directions.right)) + end) + + it('return true if direction is right and is_blocked_by_wall is true', function () + local gmr = air_motion_result(vector(2, 3), true, false, false) + assert.is_true(gmr:is_blocked_along(directions.right)) + end) + + it('return false if direction is up and is_blocked_by_ceiling is false', function () + local gmr = air_motion_result(vector(2, 3), false, false, false) + assert.is_false(gmr:is_blocked_along(directions.up)) + end) + + it('return true if direction is up and is_blocked_by_ceiling is true', function () + local gmr = air_motion_result(vector(2, 3), false, true, false) + assert.is_true(gmr:is_blocked_along(directions.up)) + end) + + it('return false if direction is down and is_landing is false', function () + local gmr = air_motion_result(vector(2, 3), false, false, false) + assert.is_false(gmr:is_blocked_along(directions.down)) + end) + + it('return true if direction is down and is_landing is true', function () + local gmr = air_motion_result(vector(2, 3), false, false, true) + assert.is_true(gmr:is_blocked_along(directions.down)) + end) + + end) + + describe('_tostring', function () + + it('should return "air_motion_result(vector(2, 3), true, false, true, -0.25)"', function () + local gmr = air_motion_result(vector(2, 3), true, false, true, -0.25) + assert.are_equal("air_motion_result(vector(2, 3), true, false, true, -0.25)", gmr:_tostring()) + end) + + end) + + end) + +end) diff --git a/tests/utestplayercharacter.lua b/tests/utestplayercharacter.lua index 098b6643..5b6e6842 100644 --- a/tests/utestplayercharacter.lua +++ b/tests/utestplayercharacter.lua @@ -2,8 +2,8 @@ require("bustedhelper") require("engine/core/math") local player_char = require("game/ingame/playercharacter") local input = require("engine/input/input") -local collision = require("engine/physics/collision") -local ground_query_info = collision.ground_query_info +local motion = require("game/platformer/motion") +local ground_query_info = motion.ground_query_info local pc_data = require("game/data/playercharacter_data") local tile_test_data = require("game/test_data/tile_test_data") @@ -633,17 +633,17 @@ describe('player_char', function () compute_signed_distance_to_closest_ground_mock = stub(player_char, "_compute_signed_distance_to_closest_ground", function (self, sensor_position) if sensor_position == vector(-1, 0) then - return collision.ground_query_info(-4, 0.25) + return motion.ground_query_info(-4, 0.25) elseif sensor_position == vector(1, 0) then - return collision.ground_query_info(5, -0.125) + return motion.ground_query_info(5, -0.125) elseif sensor_position == vector(-1, 1) then - return collision.ground_query_info(7, -0.25) + return motion.ground_query_info(7, -0.25) elseif sensor_position == vector(1, 1) then - return collision.ground_query_info(6, 0.25) + return motion.ground_query_info(6, 0.25) elseif sensor_position == vector(-1, 2) then - return collision.ground_query_info(3, 0) + return motion.ground_query_info(3, 0) else -- sensor_position == vector(1, 2) - return collision.ground_query_info(3, 0.125) + return motion.ground_query_info(3, 0.125) end end) end) @@ -660,12 +660,12 @@ describe('player_char', function () it('should return the signed distance to closest ground from left sensor if the lowest', function () -- -4 vs 5 => -4 - assert.are_same(collision.ground_query_info(-4, 0.25), pc:_compute_ground_sensors_signed_distance(vector(0, 0))) + assert.are_same(motion.ground_query_info(-4, 0.25), pc:_compute_ground_sensors_signed_distance(vector(0, 0))) end) it('should return the signed distance to closest ground from right sensor if the lowest', function () -- 7 vs 6 => 6 - assert.are_same(collision.ground_query_info(6, 0.25), pc:_compute_ground_sensors_signed_distance(vector(0, 1))) + assert.are_same(motion.ground_query_info(6, 0.25), pc:_compute_ground_sensors_signed_distance(vector(0, 1))) end) describe('(prioritized direction is left)', function () @@ -682,7 +682,7 @@ describe('player_char', function () it('should return the signed distance to left ground if both sensors are at the same level, but left is prioritized', function () -- 3 vs 3 => 3 left - assert.are_same(collision.ground_query_info(3, 0), pc:_compute_ground_sensors_signed_distance(vector(0, 2))) + assert.are_same(motion.ground_query_info(3, 0), pc:_compute_ground_sensors_signed_distance(vector(0, 2))) end) end) @@ -703,7 +703,7 @@ describe('player_char', function () it('should return the signed distance to right ground if both sensors are at the same level, but left is prioritized', function () -- 3 vs 3 => 3 right - assert.are_same(collision.ground_query_info(3, 0.125), pc:_compute_ground_sensors_signed_distance(vector(0, 2))) + assert.are_same(motion.ground_query_info(3, 0.125), pc:_compute_ground_sensors_signed_distance(vector(0, 2))) end) end) @@ -1497,7 +1497,7 @@ describe('player_char', function () setup(function () compute_ground_motion_result_mock = stub(player_char, "_compute_ground_motion_result", function (self) - return collision.ground_motion_result( + return motion.ground_motion_result( vector(3, 4), 0.25, false, @@ -1563,7 +1563,7 @@ describe('player_char', function () setup(function () compute_ground_motion_result_mock = stub(player_char, "_compute_ground_motion_result", function (self) - return collision.ground_motion_result( + return motion.ground_motion_result( vector(3, 4), 0.5, true, @@ -1621,7 +1621,7 @@ describe('player_char', function () setup(function () compute_ground_motion_result_mock = stub(player_char, "_compute_ground_motion_result", function (self) - return collision.ground_motion_result( + return motion.ground_motion_result( vector(3, 4), nil, false, @@ -1679,7 +1679,7 @@ describe('player_char', function () setup(function () compute_ground_motion_result_mock = stub(player_char, "_compute_ground_motion_result", function (self) - return collision.ground_motion_result( + return motion.ground_motion_result( vector(3, 4), nil, true, @@ -1974,7 +1974,7 @@ describe('player_char', function () pc.position = vector(3, 4) pc.slope_angle = 0.125 - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(3, 4), 0.125, false, @@ -1988,7 +1988,7 @@ describe('player_char', function () pc.position = vector(3.5, 4) pc.slope_angle = 0.125 - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(3.5, 4), 0.125, false, @@ -2024,7 +2024,7 @@ describe('player_char', function () -- we assume _compute_max_pixel_distance is correct, so it should return 0 -- but as there is no blocking, the remaining subpixels will still be added - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(3.5, 4), 0, -- character has not moved by a full pixel, so visible position and slope remains the same false, @@ -2041,7 +2041,7 @@ describe('player_char', function () pc.slope_angle = -1/6 -- cos(-pi/3) = 1/2 pc.ground_speed = 1 -- * slope cos = 0.5 - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(3.5, 4), -1/6, -- character has not moved by a full pixel, so visible position and slope remains the same false, @@ -2056,7 +2056,7 @@ describe('player_char', function () pc.ground_speed = 0.5 -- we assume _compute_max_pixel_distance is correct, so it should return 1 - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(4, 4), -0.125, false, @@ -2070,7 +2070,7 @@ describe('player_char', function () pc.position = vector(3, 4) pc.ground_speed = -2.5 - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(0.5, 4), -0.125, false, @@ -2110,7 +2110,7 @@ describe('player_char', function () pc.ground_speed = 1.5 -- we assume _compute_max_pixel_distance is correct, so it should return 2 - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(5, 4), 0.125, false, @@ -2125,7 +2125,7 @@ describe('player_char', function () pc.ground_speed = -1.5 -- we assume _compute_max_pixel_distance is correct, so it should return 2 - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(-5, 4), 0.125, false, @@ -2142,7 +2142,7 @@ describe('player_char', function () pc.ground_speed = 0.5 -- we assume _compute_max_pixel_distance is correct, so it should return 1 - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(5, 4), 0.125, false, @@ -2158,7 +2158,7 @@ describe('player_char', function () pc.ground_speed = -1 -- we assume _compute_max_pixel_distance is correct, so it should return 1 - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(-5, 4), 0.125, false, @@ -2176,7 +2176,7 @@ describe('player_char', function () pc.slope_angle = -1/6 -- cos(-pi/3) = 1/2 pc.ground_speed = 1 -- * slope cos = -0.5 - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(5, 4), 0.125, -- new slope angle, no relation with initial one false, @@ -2193,7 +2193,7 @@ describe('player_char', function () pc.slope_angle = -1/6 -- cos(-pi/3) = 1/2 pc.ground_speed = -2 -- * slope cos = -1 - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(-5, 4), 0.125, -- new slope angle, no relation with initial one false, @@ -2211,7 +2211,7 @@ describe('player_char', function () -- going "into" the wall, we floor them and consider character as blocked -- (unlike Classic Sonic that would simply ignore subpixels) - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(5, 4), 0.125, true, @@ -2229,7 +2229,7 @@ describe('player_char', function () -- going "into" the wall, we floor them and consider character as blocked -- (unlike Classic Sonic that would simply ignore subpixels) - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(-5, 4), 0.125, true, @@ -2247,7 +2247,7 @@ describe('player_char', function () pc.ground_speed = 1.5 -- * slope cos = 0.75 -- this time, due to the slope cos, charaacter doesn't reach the wall and is not blocked - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(4.75, 4), -1/6, -- character has not moved by a full pixel, so visible position and slope remains the same false, @@ -2263,7 +2263,7 @@ describe('player_char', function () pc.slope_angle = -1/6 -- cos(-pi/3) = 1/2 pc.ground_speed = -1.5 -- * slope cos = -0.75 - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(-4.85, 4), -1/6, -- character has not moved by a full pixel, so visible position and slope remains the same false, @@ -2279,7 +2279,7 @@ describe('player_char', function () pc.ground_speed = 3 -- * slope cos = 1.5 -- but here, even with the slope cos, charaacter will hit wall - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(5, 4), 0.125, true, @@ -2294,7 +2294,7 @@ describe('player_char', function () pc.slope_angle = -1/6 -- cos(-pi/3) = 1/2 pc.ground_speed = -3 -- * slope cos = -1.5 - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(-5, 4), 0.125, true, @@ -2314,7 +2314,7 @@ describe('player_char', function () -- the character is already touching the wall, so any motion, even of just a few subpixels, -- is considered blocked - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(5, 4), 0, -- character couldn't move at all, so we preserved the initial slope angle true, @@ -2331,7 +2331,7 @@ describe('player_char', function () -- the character is already touching the wall, so any motion, even of just a few subpixels, -- is considered blocked - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(-5, 4), 0, -- character couldn't move at all, so we preserved the initial slope angle true, @@ -2351,7 +2351,7 @@ describe('player_char', function () -- we assume _compute_max_pixel_distance is correct, so it should return 1 -- but we will be blocked by the wall anyway - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(5, 4), -- this works on the *right* thanks to subpixel cut working "inside" a wall 0, -- character couldn't move and went back, so we preserved the initial slope angle true, @@ -2367,7 +2367,7 @@ describe('player_char', function () -- we assume _compute_max_pixel_distance is correct, so it should return 1 -- but we will be blocked by the wall anyway - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(-6, 4), -- we are already inside the wall, floored to -6 0, -- character only snap to floored x, so we preserved the slope angle false, -- no wall detected from inside! @@ -2383,7 +2383,7 @@ describe('player_char', function () -- we assume _compute_max_pixel_distance is correct, so it should return 1 -- but we will be blocked by the wall anyway - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(-6, 4), -- we are already inside the wall, floored to -6 0, -- character only snap to floored x, so we preserved the slope angle true, -- wall detected from inside if moving 1 full pixel toward the next column on the left @@ -2399,7 +2399,7 @@ describe('player_char', function () -- we assume _compute_max_pixel_distance is correct, so it should return 3 -- but because of the blocking, we stop at x=5 instead of 6.5 - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(5, 4), 0.125, true, @@ -2415,7 +2415,7 @@ describe('player_char', function () -- we assume _compute_max_pixel_distance is correct, so it should return 3 -- but because of the blocking, we stop at x=5 instead of 6.5 - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(-5, 4), 0.125, true, @@ -2461,7 +2461,7 @@ describe('player_char', function () -- we assume _compute_max_pixel_distance is correct, so it should return 3 -- we are falling but not blocked, so we continue running in the air until x=6 - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(6, 4), nil, false, @@ -2477,7 +2477,7 @@ describe('player_char', function () -- we assume _compute_max_pixel_distance is correct, so it should return 3 -- we are falling then blocked on 7 - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(7, 4), nil, true, @@ -2504,7 +2504,7 @@ describe('player_char', function () end) it('when stepping left with the right sensor still on the ground, decrement x', function () - local motion_result = collision.ground_motion_result( + local motion_result = motion.ground_motion_result( vector(-1, 8 - pc_data.center_height_standing), 0, false, @@ -2514,7 +2514,7 @@ describe('player_char', function () -- step flat pc:_next_ground_step(horizontal_dirs.left, motion_result) - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(-2, 8 - pc_data.center_height_standing), 0, false, @@ -2525,7 +2525,7 @@ describe('player_char', function () end) it('when stepping right with the left sensor still on the ground, increment x', function () - local motion_result = collision.ground_motion_result( + local motion_result = motion.ground_motion_result( vector(9, 8 - pc_data.center_height_standing), 0, false, @@ -2535,7 +2535,7 @@ describe('player_char', function () -- step flat pc:_next_ground_step(horizontal_dirs.right, motion_result) - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(10, 8 - pc_data.center_height_standing), 0, false, @@ -2546,7 +2546,7 @@ describe('player_char', function () end) it('when stepping left leaving the ground, decrement x and fall', function () - local motion_result = collision.ground_motion_result( + local motion_result = motion.ground_motion_result( vector(-2, 8 - pc_data.center_height_standing), 0, false, @@ -2556,7 +2556,7 @@ describe('player_char', function () -- step fall pc:_next_ground_step(horizontal_dirs.left, motion_result) - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(-3, 8 - pc_data.center_height_standing), nil, false, @@ -2567,7 +2567,7 @@ describe('player_char', function () end) it('when stepping right leaving the ground, increment x and fall', function () - local motion_result = collision.ground_motion_result( + local motion_result = motion.ground_motion_result( vector(10, 8 - pc_data.center_height_standing), 0, false, @@ -2577,7 +2577,7 @@ describe('player_char', function () -- step fall pc:_next_ground_step(horizontal_dirs.right, motion_result) - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(11, 8 - pc_data.center_height_standing), nil, false, @@ -2588,7 +2588,7 @@ describe('player_char', function () end) it('when stepping right back on the ground, increment x and cancel fall', function () - local motion_result = collision.ground_motion_result( + local motion_result = motion.ground_motion_result( vector(-3, 8 - pc_data.center_height_standing), nil, false, @@ -2598,7 +2598,7 @@ describe('player_char', function () -- step land (very rare) pc:_next_ground_step(horizontal_dirs.right, motion_result) - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(-2, 8 - pc_data.center_height_standing), 0, false, @@ -2623,7 +2623,7 @@ describe('player_char', function () end) it('when stepping left and hitting the wall, preserve x and block', function () - local motion_result = collision.ground_motion_result( + local motion_result = motion.ground_motion_result( vector(3, 8 - pc_data.center_height_standing), 0, false, @@ -2633,7 +2633,7 @@ describe('player_char', function () -- step block pc:_next_ground_step(horizontal_dirs.left, motion_result) - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(3, 8 - pc_data.center_height_standing), 0, true, @@ -2644,7 +2644,7 @@ describe('player_char', function () end) it('when stepping right and hitting the wall, preserve x and block', function () - local motion_result = collision.ground_motion_result( + local motion_result = motion.ground_motion_result( vector(5, 8 - pc_data.center_height_standing), 0, false, @@ -2654,7 +2654,7 @@ describe('player_char', function () -- step block pc:_next_ground_step(horizontal_dirs.right, motion_result) - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(5, 8 - pc_data.center_height_standing), 0, true, @@ -2678,7 +2678,7 @@ describe('player_char', function () -- it will fail until _compute_signed_distance_to_closest_ground -- detects upper-level tiles as suggested in the note it('when stepping right on the ground and hitting the non-supported wall, preserve x and block', function () - local motion_result = collision.ground_motion_result( + local motion_result = motion.ground_motion_result( vector(5, 8 - pc_data.center_height_standing), 0, false, @@ -2688,7 +2688,7 @@ describe('player_char', function () -- step block pc:_next_ground_step(horizontal_dirs.right, motion_result) - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(5, 8 - pc_data.center_height_standing), 0, true, @@ -2712,7 +2712,7 @@ describe('player_char', function () -- it will fail until _compute_signed_distance_to_closest_ground -- detects upper-level tiles as suggested in the note it('when stepping right on the half-tile and hitting the head wall, preserve x and block', function () - local motion_result = collision.ground_motion_result( + local motion_result = motion.ground_motion_result( vector(5, 12 - pc_data.center_height_standing), 0, false, @@ -2722,7 +2722,7 @@ describe('player_char', function () -- step block pc:_next_ground_step(horizontal_dirs.right, motion_result) - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(5, 12 - pc_data.center_height_standing), 0, true, @@ -2747,7 +2747,7 @@ describe('player_char', function () end) it('when stepping right from the bottom of the ascending slope, increment x and adjust y', function () - local motion_result = collision.ground_motion_result( + local motion_result = motion.ground_motion_result( vector(5, 8 - pc_data.center_height_standing), 0, false, @@ -2757,7 +2757,7 @@ describe('player_char', function () -- step down pc:_next_ground_step(horizontal_dirs.right, motion_result) - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(6, 7 - pc_data.center_height_standing), -45/360, false, @@ -2781,7 +2781,7 @@ describe('player_char', function () end) it('when stepping left on the ascending slope without leaving the ground, decrement x and adjust y', function () - local motion_result = collision.ground_motion_result( + local motion_result = motion.ground_motion_result( vector(12, 9 - pc_data.center_height_standing), -45/360, false, @@ -2791,7 +2791,7 @@ describe('player_char', function () -- step down pc:_next_ground_step(horizontal_dirs.left, motion_result) - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(11, 10 - pc_data.center_height_standing), -45/360, false, @@ -2802,7 +2802,7 @@ describe('player_char', function () end) it('when stepping right on the ascending slope without leaving the ground, decrement x and adjust y', function () - local motion_result = collision.ground_motion_result( + local motion_result = motion.ground_motion_result( vector(12, 9 - pc_data.center_height_standing), -45/360, false, @@ -2812,7 +2812,7 @@ describe('player_char', function () -- step up pc:_next_ground_step(horizontal_dirs.right, motion_result) - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(13, 8 - pc_data.center_height_standing), -45/360, false, @@ -2823,7 +2823,7 @@ describe('player_char', function () end) it('when stepping right on the ascending slope and hitting the right wall, preserve x and y and block', function () - local motion_result = collision.ground_motion_result( + local motion_result = motion.ground_motion_result( vector(13, 10 - pc_data.center_height_standing), -45/360, false, @@ -2833,7 +2833,7 @@ describe('player_char', function () -- step up blocked pc:_next_ground_step(horizontal_dirs.right, motion_result) - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(13, 10 - pc_data.center_height_standing), -45/360, true, @@ -2844,7 +2844,7 @@ describe('player_char', function () end) it('when stepping left on the ascending slope and hitting the left wall, preserve x and y and block', function () - local motion_result = collision.ground_motion_result( + local motion_result = motion.ground_motion_result( vector(11, 10 - pc_data.center_height_standing), -45/360, false, @@ -2854,7 +2854,7 @@ describe('player_char', function () -- step down blocked pc:_next_ground_step(horizontal_dirs.left, motion_result) - assert.are_equal(collision.ground_motion_result( + assert.are_equal(motion.ground_motion_result( vector(11, 10 - pc_data.center_height_standing), -45/360, true, @@ -3054,7 +3054,7 @@ describe('player_char', function () setup(function () compute_air_motion_result_mock = stub(player_char, "_compute_air_motion_result", function (self) - return collision.air_motion_result( + return motion.air_motion_result( vector(2, 8), false, false, @@ -3198,7 +3198,7 @@ describe('player_char', function () setup(function () compute_air_motion_result_mock = stub(player_char, "_compute_air_motion_result", function (self) - return collision.air_motion_result( + return motion.air_motion_result( vector(2, 8), false, -- not the focus, but verified true, -- focus in this test @@ -3242,7 +3242,7 @@ describe('player_char', function () setup(function () compute_air_motion_result_mock = stub(player_char, "_compute_air_motion_result", function (self) - return collision.air_motion_result( + return motion.air_motion_result( vector(2, 8), true, -- focus in this test false, -- not the focus, but verified @@ -3286,7 +3286,7 @@ describe('player_char', function () setup(function () compute_air_motion_result_mock = stub(player_char, "_compute_air_motion_result", function (self) - return collision.air_motion_result( + return motion.air_motion_result( vector(2, 8), false, false, @@ -3367,7 +3367,7 @@ describe('player_char', function () it('(when velocity is zero) should return air_motion_result with initial position and no hits', function () pc.position = vector(4, 8) - assert.are_equal(collision.air_motion_result( + assert.are_equal(motion.air_motion_result( vector(4, 8), false, false, @@ -3410,7 +3410,7 @@ describe('player_char', function () -- character should advance of (5, -6) resulting in pos (9.5, 2) -- interface: check that the final result is correct - assert.are_equal(collision.air_motion_result( + assert.are_equal(motion.air_motion_result( vector(9.5, 2), false, true, -- hit ceiling @@ -3454,7 +3454,7 @@ describe('player_char', function () -- was using the integer max_pixel_distance instead of the float velocity[coord] -- and this revealed a bug of no motion on x at all when velocity.x is < 1 and x starts integer it('(vector(0, 10) at speed 0.5 along x) should move to vector(0.7, 10) without being blocked', function () - local motion_result = collision.air_motion_result( + local motion_result = motion.air_motion_result( vector(0, 10), false, false, @@ -3465,7 +3465,7 @@ describe('player_char', function () -- we assume _compute_max_pixel_distance is correct pc:_advance_in_air_along(motion_result, vector(0.5, 99), "x") - assert.are_equal(collision.air_motion_result( + assert.are_equal(motion.air_motion_result( vector(0.5, 10), false, false, @@ -3476,7 +3476,7 @@ describe('player_char', function () end) it('(vector(0.2, 10) at speed 0.5 along x) should move to vector(0.7, 10) without being blocked', function () - local motion_result = collision.air_motion_result( + local motion_result = motion.air_motion_result( vector(0.2, 10), false, false, @@ -3487,7 +3487,7 @@ describe('player_char', function () -- we assume _compute_max_pixel_distance is correct pc:_advance_in_air_along(motion_result, vector(0.5, 99), "x") - assert.are_equal(collision.air_motion_result( + assert.are_equal(motion.air_motion_result( vector(0.7, 10), false, false, @@ -3498,7 +3498,7 @@ describe('player_char', function () end) it('(vector(0.5, 10) at speed 0.5 along x) should move to vector(1, 10) without being blocked', function () - local motion_result = collision.air_motion_result( + local motion_result = motion.air_motion_result( vector(0.5, 10), false, false, @@ -3509,7 +3509,7 @@ describe('player_char', function () -- we assume _compute_max_pixel_distance is correct pc:_advance_in_air_along(motion_result, vector(0.5, 99), "x") - assert.are_equal(collision.air_motion_result( + assert.are_equal(motion.air_motion_result( vector(1, 10), false, false, @@ -3520,7 +3520,7 @@ describe('player_char', function () end) it('(vector(0.4, 10) at speed 2.7 along x) should move to vector(3.1, 10)', function () - local motion_result = collision.air_motion_result( + local motion_result = motion.air_motion_result( vector(0.4, 10), false, false, @@ -3531,7 +3531,7 @@ describe('player_char', function () -- we assume _compute_max_pixel_distance is correct pc:_advance_in_air_along(motion_result, vector(2.7, 99), "x") - assert.are_equal(collision.air_motion_result( + assert.are_equal(motion.air_motion_result( vector(3.1, 10), false, false, @@ -3542,7 +3542,7 @@ describe('player_char', function () end) it('(vector(2.5, 10) at speed 2.7 along x) should move to vector(5, 10) and blocked by wall', function () - local motion_result = collision.air_motion_result( + local motion_result = motion.air_motion_result( vector(2.5, 10), false, false, @@ -3553,7 +3553,7 @@ describe('player_char', function () -- we assume _compute_max_pixel_distance is correct pc:_advance_in_air_along(motion_result, vector(2.7, 99), "x") - assert.are_equal(collision.air_motion_result( + assert.are_equal(motion.air_motion_result( vector(5, 10), true, false, @@ -3564,7 +3564,7 @@ describe('player_char', function () end) it('(vector(2.5, 7.3) at speed -4.4 along y) should move to vector(2.5, 2.9) without being blocked', function () - local motion_result = collision.air_motion_result( + local motion_result = motion.air_motion_result( vector(2.5, 7.3), false, false, @@ -3593,7 +3593,7 @@ describe('player_char', function () describe('_next_air_step', function () it('(in the air) direction up should move 1px up without being blocked', function () - local motion_result = collision.air_motion_result( + local motion_result = motion.air_motion_result( vector(2, 7), false, false, @@ -3603,7 +3603,7 @@ describe('player_char', function () pc:_next_air_step(directions.up, motion_result) - assert.are_equal(collision.air_motion_result( + assert.are_equal(motion.air_motion_result( vector(2, 6), false, false, @@ -3615,7 +3615,7 @@ describe('player_char', function () end) it('(in the air) direction down should move 1px down without being blocked', function () - local motion_result = collision.air_motion_result( + local motion_result = motion.air_motion_result( vector(2, 7), false, false, @@ -3625,7 +3625,7 @@ describe('player_char', function () pc:_next_air_step(directions.down, motion_result) - assert.are_equal(collision.air_motion_result( + assert.are_equal(motion.air_motion_result( vector(2, 8), false, false, @@ -3637,7 +3637,7 @@ describe('player_char', function () end) it('(in the air) direction left should move 1px left without being blocked', function () - local motion_result = collision.air_motion_result( + local motion_result = motion.air_motion_result( vector(2, 7), false, false, @@ -3647,7 +3647,7 @@ describe('player_char', function () pc:_next_air_step(directions.left, motion_result) - assert.are_equal(collision.air_motion_result( + assert.are_equal(motion.air_motion_result( vector(1, 7), false, false, @@ -3659,7 +3659,7 @@ describe('player_char', function () end) it('(in the air) direction right should move 1px right without being blocked', function () - local motion_result = collision.air_motion_result( + local motion_result = motion.air_motion_result( vector(2, 7), false, false, @@ -3669,7 +3669,7 @@ describe('player_char', function () pc:_next_air_step(directions.right, motion_result) - assert.are_equal(collision.air_motion_result( + assert.are_equal(motion.air_motion_result( vector(3, 7), false, false, @@ -3705,7 +3705,7 @@ describe('player_char', function () end) it('direction up into ceiling should not move, and flag is_blocked_by_ceiling', function () - local motion_result = collision.air_motion_result( + local motion_result = motion.air_motion_result( vector(4, 8 + pc_data.full_height_standing - pc_data.center_height_standing), false, false, @@ -3715,7 +3715,7 @@ describe('player_char', function () pc:_next_air_step(directions.up, motion_result) - assert.are_equal(collision.air_motion_result( + assert.are_equal(motion.air_motion_result( vector(4, 8 + pc_data.full_height_standing - pc_data.center_height_standing), false, true, @@ -3727,7 +3727,7 @@ describe('player_char', function () end) it('direction down into ground should not move, and flag is_landing with slope_angle', function () - local motion_result = collision.air_motion_result( + local motion_result = motion.air_motion_result( vector(4, 0 - pc_data.center_height_standing), false, false, @@ -3737,7 +3737,7 @@ describe('player_char', function () pc:_next_air_step(directions.down, motion_result) - assert.are_equal(collision.air_motion_result( + assert.are_equal(motion.air_motion_result( vector(4, 0 - pc_data.center_height_standing), false, false, @@ -3749,7 +3749,7 @@ describe('player_char', function () end) it('direction left into wall via ground should not move, and flag is_blocked_by_wall', function () - local motion_result = collision.air_motion_result( + local motion_result = motion.air_motion_result( vector(11, 1 - pc_data.center_height_standing), false, false, @@ -3759,7 +3759,7 @@ describe('player_char', function () pc:_next_air_step(directions.left, motion_result) - assert.are_equal(collision.air_motion_result( + assert.are_equal(motion.air_motion_result( vector(11, 1 - pc_data.center_height_standing), true, false, @@ -3771,7 +3771,7 @@ describe('player_char', function () end) it('direction right into wall via ceiling should not move, and flag is_blocked_by_wall', function () - local motion_result = collision.air_motion_result( + local motion_result = motion.air_motion_result( vector(-3, 7 + pc_data.full_height_standing - pc_data.center_height_standing), false, false, @@ -3781,7 +3781,7 @@ describe('player_char', function () pc:_next_air_step(directions.right, motion_result) - assert.are_equal(collision.air_motion_result( + assert.are_equal(motion.air_motion_result( vector(-3, 7 + pc_data.full_height_standing - pc_data.center_height_standing), true, false, @@ -3793,7 +3793,7 @@ describe('player_char', function () end) it('(after landing in previous step) direction right onto new ground should move and update slope_angle', function () - local motion_result = collision.air_motion_result( + local motion_result = motion.air_motion_result( vector(-3, 0 - pc_data.center_height_standing), false, false, @@ -3803,7 +3803,7 @@ describe('player_char', function () pc:_next_air_step(directions.right, motion_result) - assert.are_equal(collision.air_motion_result( + assert.are_equal(motion.air_motion_result( vector(-2, 0 - pc_data.center_height_standing), false, false, @@ -3815,7 +3815,7 @@ describe('player_char', function () end) it('(after landing in previous step) direction left into the air should move and unset is_landing', function () - local motion_result = collision.air_motion_result( + local motion_result = motion.air_motion_result( vector(-2, 0 - pc_data.center_height_standing), false, false, @@ -3825,7 +3825,7 @@ describe('player_char', function () pc:_next_air_step(directions.left, motion_result) - assert.are_equal(collision.air_motion_result( + assert.are_equal(motion.air_motion_result( vector(-3, 0 - pc_data.center_height_standing), false, false, diff --git a/tests/utesttile.lua b/tests/utesttile.lua new file mode 100644 index 00000000..4bfe8ab2 --- /dev/null +++ b/tests/utesttile.lua @@ -0,0 +1,119 @@ +require("bustedhelper") +require("engine/core/math") +local tile = require("game/platformer/tile") +local tile_data = tile.tile_data +local height_array = tile.height_array + +describe('tile', function () + + describe('tile_data', function () + + describe('_init', function () + + it('should create a tile data setting the sprite id location and the slope angle', function () + local td = tile_data(sprite_id_location(1, 2), 0.125) + assert.are_same({sprite_id_location(1, 2), 0.125}, {td.id_loc, td.slope_angle}) + end) + + end) + + describe('_tostring', function () + + it('should return "height_array({4, 5, 6, 7, 8, 9, 10, 11}, 0.125)"', function () + local td = tile_data(sprite_id_location(1, 2), 0.125) + assert.are_equal("tile_data(sprite_id_location(1, 2), 0.125)", td:_tostring()) + end) + + end) + + end) + + describe('height_array', function () + + describe("mocking _fill_array", function () + + local fill_array_mock + + setup(function () + fill_array_mock = stub(height_array, "_fill_array", function (array, tile_mask_sprite_id_location) + for i = 1, tile_size do + array[i] = tile_mask_sprite_id_location.i + tile_mask_sprite_id_location.j + i + end + end) + end) + + teardown(function () + fill_array_mock:revert() + end) + + after_each(function () + fill_array_mock:clear() + end) + + describe('_init', function () + + it('should create a height array using fill_array and setting the slope angle', function () + local h_array = height_array(tile_data(sprite_id_location(1, 2), 0.125)) + assert.are_same({{4, 5, 6, 7, 8, 9, 10, 11}, 0.125}, {h_array._array, h_array.slope_angle}) + end) + + end) + + describe('_tostring', function () + + it('should return "height_array({4, 5, 6, 7, 8, 9, 10, 11}, 0.125)"', function () + local h_array = height_array(tile_data(sprite_id_location(1, 2), 0.125)) + assert.are_equal("height_array({4, 5, 6, 7, 8, 9, 10, 11}, 0.125)", h_array:_tostring()) + end) + + end) + + describe('get_height', function () + + it('should return the height at the given column index', function () + local h_array = height_array(tile_data(sprite_id_location(1, 2), 0.125)) + assert.are_equal(6, h_array:get_height(2)) + end) + + end) + + end) + + describe('_fill_array', function () + + local sget_mock + + setup(function () + -- simulate an sget that would return the pixel of a tile mask + -- if coordinates fall in the sprite at location (1, 2), i.e. [8-15] x [16-23], + -- where mock_height_array contains the respective height of the mask columns + -- for each column from left to right + local mock_height_array = {2, 3, 5, 6, 0, 1, 4, 2} + sget_mock = stub(_G, "sget", function (x, y) + if x >= 8 and x <= 15 and y >= 16 and y <= 23 then + -- return filled pixel color iff below mask height on this column + local height = mock_height_array[x - 7] + if y - 16 >= tile_size - height then + return 1 + else + return 0 + end + end + end) + end) + + teardown(function () + sget_mock:revert() + end) + + it('should fill the array with ', function () + local array = {} + height_array._fill_array(array, sprite_id_location(1, 2)) + assert.are_same({2, 3, 5, 6, 0, 1, 4, 2}, array) + end) + + end) + + end) + +end) diff --git a/tests/utesttile_data.lua b/tests/utesttile_data.lua index b32dca55..8ebf8ddf 100644 --- a/tests/utesttile_data.lua +++ b/tests/utesttile_data.lua @@ -7,19 +7,16 @@ describe('tiledata', function () setup(function () stub(tile_test_data, "setup") stub(tile_test_data, "teardown") - stub(pico8, "clear_map") end) teardown(function () tile_test_data.setup:revert() tile_test_data.teardown:revert() - pico8.clear_map:revert() end) after_each(function () tile_test_data.setup:clear() tile_test_data.teardown:clear() - pico8.clear_map:clear() end) describe('setup_map_data', function () @@ -38,12 +35,4 @@ describe('tiledata', function () end) end) - describe('clear_map', function () - it('should call clear_map from pico8api (busted only)', function () - clear_map() - assert.spy(pico8.clear_map).was_called(1) - assert.spy(pico8.clear_map).was_called_with(pico8) - end) - end) - end) diff --git a/tests/utesttilemap.lua b/tests/utesttilemap.lua index b5dff7a7..495e1cd7 100644 --- a/tests/utesttilemap.lua +++ b/tests/utesttilemap.lua @@ -22,4 +22,25 @@ describe('tilemap', function () end) end) + describe('clear_map', function () + + setup(function () + stub(pico8, "clear_map") + end) + + teardown(function () + pico8.clear_map:revert() + end) + + after_each(function () + pico8.clear_map:clear() + end) + + it('should call clear_map from pico8api (busted only)', function () + tilemap.clear_map() + assert.spy(pico8.clear_map).was_called(1) + assert.spy(pico8.clear_map).was_called_with(match.ref(pico8)) + end) + end) + end) diff --git a/tests/utestworld.lua b/tests/utestworld.lua index c1b3659c..10072e9b 100644 --- a/tests/utestworld.lua +++ b/tests/utestworld.lua @@ -1,4 +1,4 @@ -local world = require("engine/physics/world") +local world = require("game/platformer/world") local tile_test_data = require("game/test_data/tile_test_data") describe('world (with mock tiles data setup)', function () From 521713ac9e4caff2feaf20efcb713d902dcc0e04 Mon Sep 17 00:00:00 2001 From: hsandt Date: Sat, 27 Apr 2019 01:45:42 +0100 Subject: [PATCH 126/140] [ENGINE] Moved itest_dsl to game since it's very game-specific. Removed unused require to game script in engine ui --- src/engine/ui/ui.lua | 1 - src/{engine/test => game/itests}/itest_dsl.lua | 0 src/game/itests/itestplayercharacter.lua | 2 +- tests/utestitest_dsl.lua | 2 +- tests/utestui.lua | 1 + 5 files changed, 3 insertions(+), 3 deletions(-) rename src/{engine/test => game/itests}/itest_dsl.lua (100%) diff --git a/src/engine/ui/ui.lua b/src/engine/ui/ui.lua index 11afdec5..0abac02f 100644 --- a/src/engine/ui/ui.lua +++ b/src/engine/ui/ui.lua @@ -3,7 +3,6 @@ local logging = require("engine/debug/logging") --#endif local input = require("engine/input/input") -local visual = require("game/resources/visual") local ui = { cursor_sprite_data = nil diff --git a/src/engine/test/itest_dsl.lua b/src/game/itests/itest_dsl.lua similarity index 100% rename from src/engine/test/itest_dsl.lua rename to src/game/itests/itest_dsl.lua diff --git a/src/game/itests/itestplayercharacter.lua b/src/game/itests/itestplayercharacter.lua index 78531ffe..bdd5e154 100644 --- a/src/game/itests/itestplayercharacter.lua +++ b/src/game/itests/itestplayercharacter.lua @@ -1,6 +1,6 @@ -- gamestates: stage local integrationtest = require("engine/test/integrationtest") -local itest_dsl = require("engine/test/itest_dsl") +local itest_dsl = require("game/itests/itest_dsl") local itest_dsl_parser = itest_dsl.itest_dsl_parser local itest_manager, integration_test, time_trigger = get_members(integrationtest, "itest_manager", "integration_test", "time_trigger") diff --git a/tests/utestitest_dsl.lua b/tests/utestitest_dsl.lua index 507ae854..beaacf2f 100644 --- a/tests/utestitest_dsl.lua +++ b/tests/utestitest_dsl.lua @@ -1,7 +1,7 @@ require("bustedhelper") require("engine/core/helper") require("engine/core/math") -local itest_dsl = require("engine/test/itest_dsl") +local itest_dsl = require("game/itests/itest_dsl") local gameplay_value_data, generate_function_table = get_members(itest_dsl, "gameplay_value_data", "generate_function_table") local eval_pc_bottom_pos, eval_pc_velocity, eval_pc_ground_spd, eval_pc_motion_state, eval_pc_slope = get_members(itest_dsl, diff --git a/tests/utestui.lua b/tests/utestui.lua index 9cf87f68..5d835f01 100644 --- a/tests/utestui.lua +++ b/tests/utestui.lua @@ -2,6 +2,7 @@ require("bustedhelper") require("engine/render/color") local ui = require("engine/ui/ui") local input = require("engine/input/input") +local sprite_data = require("engine/render/sprite_data") local label = ui.label local overlay = ui.overlay From d39fcbb75dd36b563c8c2bf79e033efccdf1312d Mon Sep 17 00:00:00 2001 From: hsandt Date: Sat, 27 Apr 2019 03:12:31 +0100 Subject: [PATCH 127/140] [TEST] Separated engine and game tests in different folders --- .luacov_all | 8 +++---- {tests => src/engine/test}/bustedhelper.lua | 2 +- {tests => src/engine/test}/pico8api.lua | 2 +- .../engine/tests}/utestanimated_sprite.lua | 2 +- .../tests}/utestanimated_sprite_data.lua | 2 +- .../engine/tests}/utestassertions.lua | 2 +- .../engine/tests}/utestbustedhelper.lua | 12 +++++----- {tests => src/engine/tests}/utestclass.lua | 23 ++++++++----------- .../engine/tests}/utestcodetuner.lua | 2 +- .../engine/tests}/utestcollision.lua | 2 +- {tests => src/engine/tests}/utestcolor.lua | 2 +- .../engine/tests}/utestcoroutine.lua | 2 +- .../engine/tests}/utestdatastruct.lua | 5 +++- .../engine/tests}/utestdebug_window.lua | 2 +- {tests => src/engine/tests}/utestflow.lua | 2 +- {tests => src/engine/tests}/utesthelper.lua | 2 +- {tests => src/engine/tests}/utestinput.lua | 2 +- .../engine/tests}/utestintegrationtest.lua | 2 +- {tests => src/engine/tests}/utestlogging.lua | 2 +- {tests => src/engine/tests}/utestmath.lua | 2 +- {tests => src/engine/tests}/utestpico8api.lua | 21 +++++++---------- .../engine/tests}/utestpico8wtk.lua | 2 +- {tests => src/engine/tests}/utestprofiler.lua | 2 +- .../engine/tests}/utestsprite_data.lua | 4 ++-- {tests => src/engine/tests}/utesttilemap.lua | 2 +- {tests => src/engine/tests}/utestui.lua | 2 +- {tests => src/engine/tests}/utestunittest.lua | 2 +- .../engine/tests}/utestunittest_helper.lua | 2 +- .../engine/tests}/utestvisual_logger.lua | 2 +- src/game/itest_main.lua | 2 +- src/game/test_data/tile_test_data.lua | 2 +- {tests => src/game/tests}/utestcredits.lua | 2 +- .../game/tests}/utestdummy_gamestate.lua | 2 +- {tests => src/game/tests}/utestgameapp.lua | 2 +- .../game/tests}/utestgamestate_proxy.lua | 2 +- .../game/tests}/utestheadless_itests.lua | 2 +- {tests => src/game/tests}/utestitest_dsl.lua | 2 +- {tests => src/game/tests}/utestmotion.lua | 2 +- .../game/tests}/utestplayercharacter.lua | 10 ++++---- {tests => src/game/tests}/uteststage.lua | 2 +- {tests => src/game/tests}/utesttile.lua | 2 +- {tests => src/game/tests}/utesttile_data.lua | 2 +- {tests => src/game/tests}/utesttitlemenu.lua | 2 +- {tests => src/game/tests}/utestworld.lua | 0 test.sh | 4 ++-- 45 files changed, 77 insertions(+), 82 deletions(-) rename {tests => src/engine/test}/bustedhelper.lua (99%) rename {tests => src/engine/test}/pico8api.lua (99%) rename {tests => src/engine/tests}/utestanimated_sprite.lua (99%) rename {tests => src/engine/tests}/utestanimated_sprite_data.lua (97%) rename {tests => src/engine/tests}/utestassertions.lua (98%) rename {tests => src/engine/tests}/utestbustedhelper.lua (69%) rename {tests => src/engine/tests}/utestclass.lua (95%) rename {tests => src/engine/tests}/utestcodetuner.lua (99%) rename {tests => src/engine/tests}/utestcollision.lua (99%) rename {tests => src/engine/tests}/utestcolor.lua (98%) rename {tests => src/engine/tests}/utestcoroutine.lua (96%) rename {tests => src/engine/tests}/utestdatastruct.lua (98%) rename {tests => src/engine/tests}/utestdebug_window.lua (98%) rename {tests => src/engine/tests}/utestflow.lua (99%) rename {tests => src/engine/tests}/utesthelper.lua (99%) rename {tests => src/engine/tests}/utestinput.lua (99%) rename {tests => src/engine/tests}/utestintegrationtest.lua (99%) rename {tests => src/engine/tests}/utestlogging.lua (99%) rename {tests => src/engine/tests}/utestmath.lua (99%) rename {tests => src/engine/tests}/utestpico8api.lua (98%) rename {tests/wtk => src/engine/tests}/utestpico8wtk.lua (99%) rename {tests => src/engine/tests}/utestprofiler.lua (97%) rename {tests => src/engine/tests}/utestsprite_data.lua (99%) rename {tests => src/engine/tests}/utesttilemap.lua (97%) rename {tests => src/engine/tests}/utestui.lua (99%) rename {tests => src/engine/tests}/utestunittest.lua (98%) rename {tests => src/engine/tests}/utestunittest_helper.lua (97%) rename {tests => src/engine/tests}/utestvisual_logger.lua (99%) rename {tests => src/game/tests}/utestcredits.lua (96%) rename {tests => src/game/tests}/utestdummy_gamestate.lua (97%) rename {tests => src/game/tests}/utestgameapp.lua (99%) rename {tests => src/game/tests}/utestgamestate_proxy.lua (97%) rename {tests/itests => src/game/tests}/utestheadless_itests.lua (98%) rename {tests => src/game/tests}/utestitest_dsl.lua (99%) rename {tests => src/game/tests}/utestmotion.lua (99%) rename {tests => src/game/tests}/utestplayercharacter.lua (99%) rename {tests => src/game/tests}/uteststage.lua (99%) rename {tests => src/game/tests}/utesttile.lua (98%) rename {tests => src/game/tests}/utesttile_data.lua (96%) rename {tests => src/game/tests}/utesttitlemenu.lua (99%) rename {tests => src/game/tests}/utestworld.lua (100%) diff --git a/.luacov_all b/.luacov_all index 79b633e2..a4f78ebd 100644 --- a/.luacov_all +++ b/.luacov_all @@ -1,15 +1,15 @@ return { include = { - "src", - "tests/bustedhelper", - "tests/pico8api" + "src" }, exclude = { + "src/engine/tests", "src/engine/wtk", "src/game/itests", - "src/game/test_data" + "src/game/test_data", + "src/game/tests" } } diff --git a/tests/bustedhelper.lua b/src/engine/test/bustedhelper.lua similarity index 99% rename from tests/bustedhelper.lua rename to src/engine/test/bustedhelper.lua index 61a98e54..fde86f1e 100644 --- a/tests/bustedhelper.lua +++ b/src/engine/test/bustedhelper.lua @@ -1,5 +1,5 @@ -- required module for all tests -require("pico8api") +require("engine/test/pico8api") require("engine/test/assertions") -- mute all messages during tests, unless told otherwise during the tests diff --git a/tests/pico8api.lua b/src/engine/test/pico8api.lua similarity index 99% rename from tests/pico8api.lua rename to src/engine/test/pico8api.lua index f1ab6a21..a2d25d8e 100644 --- a/tests/pico8api.lua +++ b/src/engine/test/pico8api.lua @@ -1,6 +1,6 @@ -- pico-8 api placeholders for tests run under vanilla lua -- pico8:method calls in non-test scripts should be surrounded by --- "--#ifn pico8" but don't need a require("pico8api") (since they will +-- "--#ifn pico8" but don't need a require("engine/test/pico8api") (since they will -- always be required by a utest script already requiring bustedhelper) -- functions taken from gamax92's fork of picolove diff --git a/tests/utestanimated_sprite.lua b/src/engine/tests/utestanimated_sprite.lua similarity index 99% rename from tests/utestanimated_sprite.lua rename to src/engine/tests/utestanimated_sprite.lua index 1f08b9c9..96d0e75f 100644 --- a/tests/utestanimated_sprite.lua +++ b/src/engine/tests/utestanimated_sprite.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") local sprite_data = require("engine/render/sprite_data") local animated_sprite_data = require("engine/render/animated_sprite_data") local animated_sprite = require("engine/render/animated_sprite") diff --git a/tests/utestanimated_sprite_data.lua b/src/engine/tests/utestanimated_sprite_data.lua similarity index 97% rename from tests/utestanimated_sprite_data.lua rename to src/engine/tests/utestanimated_sprite_data.lua index 3c6aae78..478b2556 100644 --- a/tests/utestanimated_sprite_data.lua +++ b/src/engine/tests/utestanimated_sprite_data.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") local sprite_data = require("engine/render/sprite_data") local animated_sprite_data = require("engine/render/animated_sprite_data") diff --git a/tests/utestassertions.lua b/src/engine/tests/utestassertions.lua similarity index 98% rename from tests/utestassertions.lua rename to src/engine/tests/utestassertions.lua index cb74fec5..6bf33853 100644 --- a/tests/utestassertions.lua +++ b/src/engine/tests/utestassertions.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") require("engine/core/math") require("engine/test/assertions") diff --git a/tests/utestbustedhelper.lua b/src/engine/tests/utestbustedhelper.lua similarity index 69% rename from tests/utestbustedhelper.lua rename to src/engine/tests/utestbustedhelper.lua index a3b77156..012870f1 100644 --- a/tests/utestbustedhelper.lua +++ b/src/engine/tests/utestbustedhelper.lua @@ -1,23 +1,23 @@ -require("bustedhelper") +require("engine/test/bustedhelper") require("engine/core/math") describe('bustedhelper', function () describe('get_file_line', function () it('should return "file:line" of the get_file_line call by default', function () - assert.are_equal("@tests/utestbustedhelper.lua:8", get_file_line()) -- line 8 + assert.are_equal("@src/engine/tests/utestbustedhelper.lua:8", get_file_line()) -- line 8 end) it('should return "file:line" of the function calling get_file_line with extra_level 1', function () local function inside() - assert.are_equal("@tests/utestbustedhelper.lua:14", get_file_line(1)) + assert.are_equal("@src/engine/tests/utestbustedhelper.lua:14", get_file_line(1)) end inside() -- line 14 end) it('should return "file:line" of the function calling the function calling get_file_line with extra_level 1', function () local function outside() local function inside() - assert.are_equal("@tests/utestbustedhelper.lua:22", get_file_line(1)) - assert.are_equal("@tests/utestbustedhelper.lua:24", get_file_line(2)) + assert.are_equal("@src/engine/tests/utestbustedhelper.lua:22", get_file_line(1)) + assert.are_equal("@src/engine/tests/utestbustedhelper.lua:24", get_file_line(2)) end inside() -- line 22 end @@ -44,7 +44,7 @@ describe('bustedhelper', function () it('should print the current file:line with a message', function () print_at_line("text") -- line 45 assert.spy(native_print_stub).was_called(1) - assert.spy(native_print_stub).was_called_with("@tests/utestbustedhelper.lua:45: text") + assert.spy(native_print_stub).was_called_with("@src/engine/tests/utestbustedhelper.lua:45: text") end) end) diff --git a/tests/utestclass.lua b/src/engine/tests/utestclass.lua similarity index 95% rename from tests/utestclass.lua rename to src/engine/tests/utestclass.lua index a8f78613..18735e39 100644 --- a/tests/utestclass.lua +++ b/src/engine/tests/utestclass.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") local class = require("engine/core/class") local dummy_class = new_class() @@ -160,14 +160,6 @@ describe('new_struct', function () self.table = dummy_class(value) -- struct should never contain non-struct tables end - function invalid_struct:_tostring() - return "invalid_struct: "..tostr(self.value) - end - - function invalid_struct:_tostring() - return "invalid_struct: "..joinstr(", ", self.sum, self.sub_struct) - end - it('should create a new struct with _init()', function () local dummy = dummy_struct(3, 7) assert.are_same({3, 7}, {dummy.value1, dummy.value2}) @@ -293,7 +285,7 @@ describe('new_struct', function () end function dummy_derived_struct:_tostring() - return "dummy_derived: "..joinstr(", ", self.value1, self.value2, self.value3) + return "dummy_derived_struct: "..joinstr(", ", self.value1, self.value2, self.value3) end function dummy_derived_struct:get_sum() @@ -310,6 +302,11 @@ describe('new_struct', function () assert.are_equal(19, dummy_derived:get_sum()) end) + it('should support instance concatenation', function () + local dummy_derived = dummy_derived_struct(3, 7, 9) + assert.are_equal("val: dummy_derived_struct: 3, 7, 9", "val: "..dummy_derived) + end) + describe('struct equality', function () it('should return true for two structs equal by reference', function () @@ -401,7 +398,7 @@ describe('derived_singleton', function () end) function my_derived_singleton:_tostring() - return "[my_derived_singleton "..self.types[1]..", "..self.subtype.."]" + return "[my_derived_singleton "..my_singleton._tostring(self)..", "..self.subtype.."]" end local my_derived_singleton_no_init = derived_singleton(my_derived_singleton) @@ -467,11 +464,11 @@ describe('derived_singleton', function () end) it('should support custom method: _tostring', function () - assert.are_equal("[my_derived_singleton custom, special]", my_derived_singleton:_tostring()) + assert.are_equal("[my_derived_singleton [my_singleton custom], special]", my_derived_singleton:_tostring()) end) it('should support string concatenation with _tostring', function () - assert.are_equal("this is [my_derived_singleton custom, special]", "this is "..my_derived_singleton) + assert.are_equal("this is [my_derived_singleton [my_singleton custom], special]", "this is "..my_derived_singleton) end) end) diff --git a/tests/utestcodetuner.lua b/src/engine/tests/utestcodetuner.lua similarity index 99% rename from tests/utestcodetuner.lua rename to src/engine/tests/utestcodetuner.lua index b56bfe4c..fd40587f 100644 --- a/tests/utestcodetuner.lua +++ b/src/engine/tests/utestcodetuner.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") local codetuner = require("engine/debug/codetuner") describe('codetuner', function () diff --git a/tests/utestcollision.lua b/src/engine/tests/utestcollision.lua similarity index 99% rename from tests/utestcollision.lua rename to src/engine/tests/utestcollision.lua index e501a3c7..e8df4063 100644 --- a/tests/utestcollision.lua +++ b/src/engine/tests/utestcollision.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") require("engine/application/constants") require("engine/core/math") local collision = require("engine/physics/collision") diff --git a/tests/utestcolor.lua b/src/engine/tests/utestcolor.lua similarity index 98% rename from tests/utestcolor.lua rename to src/engine/tests/utestcolor.lua index e619e345..b1b6054f 100644 --- a/tests/utestcolor.lua +++ b/src/engine/tests/utestcolor.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") require("engine/render/color") describe('color_tostring', function () diff --git a/tests/utestcoroutine.lua b/src/engine/tests/utestcoroutine.lua similarity index 96% rename from tests/utestcoroutine.lua rename to src/engine/tests/utestcoroutine.lua index b4737ebc..569e88e2 100644 --- a/tests/utestcoroutine.lua +++ b/src/engine/tests/utestcoroutine.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") require("engine/core/coroutine") describe('coroutine_curry', function () diff --git a/tests/utestdatastruct.lua b/src/engine/tests/utestdatastruct.lua similarity index 98% rename from tests/utestdatastruct.lua rename to src/engine/tests/utestdatastruct.lua index d0ce24d3..b16344f6 100644 --- a/tests/utestdatastruct.lua +++ b/src/engine/tests/utestdatastruct.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") require("engine/core/datastruct") describe('circular_buffer', function () @@ -110,6 +110,8 @@ describe('circular_buffer', function () max_length = 3, buffer = {1, 2, 10}, get = function (self, i) + -- circular_buffer.__eq won't even try to compare cb1 and cb2, + -- so this will actually never be called return self._buffer[i] end } @@ -147,6 +149,7 @@ describe('circular_buffer', function () it('(^): no iteration at all', function () local cb = circular_buffer(2) for i, v in ipairs(cb) do + -- should never be called assert.is_true(false) end end) diff --git a/tests/utestdebug_window.lua b/src/engine/tests/utestdebug_window.lua similarity index 98% rename from tests/utestdebug_window.lua rename to src/engine/tests/utestdebug_window.lua index cddea119..1f0b0e11 100644 --- a/tests/utestdebug_window.lua +++ b/src/engine/tests/utestdebug_window.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") local debug_window = require("engine/debug/debug_window") local wtk = require("engine/wtk/pico8wtk") diff --git a/tests/utestflow.lua b/src/engine/tests/utestflow.lua similarity index 99% rename from tests/utestflow.lua rename to src/engine/tests/utestflow.lua index 9097a862..271e5639 100644 --- a/tests/utestflow.lua +++ b/src/engine/tests/utestflow.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") local flow = require("engine/application/flow") local helper = require("engine/core/helper") local titlemenu = require("game/menu/titlemenu") diff --git a/tests/utesthelper.lua b/src/engine/tests/utesthelper.lua similarity index 99% rename from tests/utesthelper.lua rename to src/engine/tests/utesthelper.lua index 882a9905..afc9d581 100644 --- a/tests/utesthelper.lua +++ b/src/engine/tests/utesthelper.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") helper = require("engine/core/helper") math = require("engine/core/math") -- just to test stringify and are_same diff --git a/tests/utestinput.lua b/src/engine/tests/utestinput.lua similarity index 99% rename from tests/utestinput.lua rename to src/engine/tests/utestinput.lua index 32f95554..cb72b024 100644 --- a/tests/utestinput.lua +++ b/src/engine/tests/utestinput.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") local input = require("engine/input/input") describe('input', function () diff --git a/tests/utestintegrationtest.lua b/src/engine/tests/utestintegrationtest.lua similarity index 99% rename from tests/utestintegrationtest.lua rename to src/engine/tests/utestintegrationtest.lua index b11f9b19..4f0e7216 100644 --- a/tests/utestintegrationtest.lua +++ b/src/engine/tests/utestintegrationtest.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") require("engine/core/helper") require("engine/render/color") local integrationtest = require("engine/test/integrationtest") diff --git a/tests/utestlogging.lua b/src/engine/tests/utestlogging.lua similarity index 99% rename from tests/utestlogging.lua rename to src/engine/tests/utestlogging.lua index b4d0e5bb..3b474e3c 100644 --- a/tests/utestlogging.lua +++ b/src/engine/tests/utestlogging.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") require("engine/core/math") local logging = require("engine/debug/logging") diff --git a/tests/utestmath.lua b/src/engine/tests/utestmath.lua similarity index 99% rename from tests/utestmath.lua rename to src/engine/tests/utestmath.lua index f236a5c0..2af4482e 100644 --- a/tests/utestmath.lua +++ b/src/engine/tests/utestmath.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") require("engine/core/math") describe('almost_eq', function () diff --git a/tests/utestpico8api.lua b/src/engine/tests/utestpico8api.lua similarity index 98% rename from tests/utestpico8api.lua rename to src/engine/tests/utestpico8api.lua index c972944e..89cebf41 100644 --- a/tests/utestpico8api.lua +++ b/src/engine/tests/utestpico8api.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") require("engine/core/math") describe('pico8api', function () @@ -978,12 +978,14 @@ describe('pico8api', function () it('should return an empty iterator for nil', function () for value in all(nil) do + -- should never be called assert.is_true(false) end end) it('should return an empty iterator for an empty sequence', function () for value in all({}) do + -- should never be called assert.is_true(false) end end) @@ -1087,9 +1089,10 @@ describe('pico8api', function () local file_path = dirpath..'/'..file if file ~= "." and file ~= ".." then if lfs.attributes(file_path, 'mode') == 'file' then - os.remove(file_path) + os.remove(file_path) elseif lfs.attributes(file_path, 'mode') == 'directory' then - remove_dir_recursive(file_path) + -- just a safety net (if you apply coverage to utest files you'll see it's never called) + remove_dir_recursive(file_path) end end end @@ -1107,14 +1110,6 @@ describe('pico8api', function () end end - local function remove_file_if_exists(filepath) - local f = io.open(temp_filepath) - if f then - f:close() - os.remove(temp_filepath) - end - end - local function get_lines(file) local lines = {} for line in file:lines() do @@ -1129,8 +1124,8 @@ describe('pico8api', function () after_each(function () if temp_file then - -- an error occurred (maybe the assert failed) - -- and the temp file wasn't closed and set to nil + -- an error occurred (maybe the assert failed) and the temp file wasn't closed and set to nil + -- this is never called in working tests print("WARNING: emergency close needed, the last write operation likely failed") temp_file:close() end diff --git a/tests/wtk/utestpico8wtk.lua b/src/engine/tests/utestpico8wtk.lua similarity index 99% rename from tests/wtk/utestpico8wtk.lua rename to src/engine/tests/utestpico8wtk.lua index 925c223e..449e8a20 100644 --- a/tests/wtk/utestpico8wtk.lua +++ b/src/engine/tests/utestpico8wtk.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") local wtk = require("engine/wtk/pico8wtk") describe('wtk', function () diff --git a/tests/utestprofiler.lua b/src/engine/tests/utestprofiler.lua similarity index 97% rename from tests/utestprofiler.lua rename to src/engine/tests/utestprofiler.lua index 8757dd65..7ed983c8 100644 --- a/tests/utestprofiler.lua +++ b/src/engine/tests/utestprofiler.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") local profiler = require("engine/debug/profiler") describe('profiler', function () diff --git a/tests/utestsprite_data.lua b/src/engine/tests/utestsprite_data.lua similarity index 99% rename from tests/utestsprite_data.lua rename to src/engine/tests/utestsprite_data.lua index 684a2c8d..7bf3e61b 100644 --- a/tests/utestsprite_data.lua +++ b/src/engine/tests/utestsprite_data.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") local sprite_data = require("engine/render/sprite_data") describe('sprite', function () @@ -59,7 +59,7 @@ describe('sprite', function () it('should render the sprite from the id location, at the draw position minus pivot, with correct span when not flipping', function () spr_data:render(vector(41, 80), false, false) - + assert.spy(spr_stub).was_called(1) assert.spy(spr_stub).was_called_with(49, 30, 70, 2, 3, false, false) end) diff --git a/tests/utesttilemap.lua b/src/engine/tests/utesttilemap.lua similarity index 97% rename from tests/utesttilemap.lua rename to src/engine/tests/utesttilemap.lua index 495e1cd7..62477ce4 100644 --- a/tests/utesttilemap.lua +++ b/src/engine/tests/utesttilemap.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") local tilemap = require("engine/data/tilemap") describe('tilemap', function () diff --git a/tests/utestui.lua b/src/engine/tests/utestui.lua similarity index 99% rename from tests/utestui.lua rename to src/engine/tests/utestui.lua index 5d835f01..297b8aa9 100644 --- a/tests/utestui.lua +++ b/src/engine/tests/utestui.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") require("engine/render/color") local ui = require("engine/ui/ui") local input = require("engine/input/input") diff --git a/tests/utestunittest.lua b/src/engine/tests/utestunittest.lua similarity index 98% rename from tests/utestunittest.lua rename to src/engine/tests/utestunittest.lua index f5cf2b98..2346af7c 100644 --- a/tests/utestunittest.lua +++ b/src/engine/tests/utestunittest.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") local unittest = require("engine/test/unittest") local utest_manager, unit_test, time_trigger = unittest.utest_manager, unittest.unit_test diff --git a/tests/utestunittest_helper.lua b/src/engine/tests/utestunittest_helper.lua similarity index 97% rename from tests/utestunittest_helper.lua rename to src/engine/tests/utestunittest_helper.lua index a4d7a036..ba6b93fe 100644 --- a/tests/utestunittest_helper.lua +++ b/src/engine/tests/utestunittest_helper.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") require("engine/test/unittest_helper") describe('unittest_helper', function () diff --git a/tests/utestvisual_logger.lua b/src/engine/tests/utestvisual_logger.lua similarity index 99% rename from tests/utestvisual_logger.lua rename to src/engine/tests/utestvisual_logger.lua index 52bc6baf..08d91832 100644 --- a/tests/utestvisual_logger.lua +++ b/src/engine/tests/utestvisual_logger.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") local vlogger = require("engine/debug/visual_logger") local logging = require("engine/debug/logging") local wtk = require("engine/wtk/pico8wtk") diff --git a/src/game/itest_main.lua b/src/game/itest_main.lua index 4f48aa65..a4d72a66 100644 --- a/src/game/itest_main.lua +++ b/src/game/itest_main.lua @@ -4,9 +4,9 @@ -- to use for the build. other states will be replaced with dummy equivalents. require("engine/test/integrationtest") +local input = require("engine/input/input") require("game/itests/itest$itest") local gamestate_proxy = require("game/application/gamestate_proxy") -local input = require("engine/input/input") --#if log local logging = require("engine/debug/logging") diff --git a/src/game/test_data/tile_test_data.lua b/src/game/test_data/tile_test_data.lua index 344998b7..f9a495e3 100644 --- a/src/game/test_data/tile_test_data.lua +++ b/src/game/test_data/tile_test_data.lua @@ -1,6 +1,6 @@ --#ifn pico8 -require("pico8api") +require("engine/test/pico8api") local tile = require("game/platformer/tile") local collision_data = require("game/data/collision_data") local stub = require("luassert.stub") diff --git a/tests/utestcredits.lua b/src/game/tests/utestcredits.lua similarity index 96% rename from tests/utestcredits.lua rename to src/game/tests/utestcredits.lua index 932b7d6b..ae78b66a 100644 --- a/tests/utestcredits.lua +++ b/src/game/tests/utestcredits.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") local credits = require("game/menu/credits") local gamestate = require("game/application/gamestate") diff --git a/tests/utestdummy_gamestate.lua b/src/game/tests/utestdummy_gamestate.lua similarity index 97% rename from tests/utestdummy_gamestate.lua rename to src/game/tests/utestdummy_gamestate.lua index 4c3f2c8b..73f8e83f 100644 --- a/tests/utestdummy_gamestate.lua +++ b/src/game/tests/utestdummy_gamestate.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") local dummy_gamestate = require("game/application/dummy_gamestate") describe('dummy_gamestate', function () diff --git a/tests/utestgameapp.lua b/src/game/tests/utestgameapp.lua similarity index 99% rename from tests/utestgameapp.lua rename to src/game/tests/utestgameapp.lua index cc762748..c87f7cf5 100644 --- a/tests/utestgameapp.lua +++ b/src/game/tests/utestgameapp.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") local gameapp = require("game/application/gameapp") local flow = require("engine/application/flow") diff --git a/tests/utestgamestate_proxy.lua b/src/game/tests/utestgamestate_proxy.lua similarity index 97% rename from tests/utestgamestate_proxy.lua rename to src/game/tests/utestgamestate_proxy.lua index 8bfe88be..354b5bf8 100644 --- a/tests/utestgamestate_proxy.lua +++ b/src/game/tests/utestgamestate_proxy.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") local gamestate_proxy = require("game/application/gamestate_proxy") describe('gamestate_proxy', function () diff --git a/tests/itests/utestheadless_itests.lua b/src/game/tests/utestheadless_itests.lua similarity index 98% rename from tests/itests/utestheadless_itests.lua rename to src/game/tests/utestheadless_itests.lua index 539180f3..1fccad1d 100644 --- a/tests/itests/utestheadless_itests.lua +++ b/src/game/tests/utestheadless_itests.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") require("engine/test/integrationtest") local gameapp = require("game/application/gameapp") local gamestate_proxy = require("game/application/gamestate_proxy") diff --git a/tests/utestitest_dsl.lua b/src/game/tests/utestitest_dsl.lua similarity index 99% rename from tests/utestitest_dsl.lua rename to src/game/tests/utestitest_dsl.lua index beaacf2f..7901275f 100644 --- a/tests/utestitest_dsl.lua +++ b/src/game/tests/utestitest_dsl.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") require("engine/core/helper") require("engine/core/math") local itest_dsl = require("game/itests/itest_dsl") diff --git a/tests/utestmotion.lua b/src/game/tests/utestmotion.lua similarity index 99% rename from tests/utestmotion.lua rename to src/game/tests/utestmotion.lua index d694539d..de5096d8 100644 --- a/tests/utestmotion.lua +++ b/src/game/tests/utestmotion.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") local motion = require("game/platformer/motion") local ground_query_info = motion.ground_query_info local ground_motion_result, air_motion_result = get_members(motion, diff --git a/tests/utestplayercharacter.lua b/src/game/tests/utestplayercharacter.lua similarity index 99% rename from tests/utestplayercharacter.lua rename to src/game/tests/utestplayercharacter.lua index 5b6e6842..4f40a3fb 100644 --- a/tests/utestplayercharacter.lua +++ b/src/game/tests/utestplayercharacter.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") require("engine/core/math") local player_char = require("game/ingame/playercharacter") local input = require("engine/input/input") @@ -142,7 +142,7 @@ describe('player_char', function () false, false, false, - false, + false }, { pc.control_mode, @@ -161,7 +161,7 @@ describe('player_char', function () pc.hold_jump_intention, pc.should_jump, pc.has_jumped_this_frame, - pc.has_interrupted_jump, + pc.has_interrupted_jump } ) assert.spy(animated_sprite.play).was_called(1) @@ -1277,13 +1277,13 @@ describe('player_char', function () motion_states.grounded, 0, false, - false, + false }, { pc.motion_state, pc.velocity.y, pc.has_jumped_this_frame, - pc.has_interrupted_jump, + pc.has_interrupted_jump }) assert.spy(animated_sprite.play).was_called(1) assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "idle") diff --git a/tests/uteststage.lua b/src/game/tests/uteststage.lua similarity index 99% rename from tests/uteststage.lua rename to src/game/tests/uteststage.lua index d0cf2676..a783589a 100644 --- a/tests/uteststage.lua +++ b/src/game/tests/uteststage.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") local ui = require("engine/ui/ui") local stage = require("game/ingame/stage") local state = stage.state diff --git a/tests/utesttile.lua b/src/game/tests/utesttile.lua similarity index 98% rename from tests/utesttile.lua rename to src/game/tests/utesttile.lua index 4bfe8ab2..fa49b792 100644 --- a/tests/utesttile.lua +++ b/src/game/tests/utesttile.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") require("engine/core/math") local tile = require("game/platformer/tile") local tile_data = tile.tile_data diff --git a/tests/utesttile_data.lua b/src/game/tests/utesttile_data.lua similarity index 96% rename from tests/utesttile_data.lua rename to src/game/tests/utesttile_data.lua index 8ebf8ddf..53237c17 100644 --- a/tests/utesttile_data.lua +++ b/src/game/tests/utesttile_data.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") require("game/data/tile_data") local tile_test_data = require("game/test_data/tile_test_data") diff --git a/tests/utesttitlemenu.lua b/src/game/tests/utesttitlemenu.lua similarity index 99% rename from tests/utesttitlemenu.lua rename to src/game/tests/utesttitlemenu.lua index e03b738b..7296c00c 100644 --- a/tests/utesttitlemenu.lua +++ b/src/game/tests/utesttitlemenu.lua @@ -1,4 +1,4 @@ -require("bustedhelper") +require("engine/test/bustedhelper") local input = require("engine/input/input") local titlemenu = require("game/menu/titlemenu") local flow = require("engine/application/flow") diff --git a/tests/utestworld.lua b/src/game/tests/utestworld.lua similarity index 100% rename from tests/utestworld.lua rename to src/game/tests/utestworld.lua diff --git a/test.sh b/test.sh index c8b032e0..e5a2e57c 100755 --- a/test.sh +++ b/test.sh @@ -81,8 +81,8 @@ if [[ $RENDER = true ]]; then EXTRA_ARGS+="--render" fi -LUA_PATH="src/?.lua;tests/?.lua" -CORE_TEST="busted tests --lpath=\"$LUA_PATH\" -p \"$TEST_FILE_PATTERN\" $FILTER $FILTER_OUT -c -v -- $EXTRA_ARGS" +LUA_PATH="src/?.lua;src/engine/tests/?.lua;src/game/tests/?.lua" +CORE_TEST="busted src/engine/tests src/game/tests --lpath=\"$LUA_PATH\" -p \"$TEST_FILE_PATTERN\" $FILTER $FILTER_OUT -c -v -- $EXTRA_ARGS" TEST_COMMAND="$PRE_TEST $CORE_TEST $POST_TEST" echo "Testing $1..." From f562f724aac3793d82c5451d8842115ea42094d5 Mon Sep 17 00:00:00 2001 From: hsandt Date: Sat, 27 Apr 2019 03:15:10 +0100 Subject: [PATCH 128/140] [HELPER] Fixed are_same which was completely missing custom metatable __eq definitions --- src/engine/core/helper.lua | 8 +++-- src/engine/tests/utesthelper.lua | 55 +++++++++++++++++++++++++++++--- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/src/engine/core/helper.lua b/src/engine/core/helper.lua index b75acf19..a14d2527 100644 --- a/src/engine/core/helper.lua +++ b/src/engine/core/helper.lua @@ -56,15 +56,19 @@ end -- if no_deep_raw_content is true, do not pass the compare_raw_content parameter to deeper calls -- this is useful if you want to compare content at the first level but delegate equality for embedded structs function are_same(t1, t2, compare_raw_content, no_deep_raw_content) + -- compare_raw_content and no_deep_raw_content default to false (we count on nil being falsy here) + if type(t1) ~= 'table' or type(t2) ~= 'table' then -- we have at least one non-table argument, compare by equality -- if both arguments have different types, it will return false return t1 == t2 end - -- both arguments are tables + -- both arguments are tables, check meta __eq - if (t1.__eq or t2.__eq) and not compare_raw_content then + local mt1 = getmetatable(t1) + local mt2 = getmetatable(t2) + if (mt1 and mt1.__eq or mt2 and mt2.__eq) and not compare_raw_content then -- we are not comparing raw content and equality is defined, use it return t1 == t2 end diff --git a/src/engine/tests/utesthelper.lua b/src/engine/tests/utesthelper.lua index afc9d581..00d141d6 100644 --- a/src/engine/tests/utesthelper.lua +++ b/src/engine/tests/utesthelper.lua @@ -43,15 +43,62 @@ end) describe('are_same', function () local single_t = {} - local comparable_mt = { + local comparable_mt_sum = { __eq = function (lhs, rhs) - return lhs.a == rhs.a and lhs.b == rhs.b + -- a flexible check that allows different member values to have the table considered equal in the end + return lhs.a + lhs.b == rhs.a + rhs.b end } + local comparable_mt_offset = { + __eq = function (lhs, rhs) + -- a contrived check that makes sure __eq is used by returning true when it should be false in raw content + return lhs.a == rhs.a - 1 + end + } + local comparable_struct1 = {a = 1, b = 2} local comparable_struct2 = {a = 1, b = 2} - setmetatable(comparable_struct1, comparable_mt) - setmetatable(comparable_struct2, comparable_mt) -- actually, only one is enough + local comparable_struct3 = {a = 2, b = 1} + local comparable_struct4 = {a = 1} + local comparable_struct5 = {a = 1} + local comparable_struct6 = {a = 2} + + setmetatable(comparable_struct1, comparable_mt_sum) + setmetatable(comparable_struct2, comparable_mt_sum) + setmetatable(comparable_struct3, comparable_mt_sum) + setmetatable(comparable_struct4, comparable_mt_offset) + setmetatable(comparable_struct5, comparable_mt_offset) + setmetatable(comparable_struct6, comparable_mt_offset) + + -- bugfix history: + -- _ the non-table and comparable_struct tests below have been added, as I was exceptionally covering + -- the utest files themselves and saw that the metatables were not used at all; so I fixed are_same itself + -- to check __eq on the metatable instead of the table + + it('return true if both elements are not table, but equal', function () + assert.is_true(are_same(2, 2)) + end) + it('return false if both elements are not table, and not equal', function () + assert.is_false(are_same(2, 3)) + end) + + it('return true if both tables define __eq that returns true, and not comparing raw content', function () + assert.is_true(are_same(comparable_struct1, comparable_struct2)) + assert.is_true(are_same(comparable_struct1, comparable_struct3)) + assert.is_true(are_same(comparable_struct4, comparable_struct6)) + end) + it('return true if both tables define __eq that returns false, and not comparing raw content', function () + assert.is_false(are_same(comparable_struct4, comparable_struct5)) + end) + + it('return false if both tables define __eq that returns true, but comparing different raw content', function () + assert.is_false(are_same(comparable_struct1, comparable_struct3, true)) + assert.is_false(are_same(comparable_struct4, comparable_struct6, true)) + end) + + it('return true if both tables define __eq that returns false, but comparing same raw content', function () + assert.is_true(are_same(comparable_struct4, comparable_struct5, true)) + end) it('return true both tables are empty', function () assert.is_true(are_same({}, {})) From f2c97372da8ef069f354292ddecc89dc421c7d42 Mon Sep 17 00:00:00 2001 From: hsandt Date: Sat, 27 Apr 2019 03:20:46 +0100 Subject: [PATCH 129/140] [ENGINE] Added note for remaining game dependencies to isolate --- src/engine/tests/utestflow.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/engine/tests/utestflow.lua b/src/engine/tests/utestflow.lua index 271e5639..505064b6 100644 --- a/src/engine/tests/utestflow.lua +++ b/src/engine/tests/utestflow.lua @@ -1,6 +1,9 @@ require("engine/test/bustedhelper") local flow = require("engine/application/flow") local helper = require("engine/core/helper") +-- engine -> game reference needs cleanup +-- fortunately we are using nothing specific to those gamestates in the tests, +-- so we could simply replace them with mock gamestates that do nothing local titlemenu = require("game/menu/titlemenu") local credits = require("game/menu/credits") From 4327e9d30dcef9f4bed5a580fbd63f7faed901f5 Mon Sep 17 00:00:00 2001 From: hsandt Date: Wed, 1 May 2019 00:28:56 +0100 Subject: [PATCH 130/140] [LICENSE] Bumped to 2019 --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 732b1abb..4f9ba177 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2018 Long Nguyen Huu +Copyright (c) 2018-2019 Long Nguyen Huu Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 5026f51fc4c33853659982f724d40d4900429677 Mon Sep 17 00:00:00 2001 From: hsandt Date: Wed, 1 May 2019 00:36:14 +0100 Subject: [PATCH 131/140] [ANIM] Imported 11-sprite run cycle [STAGE] Reduced height so we don't see memory shared with spritesheet --- data/data.p8 | 64 +++++++++++++------------- src/game/data/playercharacter_data.lua | 19 ++++++-- src/game/data/stage_data.lua | 2 +- 3 files changed, 48 insertions(+), 37 deletions(-) diff --git a/data/data.p8 b/data/data.p8 index 9e0e708d..e216db58 100644 --- a/data/data.p8 +++ b/data/data.p8 @@ -67,22 +67,38 @@ eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeccccccee1eeeeeeeeccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeceeccccccceeeeeeececccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeccffccccceeeeeeeeccffcccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeecccfcc7ccceeeeeeecccfcc7ccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeecccccc770cceeeeeeccccccc70cceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeecee1cc770ceeeeeeeceecccc70ceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeccccf77f0eeeeeeeeccccf7ff0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeececccffeeeeeeeeeceecccffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeee1fccf7eeeeeeeeeecfcffe77eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeee777cf77eeeeeeee77eccff77eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeee7777c77eeeeeeee77ecccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeee77eceeeeeeeeeeeeccee7ceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeececeeeeeeeeee7ccee888eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeee7e7eeeeeeeee877eeee877eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeee0872088eeeeee88878eee88eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeee27888028eeeeee87888eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeccccccee1eeeeeeeccccceeeeeeeeeeeccccceeeeeeeeeeeccccceeeeeeeeeeeccccceeeeeeeeeeecccccceeceeeeeeecccccceeceeeeeeecccccceeceee +eeeceeccccccceeeeeececccccceeeeeeeececccccceeeeeeeececccccceeeeeeeececccccceeeeeeeeceeccccccceeeeeeceeccccccceeeeeeceeccccccceee +eeeeccffccccceeeeeeeccffcccceeeeeeeeccffcccceeeeeeeeccffcccceeeeeeeeccffcccceeeeeeeeccffccccceeeeeeeccffccccceeeeeeeccffccccceee +eeecccfcc7ccceeeeeecccfcc7ccceeeeeecccfcc7ccceeeeeecccfcc7ccceeeeeecccfcc7ccceeeeeecccfcc7ccceeeeeecccfcc7ccceeeeeecccfcc7ccceee +eecccccc770cceeeeeccccccc70cceeeeeccccccc70cceeeeeccccccc70cceeeeeccccccc70cceeeeecccccc770cceeeeecccccc770cceeeeecccccc771cceee +eecee1cc770ceeeeeeceecccc70ceeeeeeceecccc70ceeeeeeceecccc70ceeeeeeceecccc70ceeeeeeceeecc770ceeeeeeceeecc770ceeeeeeceeecc771ceeee +eeeeccccf77f0eeeeeeeccccf7ff0eeeeeeeccccf7ff0eeeeeeeccccf7ff0eeeeeeeccccf7ff0eeeeeeeccccf77f0eeeeeeeccccf77f0eeeeeeeffccf77f0eee +eeeececccffeeeeeeeeceecccffeeeeeeeeceecccffeeeeeeeeceecccffeeeeeeeecefcccffeeeeeeeeececccffeeeeeeee77ffcfffeeeeee77fecfccfeee77e +eeeeee1fccf7eeeeeeeeecfcffe77eeeeeeeecccffee77eeeeeeecccffeeeeeeeeeefcccfeeeeeeeeeeeee7fcfeeeeeeee7777ccfeee77ee7777eeccfffff77e +eeeeee777cf77eeeeeee77eccff77eeeeee77fcccfff77eeeeeeee7ccff77eeeeee77eecfeeeeeeeeeeee7777ff77eeeeee77ecccfcf77eee77eeeccfcceee88 +eeeeee7777c77eeeeeee77ecccceeeeeeee77eeccceeeeeeeeeeeeeccce77eeeeee77eecf77eeeeeeeeeee77ccc77eeeeeeeeecccc7ceeeeeeeeeeeccec78780 +eeeeeee77eceeeeeeeeeeeccee7ceeeeeeeeeecceccee88eee000eececceeeeeeeeeeeecc77eeeeeeeeeeee87ceeeeeeeeeee7cee788eeeeeeee7cceee77878e +eeeeeeeececeeeeeeeee7ccee888eeeeee87ccceeec7870ee07887cceecceeeeeee087ccceeeeeeeeeeeeec887eeeeeeeee0877ee8878eeeeee787eeeee880ee +eeeeeeee7e7eeeeeeee877eeee877eeee787eeeeee7880eee8787cceeee778eeee077eee7eeeeeeeeeeeeec087eeeeeeeee0888eee788eeeee087eeeeee00eee +eeeeeee0872088eeeee88878eee88eeee87eeeeeeee80eeeeeeeeeeeeee8878eee08eeee8788eeeeeeeee870788eeeeeeeee8778eee08eeeee888eeeeeeeeeee +eeeeeee27888028eeeee87888eeeeeeee88eeeeeeeeeeeeeeeeeeeeeeeee8788ee88eeee07088eeeeeeee8880888eeeeeeee0888eeeeeeeeee0888eeeeeeeeee +eeeecccccceeceeeeeeecccccceeceeeeeeecccccceeceeeeeeeccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeceeccccccceeeeeeceeccccccceeeeeeceeccccccceeeeeececccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeccffccccceeeeeeeccffccccceeeeeeeccffccccceeeeeeeccffcccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeecccfcc7ccceeeeeecccfcc7ccceeeeeecccfcc7ccceeeeeecccfcc7ccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eecccccc771cceeeeecccccc771cceeeeecccccc771cceeeeeccccccc70cceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeceeecc771ceeeeeeceeecc771ceeeeeeceeecc771ceeeeeeceecccc70ceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeccccf77f0eeeeeeeccccf77f0eeeeeeeccccf77f0eeeeeeeccccf7ff0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeecefccfeeeeeeeeeecefccfeeeeeeeeeecffccffeeeeeeeeceecccffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeefccffe77eeeeeeeefcccfeeeeeeeeeefcccfeeeeeeeeeeeeccffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeefecccff77eeeeeeee77ccf77eeeeeee77eecfeeeeeeeeeeee77cfe77eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eee77eeccceeeeeeeeeee77ccc77eeeeeee77eeccf77eeeeeeeee77ccf77eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eee77ecceccee88eee000eececceeeeeeeeeeeecce77eeeeeeeeeeececeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +ee80ccceeec7870ee07887cceecceeeeeee087ccceeeeeeeeeeeeeec07eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +e787eeeeee7880eee8787cceeee778eeee077eee7eeeeeeeeeeeee7c88eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +e87eeeeeeee80eeeeeeeeeeeeee8878eee08eeee8788eeeeeeeee08700eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +e88eeeeeeeeeeeeeeeeeeeeeeeee8788ee88eeee07088eeeeeeee078880eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeecccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee ecccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eccccc7cceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee @@ -115,22 +131,6 @@ eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee __gff__ 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001010101010001010100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 diff --git a/src/game/data/playercharacter_data.lua b/src/game/data/playercharacter_data.lua index c9879003..341d2071 100644 --- a/src/game/data/playercharacter_data.lua +++ b/src/game/data/playercharacter_data.lua @@ -72,9 +72,19 @@ local playercharacter_data = { -- stand right sonic_sprite_data_table = { - idle = sprite_data(sprite_id_location(0, 8), tile_vector(2, 2), vector(12, 8)), - run1 = sprite_data(sprite_id_location(2, 8), tile_vector(2, 2), vector(12, 8)), - spin = sprite_data(sprite_id_location(0, 10), tile_vector(2, 2), vector(5, 5)), + idle = sprite_data(sprite_id_location(0, 8), tile_vector(2, 2), vector(11, 8)), + run1 = sprite_data(sprite_id_location(2, 8), tile_vector(2, 2), vector(11, 8)), + run2 = sprite_data(sprite_id_location(4, 8), tile_vector(2, 2), vector(11, 8)), + run3 = sprite_data(sprite_id_location(6, 8), tile_vector(2, 2), vector(11, 8)), + run4 = sprite_data(sprite_id_location(8, 8), tile_vector(2, 2), vector(11, 8)), + run5 = sprite_data(sprite_id_location(10, 8), tile_vector(2, 2), vector(11, 8)), + run6 = sprite_data(sprite_id_location(12, 8), tile_vector(2, 2), vector(11, 8)), + run7 = sprite_data(sprite_id_location(14, 8), tile_vector(2, 2), vector(11, 8)), + run8 = sprite_data(sprite_id_location(0, 10), tile_vector(2, 2), vector(11, 8)), + run9 = sprite_data(sprite_id_location(2, 10), tile_vector(2, 2), vector(11, 8)), + run10 = sprite_data(sprite_id_location(4, 10), tile_vector(2, 2), vector(11, 8)), + run11 = sprite_data(sprite_id_location(6, 10), tile_vector(2, 2), vector(11, 8)), + spin = sprite_data(sprite_id_location(0, 12), tile_vector(2, 2), vector(5, 5)), }, } @@ -82,7 +92,8 @@ local playercharacter_data = { -- define animated sprite data in a second step, as it needs sprite data to be defined first playercharacter_data.sonic_animated_sprite_data_table = { idle = animated_sprite_data.create(playercharacter_data.sonic_sprite_data_table, {"idle"}, 10, true), - run = animated_sprite_data.create(playercharacter_data.sonic_sprite_data_table, {"idle", "run1"}, 10, true), + run = animated_sprite_data.create(playercharacter_data.sonic_sprite_data_table, + {"run1", "run2", "run3", "run4", "run5", "run6", "run7", "run8", "run9", "run10", "run11"}, 5, true), spin = animated_sprite_data.create(playercharacter_data.sonic_sprite_data_table, {"spin"}, 10, true), } diff --git a/src/game/data/stage_data.lua b/src/game/data/stage_data.lua index f53e5e64..c0e09915 100644 --- a/src/game/data/stage_data.lua +++ b/src/game/data/stage_data.lua @@ -20,7 +20,7 @@ return { title = "proto zone", width = 100, - height = 40, + height = 32, -- where the player character spawns on stage start spawn_location = location(2, 10), From 47771be90654467d9a3c5e5f74fd2b5bff3666c9 Mon Sep 17 00:00:00 2001 From: hsandt Date: Thu, 2 May 2019 00:52:15 +0100 Subject: [PATCH 132/140] [ANIM] Added playback speed --- src/engine/render/animated_sprite.lua | 31 +++-- src/engine/tests/utestanimated_sprite.lua | 139 ++++++++++++++++++++-- 2 files changed, 150 insertions(+), 20 deletions(-) diff --git a/src/engine/render/animated_sprite.lua b/src/engine/render/animated_sprite.lua index a96d6b68..ab763c31 100644 --- a/src/engine/render/animated_sprite.lua +++ b/src/engine/render/animated_sprite.lua @@ -5,12 +5,15 @@ animated_sprite = new_class() -- data_table {string: animated_sprite_data} table of animated sprite data, indexed by animation key (unique name) -- playing bool is the animation playing? false if the animation has reached the end and stopped +-- play_speed_frame float > 0 playback speed multiplier (in frames per update). it's a float so fractions of frames may be advanced every frame -- current_anim_key string|nil key in data_table of animation currently played / paused, or nil if no animation is set at all -- current_step int|nil index of the current sprite shown in the animation sequence, starting at 1, or nil if no animation is set at all --- local_frame int|nil current frame inside the current step, starting at 0, or nil if no animation is set at all +-- local_frame float|nil current frame inside the current step, starting at 0, or nil if no animation is set at all +-- since play_speed_frame is a float, local_frame is also a float to allow fractional advance function animated_sprite:_init(data_table) self.data_table = data_table self.playing = false + self.play_speed_frame = 0. self.current_anim_key = nil -- the sprite will be invisible until we start an animation self.current_step = nil self.local_frame = nil @@ -18,24 +21,29 @@ end --#if log function animated_sprite:_tostring() - return "animated_sprite("..joinstr(", ", nice_dump(self.data_table, true), self.playing, self.current_anim_key, self.current_step, self.local_frame)..")" + return "animated_sprite("..joinstr(", ", nice_dump(self.data_table, true), self.playing, self.play_speed_frame, self.current_anim_key, self.current_step, self.local_frame)..")" end --#endif --- play animation with given key: string +-- play animation with given key: string at playback speed: float (default: 1.) -- if this animation is not already set, play it from start -- if this animation is already set, check from_start: -- - if true, play it from start -- - if false, do nothing (if playing, it means continuing to play; if not playing (e.g. stopped at the end), do not replay from start) -- note that even if the animation is paused, it won't be resumed in this case (because we don't have a flag has_ended to distinguish pause and end) -- by default, continue animation already playing -function animated_sprite:play(anim_key, from_start) +function animated_sprite:play(anim_key, from_start, speed) assert(self.data_table[anim_key] ~= nil, "animated_sprite:play: self.data_table['"..anim_key.."'] doesn't exist") if from_start == nil then from_start = false end + speed = speed or 1. + + -- always update speed. this is useful to change anim speed while continue playing the same animation + self.play_speed = speed + if self.current_anim_key ~= anim_key or from_start then self.playing = true -- this will do nothing if forcing replay from start during play self.current_anim_key = anim_key -- this will do nothing if this animation is already set @@ -46,28 +54,31 @@ end -- update the sprite animation -- this must be called once per update at 60 fps, before the render phase +-- fractional playback speed is supported, but not negative playback function animated_sprite:update() if self.playing then local anim_spr_data = self.data_table[self.current_anim_key] + -- advance by playback speed + self.local_frame = self.local_frame + self.play_speed_frame -- check if we have reached the end of this step - if self.local_frame + 1 < anim_spr_data.step_frames then - -- keep same sprite and increment local frame counter - self.local_frame = self.local_frame + 1 - else + -- in case the playback speed is so high we will skip frames, + -- continue checking until time remainder is less than a step duration + while self.local_frame >= anim_spr_data.step_frames do -- end of step reached, check if there is another sprite afterward if self.current_step < #anim_spr_data.sprites then -- show next sprite and reset local frame counter self.current_step = self.current_step + 1 - self.local_frame = 0 + self.local_frame = self.local_frame - anim_spr_data.step_frames else -- end of last step reached, should we loop? if anim_spr_data.looping then -- continue playing from start self.current_step = 1 - self.local_frame = 0 + self.local_frame = self.local_frame - anim_spr_data.step_frames else -- stop playing self.playing = false + break end end end diff --git a/src/engine/tests/utestanimated_sprite.lua b/src/engine/tests/utestanimated_sprite.lua index 96d0e75f..0885993c 100644 --- a/src/engine/tests/utestanimated_sprite.lua +++ b/src/engine/tests/utestanimated_sprite.lua @@ -7,7 +7,7 @@ describe('animated_sprite', function () local spr_data1 = sprite_data(sprite_id_location(1, 0), tile_vector(1, 2), vector(4, 6)) local spr_data2 = sprite_data(sprite_id_location(2, 0), tile_vector(1, 2), vector(4, 6)) - local anim_spr_data = animated_sprite_data({spr_data1, spr_data2}, 10, true) + local anim_spr_data = animated_sprite_data({spr_data1, spr_data2, spr_data1}, 10, true) local anim_spr_data_no_loop = animated_sprite_data({spr_data1, spr_data2}, 10, false) local anim_spr_data_table = { loop = anim_spr_data, @@ -17,8 +17,8 @@ describe('animated_sprite', function () describe('_init', function () it('should init an animated sprite with data, automatically playing from step 1, frame 0', function () local anim_spr = animated_sprite(anim_spr_data_table) - assert.are_same({anim_spr_data_table, false, nil, nil, nil}, - {anim_spr.data_table, anim_spr.playing, anim_spr.current_anim_key, anim_spr.current_step, anim_spr.local_frame}) + assert.are_same({anim_spr_data_table, false, 0., nil, nil, nil}, + {anim_spr.data_table, anim_spr.playing, anim_spr.play_speed_frame, anim_spr.current_anim_key, anim_spr.current_step, anim_spr.local_frame}) end) end) @@ -27,10 +27,11 @@ describe('animated_sprite', function () it('should return a string describing data, current step and local frame', function () local anim_spr = animated_sprite(anim_spr_data_table) anim_spr.playing = true + anim_spr.play_speed_frame = 1.5 anim_spr.current_anim_key = "idle" anim_spr.current_step = 2 anim_spr.local_frame = 5 - assert.are_equal("animated_sprite({loop = animated_sprite_data([2 sprites], 10, true), no_loop = animated_sprite_data([2 sprites], 10, false)}, true, idle, 2, 5)", anim_spr:_tostring()) + assert.are_equal("animated_sprite({loop = animated_sprite_data([3 sprites], 10, true), no_loop = animated_sprite_data([2 sprites], 10, false)}, true, 1.5, idle, 2, 5)", anim_spr:_tostring()) end) end) @@ -92,6 +93,30 @@ describe('animated_sprite', function () {anim_spr.playing, anim_spr.current_anim_key, anim_spr.current_step, anim_spr.local_frame}) end) + it('set play speed to 1 by default', function () + local anim_spr = animated_sprite(anim_spr_data_table) + anim_spr.playing = false + anim_spr.current_anim_key = "no_loop" + anim_spr.current_step = 0 + anim_spr.local_frame = 0 + + anim_spr:play("no_loop", false) + + assert.are_equal(1, anim_spr.play_speed) + end) + + it('set play speed to any custom speed', function () + local anim_spr = animated_sprite(anim_spr_data_table) + anim_spr.playing = false + anim_spr.current_anim_key = "no_loop" + anim_spr.current_step = 0 + anim_spr.local_frame = 0 + + anim_spr:play("no_loop", true, 2.3) + + assert.are_equal(2.3, anim_spr.play_speed) + end) + end) describe('update', function () @@ -99,6 +124,7 @@ describe('animated_sprite', function () it('should do nothing when not playing', function () local anim_spr = animated_sprite(anim_spr_data_table) anim_spr.playing = false + anim_spr.play_speed_frame = 1 anim_spr.current_step = 9 anim_spr.local_frame = 99 @@ -108,9 +134,10 @@ describe('animated_sprite', function () {anim_spr.playing, anim_spr.current_step, anim_spr.local_frame}) end) - it('should increment the local frame if under the animation step_frames', function () + it('should increment the local frame if under the animation step_frames at playback speed 1', function () local anim_spr = animated_sprite(anim_spr_data_table) anim_spr.playing = true + anim_spr.play_speed_frame = 1 anim_spr.current_anim_key = "loop" anim_spr.current_step = 1 anim_spr.local_frame = 8 -- data.step_frames is 10, so frames play from 0 to 9 @@ -121,9 +148,24 @@ describe('animated_sprite', function () {anim_spr.current_step, anim_spr.local_frame}) end) - it('should reset local frame and enter next step when step_frames is reached', function () + it('should increase the local frame with playback speed if under the animation step_frames', function () + local anim_spr = animated_sprite(anim_spr_data_table) + anim_spr.playing = true + anim_spr.play_speed_frame = 1.5 + anim_spr.current_anim_key = "loop" + anim_spr.current_step = 1 + anim_spr.local_frame = 8.2 -- data.step_frames is 10, so frames play from 0 to 9 + + anim_spr:update() + + assert.are_same({1, 9.7}, + {anim_spr.current_step, anim_spr.local_frame}) + end) + + it('should reset local frame and enter next step when step_frames is reached at playback speed 1', function () local anim_spr = animated_sprite(anim_spr_data_table) anim_spr.playing = true + anim_spr.play_speed_frame = 1 anim_spr.current_anim_key = "loop" anim_spr.current_step = 1 anim_spr.local_frame = 9 -- data.step_frames - 1 @@ -134,11 +176,42 @@ describe('animated_sprite', function () {anim_spr.current_step, anim_spr.local_frame}) end) + it('should decrease the local frame by step_frames and enter next step when step_frames is reached when playback speed is not 1', function () + local anim_spr = animated_sprite(anim_spr_data_table) + anim_spr.playing = true + anim_spr.play_speed_frame = 2 + anim_spr.current_anim_key = "loop" + anim_spr.current_step = 1 + anim_spr.local_frame = 9 -- data.step_frames - 1 + + anim_spr:update() + + assert.are_same({2, 1}, + {anim_spr.current_step, anim_spr.local_frame}) + end) + + it('should decrease the local frame by 2*step_frames and advance by 2 steps when playback speed is enough to cover 2 step_frames (with initial fraction offset)', function () + local anim_spr = animated_sprite(anim_spr_data_table) + anim_spr.playing = true + anim_spr.play_speed_frame = 14.5 + anim_spr.current_anim_key = "loop" + anim_spr.current_step = 1 + anim_spr.local_frame = 8 + -- data.step_frames = 10, and we will reach 8 + 14.5 = 22.5, so 2 steps ahead and 2.5 remaining + -- this is testing the internal loop supporting high playback speeds with remainders in chain + + anim_spr:update() + + assert.are_same({3, 2.5}, + {anim_spr.current_step, anim_spr.local_frame}) + end) + it('should continue playing from the start when looping and end of animation has been reached', function () local anim_spr = animated_sprite(anim_spr_data_table) anim_spr.playing = true + anim_spr.play_speed_frame = 1 anim_spr.current_anim_key = "loop" - anim_spr.current_step = 2 + anim_spr.current_step = 3 anim_spr.local_frame = 9 -- data.step_frames - 1 anim_spr:update() @@ -147,16 +220,62 @@ describe('animated_sprite', function () {anim_spr.playing, anim_spr.current_step, anim_spr.local_frame}) end) - it('should stop playing when not looping and end of animation has been reached', function () + it('should continue playing from the start when looping and end of animation has been reached, with any remaining frame fraction', function () + local anim_spr = animated_sprite(anim_spr_data_table) + anim_spr.playing = true + anim_spr.play_speed_frame = 2.5 + anim_spr.current_anim_key = "loop" + anim_spr.current_step = 3 + anim_spr.local_frame = 9 -- data.step_frames - 1 + + anim_spr:update() + + assert.are_same({true, 1, 1.5}, + {anim_spr.playing, anim_spr.current_step, anim_spr.local_frame}) + end) + + it('should continue playing from the start when looping and end of animation has been reached with a high playback speed skipping 1 frame', function () local anim_spr = animated_sprite(anim_spr_data_table) - anim_spr.current_anim_key = "no_loop" anim_spr.playing = true + anim_spr.play_speed_frame = 17 + anim_spr.current_anim_key = "loop" anim_spr.current_step = 2 + anim_spr.local_frame = 5 + -- data.step_frames = 10, and we will reach 5 + 17 = 22, so 2 steps ahead and 2 remaining, but there are only 3 steps + -- so we go back to 1 + -- this is testing the internal loop supporting high playback speeds with remainders in chain + + anim_spr:update() + + assert.are_same({true, 1, 2}, + {anim_spr.playing, anim_spr.current_step, anim_spr.local_frame}) + end) + + it('should stop playing when not looping and end of animation has been reached, keeping local frame equal to step frames', function () + local anim_spr = animated_sprite(anim_spr_data_table) + anim_spr.playing = true + anim_spr.play_speed_frame = 1 + anim_spr.current_anim_key = "no_loop" + anim_spr.current_step = 3 + anim_spr.local_frame = 9 -- data.step_frames - 1 + + anim_spr:update() + + assert.are_same({false, 3, 10}, -- 10 doesn't exist, but ok for stopped anim + {anim_spr.playing, anim_spr.current_step, anim_spr.local_frame}) + end) + + it('should stop playing when not looping and end of animation has been reached, keeping even a local_frame beyond last frame', function () + local anim_spr = animated_sprite(anim_spr_data_table) + anim_spr.playing = true + anim_spr.play_speed_frame = 1.5 + anim_spr.current_anim_key = "no_loop" + anim_spr.current_step = 3 anim_spr.local_frame = 9 -- data.step_frames - 1 anim_spr:update() - assert.are_same({false, 2, 9}, + assert.are_same({false, 3, 10.5}, -- 10.5 doesn't exist, but ok for stopped anim {anim_spr.playing, anim_spr.current_step, anim_spr.local_frame}) end) From cb6a1099580db1c06092fc26f413951a0b5ba211 Mon Sep 17 00:00:00 2001 From: hsandt Date: Thu, 2 May 2019 00:57:57 +0100 Subject: [PATCH 133/140] [ANIM] Play player character run anim with ground speed playback (no multiplier yet) --- src/game/ingame/playercharacter.lua | 2 +- src/game/tests/utestplayercharacter.lua | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/game/ingame/playercharacter.lua b/src/game/ingame/playercharacter.lua index 9f62c8a5..596c2e60 100644 --- a/src/game/ingame/playercharacter.lua +++ b/src/game/ingame/playercharacter.lua @@ -411,7 +411,7 @@ function player_char:_update_platformer_motion_grounded() if self.ground_speed == 0 then self.anim_spr:play("idle") else - self.anim_spr:play("run") + self.anim_spr:play("run", false, abs(self.ground_speed)) end end diff --git a/src/game/tests/utestplayercharacter.lua b/src/game/tests/utestplayercharacter.lua index 4f40a3fb..f29a3e1a 100644 --- a/src/game/tests/utestplayercharacter.lua +++ b/src/game/tests/utestplayercharacter.lua @@ -1547,12 +1547,13 @@ describe('player_char', function () assert.spy(enter_motion_state_stub).was_not_called() end) - it('should play the run animation (ground speed ~= 0)', function () + it('#solo should play the run animation at playback speed = abs(ground speed), if not 0', function () + -- mock is setting ground speed to -2.5 pc:_update_platformer_motion_grounded() -- implementation assert.spy(animated_sprite.play).was_called(1) - assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "run") + assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "run", false, 2.5) end) end) From e948e42a2e47def03d5d3efeeb75f82e53c9176a Mon Sep 17 00:00:00 2001 From: hsandt Date: Thu, 2 May 2019 01:23:40 +0100 Subject: [PATCH 134/140] [ANIM] Fixed wrong member name for play_speed_frame --- src/engine/render/animated_sprite.lua | 2 +- src/engine/tests/utestanimated_sprite.lua | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/engine/render/animated_sprite.lua b/src/engine/render/animated_sprite.lua index ab763c31..5128f281 100644 --- a/src/engine/render/animated_sprite.lua +++ b/src/engine/render/animated_sprite.lua @@ -42,7 +42,7 @@ function animated_sprite:play(anim_key, from_start, speed) speed = speed or 1. -- always update speed. this is useful to change anim speed while continue playing the same animation - self.play_speed = speed + self.play_speed_frame = speed if self.current_anim_key ~= anim_key or from_start then self.playing = true -- this will do nothing if forcing replay from start during play diff --git a/src/engine/tests/utestanimated_sprite.lua b/src/engine/tests/utestanimated_sprite.lua index 0885993c..c3ebe733 100644 --- a/src/engine/tests/utestanimated_sprite.lua +++ b/src/engine/tests/utestanimated_sprite.lua @@ -102,7 +102,7 @@ describe('animated_sprite', function () anim_spr:play("no_loop", false) - assert.are_equal(1, anim_spr.play_speed) + assert.are_equal(1, anim_spr.play_speed_frame) end) it('set play speed to any custom speed', function () @@ -114,7 +114,7 @@ describe('animated_sprite', function () anim_spr:play("no_loop", true, 2.3) - assert.are_equal(2.3, anim_spr.play_speed) + assert.are_equal(2.3, anim_spr.play_speed_frame) end) end) From f93b0c871c3b7b0675e6acf3fb41c221b7093ade Mon Sep 17 00:00:00 2001 From: hsandt Date: Thu, 2 May 2019 01:24:24 +0100 Subject: [PATCH 135/140] [TEST] Added note on wrong coverage for filenames having other files with same name + suffix --- test.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test.sh b/test.sh index e5a2e57c..8f989326 100755 --- a/test.sh +++ b/test.sh @@ -48,8 +48,10 @@ if [[ $MODULE = "all" || -z $MODULE ]] ; then else # prepend "utest" again, and append ".lua" in case a module name contains another one # (e.g. logger for visual_logger or tile for tile_data) - TEST_FILE_PATTERN="utest$MODULE.lua" - COVERAGE_OPTIONS="-c .luacov_current \"/$MODULE\"" + TEST_FILE_PATTERN="utest${MODULE}.lua" + # FIXME + # "/"" makes sure the filename starts with MODULE, but "."" is interpreted as "any char" so _data would be included + COVERAGE_OPTIONS="-c .luacov_current \"/${MODULE}.lua\"" fi if [[ $TEST_FILTER_MODE = "all" ]] ; then From 0b44c8d7ace8ce304eb881b293bc75421c5d559c Mon Sep 17 00:00:00 2001 From: hsandt Date: Thu, 2 May 2019 01:27:31 +0100 Subject: [PATCH 136/140] [BUILD] Bumped to 2.3, added animated sprite and jump block features to README --- .travis.yml | 2 +- README.md | 6 +++--- sonic-2d-tech-demo.sublime-project | 8 ++++---- src/game/application/gameapp.lua | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index c2bc0fce..12af83f7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ os: - linux env: - - LUA="lua 5.3" BUILD_VERSION="2.2" + - LUA="lua 5.3" BUILD_VERSION="2.3" cache: apt: true # private repositories only diff --git a/README.md b/README.md index 43fbf368..d3a6bafb 100644 --- a/README.md +++ b/README.md @@ -12,17 +12,17 @@ A partial clone of classic Sonic the Hedgehog games made with PICO-8 ## Features -Version: 2.2 +Version: 2.3 ### Physics * Character runs on flat ground and slopes -* Character is blocked by walls when running +* Character is blocked by walls when running, walls and ceiling when jumping * Character jumps with variable height ### Rendering -* Character rendered with Idle and Spin sprite +* Character rendered with Idle and Spin static sprite, Run animated sprite * Environment rendered with tilemap ## Build diff --git a/sonic-2d-tech-demo.sublime-project b/sonic-2d-tech-demo.sublime-project index 0196424c..b821b306 100644 --- a/sonic-2d-tech-demo.sublime-project +++ b/sonic-2d-tech-demo.sublime-project @@ -9,7 +9,7 @@ "file_regex": "(.*\\.sh)(?:\\: line )([\\d]+)(?:\\:)([\\d]+)? (.*)", "syntax": "Packages/Python/Python.sublime-syntax", "env": { - "BUILD_VERSION": "2.2" + "BUILD_VERSION": "2.3" }, "variants": [ @@ -67,7 +67,7 @@ "file_regex": "(.*\\.sh)(?:\\: line )([\\d]+)(?:\\:)([\\d]+)? (.*)", "syntax": "Packages/Python/Python.sublime-syntax", "env": { - "BUILD_VERSION": "2.2" + "BUILD_VERSION": "2.3" }, "variants": [ @@ -123,7 +123,7 @@ "shell": true, "cmd": ["./run.sh $file_base_name debug"], "env": { - "BUILD_VERSION": "2.2" + "BUILD_VERSION": "2.3" }, "variants": [ @@ -175,7 +175,7 @@ "shell": true, "cmd": ["./install.sh release"], "env": { - "BUILD_VERSION": "2.2" + "BUILD_VERSION": "2.3" }, "variants": [ diff --git a/src/game/application/gameapp.lua b/src/game/application/gameapp.lua index ae09f5b8..a9f8c770 100644 --- a/src/game/application/gameapp.lua +++ b/src/game/application/gameapp.lua @@ -7,7 +7,7 @@ local visual = require("game/resources/visual") --#if log local logging = require("engine/debug/logging") -- pico8 doesn't support output file path containing "-" so use "_" -logging.file_log_stream.file_prefix = "sonic_pico8_v2.2" +logging.file_log_stream.file_prefix = "sonic_pico8_v2.3" --#endif --#if visual_logger From cb2a5557ef44e357e7ea00484b91397dc2e65922 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 3 May 2019 22:31:16 +0100 Subject: [PATCH 137/140] [ENGINE] Removed references to game in utestflow --- src/engine/tests/utestflow.lua | 154 ++++++++++++++++++--------------- 1 file changed, 85 insertions(+), 69 deletions(-) diff --git a/src/engine/tests/utestflow.lua b/src/engine/tests/utestflow.lua index 505064b6..7ceb2a19 100644 --- a/src/engine/tests/utestflow.lua +++ b/src/engine/tests/utestflow.lua @@ -1,14 +1,30 @@ require("engine/test/bustedhelper") local flow = require("engine/application/flow") local helper = require("engine/core/helper") --- engine -> game reference needs cleanup --- fortunately we are using nothing specific to those gamestates in the tests, --- so we could simply replace them with mock gamestates that do nothing -local titlemenu = require("game/menu/titlemenu") -local credits = require("game/menu/credits") describe('flow', function () + local mock_gamestate = new_class() + + function mock_gamestate:_init(type) + self.type = type + end + + function mock_gamestate:on_enter() + end + + function mock_gamestate:on_exit() + end + + function mock_gamestate:update() + end + + function mock_gamestate:render() + end + + local mock_gamestate1 = mock_gamestate("mock1") + local mock_gamestate2 = mock_gamestate("mock2") + describe('init', function () assert.are_same({{}, nil, nil}, {flow.gamestates, flow.curr_state, flow.next_state}) @@ -29,8 +45,8 @@ describe('flow', function () end) it('should add a gamestate', function () - flow:add_gamestate(titlemenu.state) - assert.are_equal(titlemenu.state, flow.gamestates[titlemenu.state.type]) + flow:add_gamestate(mock_gamestate1) + assert.are_equal(mock_gamestate1, flow.gamestates[mock_gamestate1.type]) end) it('should assert if a nil gamestate is passed', function () @@ -42,10 +58,10 @@ describe('flow', function () end) - describe('(titlemenu gamestate added)', function () + describe('(mock gamestate 1 added)', function () setup(function () - flow:add_gamestate(titlemenu.state) + flow:add_gamestate(mock_gamestate1) end) teardown(function () @@ -59,13 +75,13 @@ describe('flow', function () end) it('should query a new gamestate with the correct type', function () - flow:query_gamestate_type(titlemenu.state.type) - assert.are_equal(titlemenu.state.type, flow.next_state.type) + flow:query_gamestate_type(mock_gamestate1.type) + assert.are_equal(mock_gamestate1.type, flow.next_state.type) end) it('should query a new gamestate with the correct reference', function () - flow:query_gamestate_type(titlemenu.state.type) - assert.are_equal(flow.gamestates[titlemenu.state.type], flow.next_state) + flow:query_gamestate_type(mock_gamestate1.type) + assert.are_equal(flow.gamestates[mock_gamestate1.type], flow.next_state) end) it('should assert if a nil gamestate type is passed', function () @@ -75,10 +91,10 @@ describe('flow', function () "flow:query_gamestate_type: passed gamestate_type is nil") end) - describe('(titlemenu state entered)', function () + describe('(mock gamestate 1 entered)', function () before_each(function () - flow.curr_state = titlemenu.state + flow.curr_state = mock_gamestate1 end) after_each(function () @@ -87,9 +103,9 @@ describe('flow', function () it('should assert if the same gamestate type as the current one is passed', function () assert.has_error(function () - flow:query_gamestate_type(titlemenu.state.type) + flow:query_gamestate_type(mock_gamestate1.type) end, - "flow:query_gamestate_type: cannot query the current gamestate type 'titlemenu' itself") + "flow:query_gamestate_type: cannot query the current gamestate type 'mock1' itself") end) end) @@ -99,7 +115,7 @@ describe('flow', function () describe('query_gamestate_type', function () before_each(function () - flow:query_gamestate_type(titlemenu.state.type) + flow:query_gamestate_type(mock_gamestate1.type) end) after_each(function () @@ -113,16 +129,16 @@ describe('flow', function () end) after_each(function () - flow.curr_state:on_exit() -- just cleanup in case titlemenu on_enter had some side effects, since we didn't stub it + -- no need to on_exit current state, we know our mock states do nothing flow.curr_state = nil end) it('should enter a new gamestate with the correct type', function () - assert.are_equal(titlemenu.state.type, flow.curr_state.type) + assert.are_equal(mock_gamestate1.type, flow.curr_state.type) end) it('should enter a new gamestate with the correct reference', function () - assert.are_equal(flow.gamestates[titlemenu.state.type], flow.curr_state) + assert.are_equal(flow.gamestates[mock_gamestate1.type], flow.curr_state) end) it('should clear the next gamestate query', function () @@ -138,16 +154,16 @@ describe('flow', function () end) after_each(function () - flow.curr_state:on_exit() -- just cleanup in case titlemenu on_enter had some side effects, since we didn't stub it + -- no need to on_exit current state, we know our mock states do nothing flow.curr_state = nil end) it('via _check_next_state enter a new gamestate with the correct type', function () - assert.are_equal(titlemenu.state.type, flow.curr_state.type) + assert.are_equal(mock_gamestate1.type, flow.curr_state.type) end) it('via _check_next_state enter a new gamestate with correct reference', function () - assert.are_equal(flow.gamestates[titlemenu.state.type], flow.curr_state) + assert.are_equal(flow.gamestates[mock_gamestate1.type], flow.curr_state) end) it('via _check_next_state hence clear the next gamestate query', function () @@ -160,7 +176,7 @@ describe('flow', function () after_each(function () if flow.curr_state then - flow.curr_state:on_exit() -- just cleanup in case titlemenu on_enter had some side effects, since we didn't stub it + -- no need to on_exit current state, we know our mock states do nothing flow.curr_state = nil end end) @@ -173,13 +189,13 @@ describe('flow', function () end) it('should directly enter a gamestate', function () - flow:_change_state(titlemenu.state) - assert.are_equal(flow.gamestates[titlemenu.state.type], flow.curr_state) - assert.are_equal(titlemenu.state.type, flow.curr_state.type) + flow:_change_state(mock_gamestate1) + assert.are_equal(flow.gamestates[mock_gamestate1.type], flow.curr_state) + assert.are_equal(mock_gamestate1.type, flow.curr_state.type) end) it('should cleanup the now obsolete next gamestate query', function () - flow:_change_state(titlemenu.state) + flow:_change_state(mock_gamestate1) assert.is_nil(flow.next_state) end) @@ -198,7 +214,7 @@ describe('flow', function () after_each(function () if flow.curr_state then - flow.curr_state:on_exit() -- just cleanup in case titlemenu on_enter had some side effects, since we didn't stub it + -- no need to on_exit current state, we know our mock states do nothing flow.curr_state = nil end flow._change_state:clear() @@ -212,14 +228,14 @@ describe('flow', function () end) it('should directly enter a gamestate by type', function () - flow:change_gamestate_by_type(titlemenu.state.type) + flow:change_gamestate_by_type(mock_gamestate1.type) -- implementation assert.spy(flow._change_state).was_called(1) - assert.spy(flow._change_state).was_called_with(match.ref(flow), match.ref(titlemenu.state)) + assert.spy(flow._change_state).was_called_with(match.ref(flow), match.ref(mock_gamestate1)) -- interface - assert.are_equal(flow.gamestates[titlemenu.state.type], flow.curr_state) - assert.are_equal(titlemenu.state.type, flow.curr_state.type) + assert.are_equal(flow.gamestates[mock_gamestate1.type], flow.curr_state) + assert.are_equal(mock_gamestate1.type, flow.curr_state.type) end) end) @@ -227,89 +243,89 @@ describe('flow', function () end) describe('_change_state 1st time', function () - local titlemenu_on_enter_stub + local mock_gamestate1_on_enter_stub setup(function () - titlemenu_on_enter_stub = stub(titlemenu.state, "on_enter") + mock_gamestate1_on_enter_stub = stub(mock_gamestate1, "on_enter") end) teardown(function () - titlemenu_on_enter_stub:revert() + mock_gamestate1_on_enter_stub:revert() end) before_each(function () - flow:_change_state(titlemenu.state) + flow:_change_state(mock_gamestate1) end) after_each(function () flow.curr_state = nil - titlemenu_on_enter_stub:clear() + mock_gamestate1_on_enter_stub:clear() end) it('should directly enter a gamestate', function () - assert.are_equal(flow.gamestates[titlemenu.state.type], flow.curr_state) + assert.are_equal(flow.gamestates[mock_gamestate1.type], flow.curr_state) end) it('should call the gamestate:on_enter', function () - assert.spy(titlemenu_on_enter_stub).was_called(1) - assert.spy(titlemenu_on_enter_stub).was_called_with(match.ref(titlemenu.state)) + assert.spy(mock_gamestate1_on_enter_stub).was_called(1) + assert.spy(mock_gamestate1_on_enter_stub).was_called_with(match.ref(mock_gamestate1)) end) - describe('(credits gamestate added)', function () + describe('(mock gamestate 2 added)', function () setup(function () - flow:add_gamestate(credits.state) + flow:add_gamestate(mock_gamestate2) end) teardown(function () - flow.gamestates[credits.state.type] = nil + flow.gamestates[mock_gamestate2.type] = nil end) describe('_change_state 2nd time', function () - local titlemenu_on_exit_stub - local credits_on_enter_stub + local mock_gamestate1_on_exit_stub + local mock_gamestate2_on_enter_stub setup(function () - titlemenu_on_exit_stub = stub(titlemenu.state, "on_exit") - credits_on_enter_stub = stub(credits.state, "on_enter") + mock_gamestate1_on_exit_stub = stub(mock_gamestate1, "on_exit") + mock_gamestate2_on_enter_stub = stub(mock_gamestate2, "on_enter") end) teardown(function () - titlemenu_on_exit_stub:revert() - credits_on_enter_stub:revert() + mock_gamestate1_on_exit_stub:revert() + mock_gamestate2_on_enter_stub:revert() end) before_each(function () - flow:_change_state(credits.state) + flow:_change_state(mock_gamestate2) end) after_each(function () - flow.curr_state = titlemenu.state - titlemenu_on_exit_stub:clear() - credits_on_enter_stub:clear() + flow.curr_state = mock_gamestate1 + mock_gamestate1_on_exit_stub:clear() + mock_gamestate2_on_enter_stub:clear() end) it('should directly enter another gamestate', function () - assert.are_equal(flow.gamestates[credits.state.type], flow.curr_state) + assert.are_equal(flow.gamestates[mock_gamestate2.type], flow.curr_state) end) it('should call the old gamestate:on_exit', function () - assert.spy(titlemenu_on_exit_stub).was_called(1) - assert.spy(titlemenu_on_exit_stub).was_called_with(match.ref(titlemenu.state)) + assert.spy(mock_gamestate1_on_exit_stub).was_called(1) + assert.spy(mock_gamestate1_on_exit_stub).was_called_with(match.ref(mock_gamestate1)) end) it('should call the new gamestate:on_enter', function () - assert.spy(credits_on_enter_stub).was_called(1) - assert.spy(credits_on_enter_stub).was_called_with(match.ref(credits.state)) + assert.spy(mock_gamestate2_on_enter_stub).was_called(1) + assert.spy(mock_gamestate2_on_enter_stub).was_called_with(match.ref(mock_gamestate2)) end) end) - end) -- (credits gamestate added) + end) -- (mock_gamestate2 gamestate added) end) -- changed gamestate 1st time - end) -- (titlemenu gamestate added) + end) -- (mock_gamestate1 gamestate added) describe('render', function () @@ -319,30 +335,30 @@ describe('flow', function () describe('(when current gamestate is set)', function () - local titlemenu_render_stub + local mock_gamestate1_render_stub setup(function () - titlemenu_render_stub = stub(titlemenu.state, "render") + mock_gamestate1_render_stub = stub(mock_gamestate1, "render") end) teardown(function () - titlemenu_render_stub:revert() + mock_gamestate1_render_stub:revert() end) before_each(function () - flow:_change_state(titlemenu.state) + flow:_change_state(mock_gamestate1) end) after_each(function () flow.curr_state:on_exit() flow.curr_state = nil - titlemenu_render_stub:clear() + mock_gamestate1_render_stub:clear() end) it('should not delegate render to current gamestate', function () flow:render() - assert.spy(titlemenu_render_stub).was_called(1) - assert.spy(titlemenu_render_stub).was_called_with(match.ref(titlemenu.state)) + assert.spy(mock_gamestate1_render_stub).was_called(1) + assert.spy(mock_gamestate1_render_stub).was_called_with(match.ref(mock_gamestate1)) end) end) From 87ca841bdec59c7cf46e442f34044fe25c5f82d7 Mon Sep 17 00:00:00 2001 From: hsandt Date: Fri, 3 May 2019 22:35:05 +0100 Subject: [PATCH 138/140] [ENGINE] Reordered require to put engine at the top --- src/game/ingame/stage.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/game/ingame/stage.lua b/src/game/ingame/stage.lua index e482c9a6..22e27961 100644 --- a/src/game/ingame/stage.lua +++ b/src/game/ingame/stage.lua @@ -2,12 +2,12 @@ require("engine/core/class") require("engine/core/coroutine") require("engine/core/math") require("engine/render/color") -local player_char = require("game/ingame/playercharacter") -local gamestate = require("game/application/gamestate") local flow = require("engine/application/flow") -local audio = require("game/resources/audio") local ui = require("engine/ui/ui") +local player_char = require("game/ingame/playercharacter") +local gamestate = require("game/application/gamestate") local stage_data = require("game/data/stage_data") +local audio = require("game/resources/audio") local stage = { From e35be0fead4dee9063da32f5270ecc559fa5ed1d Mon Sep 17 00:00:00 2001 From: hsandt Date: Tue, 14 May 2019 00:14:16 +0100 Subject: [PATCH 139/140] [ENGINE] Separated last bits of mixed engine/game - integration test only refers to a generic gameapp - define a specific game app for pico-sonic - set/inject app/gamestate proxies on game side - initialize gameapp correctly from each main entry point --- README.md | 8 + src/engine/application/flow.lua | 7 + src/engine/application/gameapp.lua | 98 +++++ src/engine/test/integrationtest.lua | 55 ++- src/engine/tests/utestgameapp.lua | 270 ++++++++++++++ src/engine/tests/utestintegrationtest.lua | 336 +++++++++++------- .../{gameapp.lua => picosonic_app.lua} | 53 +-- src/game/itest_main.lua | 4 +- src/game/itests/itest_dsl.lua | 3 +- src/game/main.lua | 12 +- src/game/tests/utestgameapp.lua | 176 --------- src/game/tests/utestheadless_itests.lua | 13 +- src/game/tests/utestitest_dsl.lua | 53 ++- src/game/tests/utestpicosonic_app.lua | 181 ++++++++++ 14 files changed, 884 insertions(+), 385 deletions(-) create mode 100644 src/engine/application/gameapp.lua create mode 100644 src/engine/tests/utestgameapp.lua rename src/game/application/{gameapp.lua => picosonic_app.lua} (55%) delete mode 100644 src/game/tests/utestgameapp.lua create mode 100644 src/game/tests/utestpicosonic_app.lua diff --git a/README.md b/README.md index d3a6bafb..74a96072 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,14 @@ Alternatively, to edit the spritesheet in your favorite editor: 2. Edit it in your favorite editor 3. Import it back to PICO-8 with the PICO-8 command `import spritesheet.png` +## Development + +### Documentation + +Most of the documentation lies in code comment. + +`` means a duck-typed object that must implement functions named `fun1` and `fun2` + ## New project If you use the scripts of this project to create a new game, in order to use build command `p8tool: edit data` you need to create a pico8 file at data/data.p8 first. To do this, open PICO-8, type `save data`, then copy the boilerplate file to data/data.p8. diff --git a/src/engine/application/flow.lua b/src/engine/application/flow.lua index 74250e61..7d6e891e 100644 --- a/src/engine/application/flow.lua +++ b/src/engine/application/flow.lua @@ -3,6 +3,13 @@ require("engine/core/class") local logging = require("engine/debug/logging") --#endif +-- abstract gamestate singleton (no actual class, make your own as long as it has member/interface below) +-- type string gamestate type name +-- on_enter function() gamestate enter callback +-- on_exit function() gamestate exit callback +-- update function() gamestate update callback +-- render function() gamestate render callback + -- flow singleton -- state vars -- curr_state gamestates current gamestate diff --git a/src/engine/application/gameapp.lua b/src/engine/application/gameapp.lua new file mode 100644 index 00000000..e8169fb7 --- /dev/null +++ b/src/engine/application/gameapp.lua @@ -0,0 +1,98 @@ +local flow = require("engine/application/flow") +local class = require("engine/core/class") +local input = require("engine/input/input") + +-- main class for the game, taking care of the overall init, update, render +-- usage: derive from gameapp and override on_start, on_reset, on_update, on_render +-- in the main _init, set the initial_gamestate and call the app start() +-- in the main _update(60), call the app update() +-- in the main _draw, call the app render() +-- in integration tests, call the app reset() before starting a new itest +local gameapp = new_class() + +-- constructor: members are only config values for init_modules +-- managers {} sequence of managers to update and render in the loop +-- initial_gamestate string|nil key of the initial first gamestate to enter (nil if unset) +-- set it manually before calling start(), +-- and make sure you called register_gamestates with a matching state +function gameapp:_init() + self.managers = {} + self.initial_gamestate = nil +end + +-- register the managers you want to update and render +-- they may be managers provided by the engine like visual_logger and profiler, +-- or custom managers, as long as they provide the methods `update` and `render` +-- in this engine, we prefer injection to having a configuration with many flags +-- to enable/disable certain managers. +-- we can still override on_update/on_render for custom effects, but prefer handling managers when possible +function gameapp:register_managers(...) + for manager in all({...}) do + add(self.managers, manager) + end +end + +-- override to add gamestates to flow singleton +function gameapp:register_gamestates() + -- ex: + -- flow:add_gamestate(...) +end + +-- unlike _init, init_modules is called later, after finishing the configuration +-- in pico-8, it must be called in the global _init function +function gameapp:start() + self:register_gamestates() + + -- REFACTOR: consider making flow a very generic manager, that knows the initial gamestate + -- and is only added if you want + assert(self.initial_gamestate ~= nil, "gameapp:start: gameapp.initial_gamestate is not set") + flow:query_gamestate_type(self.initial_gamestate) + for manager in all(self.managers) do + manager:start() + end + self:on_start() +end + +-- override to initialize custom managers +function gameapp:on_start() -- virtual +end + +--#if itest +function gameapp:reset() + flow:init() + self:on_reset() +end + +-- override to call :init on your custom managers, or to reset anything set up in +-- in gameapp:start/on_start, really +function gameapp:on_reset() -- virtual +end +--#endif + +function gameapp:update() + input:process_players_inputs() + for manager in all(self.managers) do + manager:update() + end + flow:update() + self:on_update() +end + +-- override to add custom update behavior +function gameapp:on_update() -- virtual +end + +function gameapp:draw() + cls() + flow:render() + for manager in all(self.managers) do + manager:render() + end + self:on_render() +end + +-- override to add custom render behavior +function gameapp:on_render() -- virtual +end + +return gameapp diff --git a/src/engine/test/integrationtest.lua b/src/engine/test/integrationtest.lua index 8be9fe17..5d9e7e54 100644 --- a/src/engine/test/integrationtest.lua +++ b/src/engine/test/integrationtest.lua @@ -5,9 +5,6 @@ require("engine/test/assertions") --#if log local logging = require("engine/debug/logging") --#endif --- engine -> game reference is not good, consider using flow directly --- and isolating active gamestates somewhere else (e.g. a generic gameapp in engine) -local gameapp = require("game/application/gameapp") local input = require("engine/input/input") local mod = {} @@ -21,7 +18,7 @@ test_states = { } -- integration test manager: registers all itests --- itests {string: itest} registered itests, indexed by name +-- itests {string: itest} registered itests, indexed by name itest_manager = singleton(function (self) self.itests = {} end) @@ -118,7 +115,26 @@ function itest_manager:init_game_and_start_by_index(index) end -- integration test runner singleton --- test lifetime: +-- usage: +-- first, make sure you have registered itests via the itest_manager +-- and that you are running an itest via itest_manager:init_game_and_start_by_index (a proxy for itest_runner:init_game_and_start) +-- in _init, create a game app, set its initial_gamestate and set itest_runner.app to this app instance +-- in _update(60), call itest_runner:update_game_and_test +-- in _draw, call itest_runner:draw_game_and_test + +-- attributes +-- initialized bool true if it has already been initialized. +-- initialization is lazy and is only needed once +-- current_test integration_test current itest being run +-- current_frame int index of the current frame run +-- _last_trigger_frame int stored index of the frame where the last command trigger was received +-- _next_action_index int index of the next action to execute in the action list +-- current_state test_states stores if test has not started, is still running or has succeeded/failed +-- current_message string failure message, nil if test has not failed +-- app gameapp gameapp instance of the tested game +-- must be set directly with itest_runner.app = ... + +-- a test's lifetime follows the phases: -- none -> running -> success/failure/timeout (still alive, but not updated) -- -> stopped when a another test starts running itest_runner = singleton(function (self) @@ -128,32 +144,36 @@ itest_runner = singleton(function (self) self._last_trigger_frame = 0 self._next_action_index = 1 self.current_state = test_states.none - self.current_message = nil -- only defined when current_state is failure + self.current_message = nil + self.app = nil end) -- helper method to use in rendered itest _init function itest_runner:init_game_and_start(test) - -- if there was a previous test, gameapp was already initialized, - -- so reset it now (we could also just keep it and change the gamestate - -- to void, if we are sure that all the itests have the same required modules) + assert(self.app ~= nil, "itest_runner:init_game_and_start: self.app is not set") + + -- if there was a previous test, app was initialized too, so reset both now + -- (in reverse order of start) if self.current_test then - gameapp.reinit_modules() + self:stop() + self.app:reset() end - gameapp.init(test.active_gamestates) + self.app:start() itest_runner:start(test) end -- helper method to use in rendered itest _update60 function itest_runner:update_game_and_test() if self.current_state == test_states.running then - -- update gameapp, then test runner + + -- update app, then test runner -- updating test runner 2nd allows us to check the actual game state at final frame f, -- after everything has been computed -- time_trigger(0.) initial actions will still be applied before first frame -- thanks to the initial _check_next_action on start, but setup is still recommended log("frame #"..self.current_frame + 1, "trace") - gameapp.update() + self.app:update() self:update() if self.current_state ~= test_states.running then log("itest '"..self.current_test.name.."' ended with "..self.current_state, "itest") @@ -166,10 +186,11 @@ end -- helper method to use in rendered itest _draw function itest_runner:draw_game_and_test() - gameapp.draw() + self.app:draw() self:draw() end +-- start a test: integration_test function itest_runner:start(test) -- lazy initialization if not self.initialized then @@ -179,10 +200,6 @@ function itest_runner:start(test) -- log after _initialize which sets up the logger log("starting itest: "..test.name, "trace") - if self.current_test then - self:stop() - end - self.current_test = test self.current_state = test_states.running @@ -222,7 +239,7 @@ function itest_runner:draw() api.print(self.current_test.name, 2, 2, colors.yellow) api.print(self.current_state, 2, 9, self:_get_test_state_color(self.current_state)) else - api.print("no itest running", tuned("x", 8), tuned("y", 8), colors.white) + api.print("no itest running", 8, 8, colors.white) end end diff --git a/src/engine/tests/utestgameapp.lua b/src/engine/tests/utestgameapp.lua new file mode 100644 index 00000000..910a559b --- /dev/null +++ b/src/engine/tests/utestgameapp.lua @@ -0,0 +1,270 @@ +require("engine/test/bustedhelper") +local gameapp = require("engine/application/gameapp") + +local flow = require("engine/application/flow") +local input = require("engine/input/input") +local ui = require("engine/ui/ui") + +describe('gameapp', function () + + describe('init', function () + + it('should set empty managers sequence and nil initial gamestate', function () + local app = gameapp() + assert.are_same({{}, nil}, {app.managers, app.initial_gamestate}) + end) + + end) + + describe('(with default app)', function () + + local app + + local mock_manager1 = { + start = spy.new(function () end), + update = spy.new(function () end), + render = spy.new(function () end) + } + local mock_manager2 = { + start = spy.new(function () end), + update = spy.new(function () end), + render = spy.new(function () end) + } + + before_each(function () + app = gameapp() + end) + + describe('register_managers', function () + + it('should register each manager passed in variadic arg', function () + app:register_managers(mock_manager1, mock_manager2) + assert.are_same({mock_manager1, mock_manager2}, app.managers) + end) + + end) + + describe('(with mock_manager1 and mock_manager2 registered)', function () + + before_each(function () + app:register_managers(mock_manager1, mock_manager2) + end) + + describe('start', function () + + setup(function () + spy.on(gameapp, "register_gamestates") + spy.on(gameapp, "on_start") + stub(flow, "query_gamestate_type") + end) + + teardown(function () + gameapp.register_gamestates:revert() + gameapp.on_start:revert() + flow.query_gamestate_type:revert() + end) + + after_each(function () + gameapp.register_gamestates:clear() + gameapp.on_start:clear() + flow.query_gamestate_type:clear() + + mock_manager1.start:clear() + mock_manager2.start:clear() + end) + + it('should assert if initial_gamestate is not set', function () + assert.has_error(function () + app:start() + end, "gameapp:start: gameapp.initial_gamestate is not set") + end) + + describe('(initial gamestate set to "dummy")', function () + + before_each(function () + app.initial_gamestate = "dummy" + end) + + it('should call register_gamestates', function () + app:start() + + assert.spy(gameapp.register_gamestates).was_called(1) + assert.spy(gameapp.register_gamestates).was_called_with(match.ref(app)) + end) + + it('should call flow:query_gamestate_type with self.initial_gamestate', function () + app.initial_gamestate = "dummy_state" + + app:start() + + assert.spy(flow.query_gamestate_type).was_called(1) + assert.spy(flow.query_gamestate_type).was_called_with(match.ref(flow), "dummy_state") + end) + + it('should call start on each manager', function () + app:start() + + assert.spy(mock_manager1.start).was_called(1) + assert.spy(mock_manager1.start).was_called_with(match.ref(mock_manager1)) + assert.spy(mock_manager2.start).was_called(1) + assert.spy(mock_manager2.start).was_called_with(match.ref(mock_manager2)) + end) + + it('should call start on_start', function () + app:start() + + assert.spy(gameapp.on_start).was_called(1) + assert.spy(gameapp.on_start).was_called_with(match.ref(app)) + end) + + end) -- (initial gamestate set to "dummy") + + end) + + describe('reset', function () + + setup(function () + stub(flow, "init") + spy.on(gameapp, "on_reset") + end) + + teardown(function () + flow.init:revert() + gameapp.on_reset:revert() + end) + + after_each(function () + flow.init:clear() + gameapp.on_reset:clear() + end) + + it('should call flow:init', function () + app:reset() + + assert.spy(flow.init).was_called(1) + assert.spy(flow.init).was_called_with(match.ref(flow)) + end) + + it('should call on_reset', function () + app:reset() + + assert.spy(gameapp.on_reset).was_called(1) + assert.spy(gameapp.on_reset).was_called_with(match.ref(app)) + end) + + end) + + describe('update', function () + + setup(function () + stub(input, "process_players_inputs") + stub(flow, "update") + spy.on(gameapp, "on_update") + end) + + teardown(function () + input.process_players_inputs:revert() + flow.update:revert() + gameapp.on_update:revert() + end) + + after_each(function () + input.process_players_inputs:clear() + flow.update:clear() + gameapp.on_update:clear() + + mock_manager1.update:clear() + mock_manager2.update:clear() + end) + + it('should call input:process_players_inputs', function () + app:update() + + local s = assert.spy(input.process_players_inputs) + s.was_called(1) + s.was_called_with(match.ref(input)) + end) + + -- bugfix history: + -- + forget self. in front of managers + it('should update all registered managers', function () + app:update() + + local s1 = assert.spy(mock_manager1.update) + s1.was_called(1) + s1.was_called_with(match.ref(mock_manager1)) + local s2 = assert.spy(mock_manager2.update) + s2.was_called(1) + s2.was_called_with(match.ref(mock_manager2)) + end) + + it('should update the flow', function () + app:update() + + local s2 = assert.spy(flow.update) + s2.was_called(1) + s2.was_called_with(match.ref(flow)) + end) + + it('should call on_update', function () + app:update() + + local s2 = assert.spy(app.on_update) + s2.was_called(1) + s2.was_called_with(match.ref(app)) + end) + + end) + + describe('draw', function () + + setup(function () + stub(_G, "cls") + stub(flow, "render") + end) + + teardown(function () + cls:revert() + flow.render:revert() + end) + + after_each(function () + cls:clear() + flow.render:clear() + + mock_manager1.render:clear() + mock_manager2.render:clear() + end) + + it('should clear screen', function () + app:draw() + assert.spy(cls).was_called(1) + end) + + it('should call flow:render', function () + app:draw() + local s = assert.spy(flow.render) + s.was_called(1) + s.was_called_with(match.ref(flow)) + end) + + -- bugfix history: + -- + forget self. in front of managers + it('should render all registered managers', function () + app:draw() + + local s1 = assert.spy(mock_manager1.render) + s1.was_called(1) + s1.was_called_with(match.ref(mock_manager1)) + local s2 = assert.spy(mock_manager2.render) + s2.was_called(1) + s2.was_called_with(match.ref(mock_manager2)) + end) + + end) + + end) -- (with mock_manager1 and mock_manager2 registered) + + end) -- (with default app) + +end) diff --git a/src/engine/tests/utestintegrationtest.lua b/src/engine/tests/utestintegrationtest.lua index 4f0e7216..2a7cf4f7 100644 --- a/src/engine/tests/utestintegrationtest.lua +++ b/src/engine/tests/utestintegrationtest.lua @@ -1,10 +1,10 @@ require("engine/test/bustedhelper") require("engine/core/helper") require("engine/render/color") +local gameapp = require("engine/application/gameapp") local integrationtest = require("engine/test/integrationtest") local itest_manager, integration_test, time_trigger = integrationtest.itest_manager, integrationtest.integration_test, integrationtest.time_trigger local logging = require("engine/debug/logging") -local gameapp = require("game/application/gameapp") local input = require("engine/input/input") local function repeat_callback(time, callback) @@ -24,6 +24,14 @@ describe('itest_manager', function () itest_manager:init() end) + describe('init', function () + + it('should create a singleton instance with empty itests', function () + assert.are_same({}, itest_manager.itests) + end) + + end) + describe('register_itest', function () it('should register a new test', function () @@ -126,6 +134,9 @@ end) describe('itest_runner', function () + -- prepare mock app with default implementation + local mock_app = gameapp() + local test before_each(function () @@ -139,183 +150,260 @@ describe('itest_runner', function () logging.logger:init() end) + describe('init', function () + + it('should initialize parameters', function () + assert.are_same({ + false, + nil, + 0, + 0, + 1, + test_states.none, + nil, + nil + }, + { + itest_runner.initialized, + itest_runner.current_test, + itest_runner.current_frame, + itest_runner._last_trigger_frame, + itest_runner._next_action_index, + itest_runner.current_state, + itest_runner.current_message, + itest_runner.gameapp + }) + end) + + end) + describe('init_game_and_start', function () setup(function () - gameapp_init_stub = stub(gameapp, "init") - gameapp_reinit_modules_stub = stub(gameapp, "reinit_modules") - itest_runner_start_stub = stub(itest_runner, "start") + stub(gameapp, "reset") + stub(gameapp, "start") + stub(itest_runner, "stop") + stub(itest_runner, "start") end) teardown(function () - gameapp_init_stub:revert() - gameapp_reinit_modules_stub:revert() - itest_runner_start_stub:revert() + gameapp.reset:revert() + gameapp.start:revert() + itest_runner.stop:revert() + itest_runner.start:revert() end) after_each(function () - gameapp_init_stub:clear() - gameapp_reinit_modules_stub:clear() - itest_runner_start_stub:clear() - end) - - it('should init the gameapp and the passed test', function () - itest_runner:init_game_and_start(test) - assert.spy(gameapp_init_stub).was_called(1) - assert.spy(gameapp_init_stub).was_called_with({'stage'}) - assert.spy(itest_runner_start_stub).was_called(1) - assert.spy(itest_runner_start_stub).was_called_with(match.ref(itest_runner), test) + gameapp.reset:clear() + gameapp.start:clear() + itest_runner.stop:clear() + itest_runner.start:clear() end) - describe('(when another test was running)', function () - - it('should reinit the gameapp modules first', function () - itest_runner.current_test = integration_test('previous test', {}) + it('should error if app is not set', function () + assert.has_error(function () itest_runner:init_game_and_start(test) + end, "itest_runner:init_game_and_start: self.app is not set") + end) - assert.spy(gameapp_reinit_modules_stub).was_called(1) - assert.spy(gameapp_reinit_modules_stub).was_called_with() + describe('(with mock app)', function () + + before_each(function () + itest_runner.app = mock_app end) - end) + describe('(when current_test is already set)', function () - end) - - describe('update_game_and_test', function () + before_each(function () + itest_runner.current_test = test + end) - setup(function () - gameapp_update_stub = stub(gameapp, "update") - spy.on(itest_runner, "update") - end) + it('should reset the app', function () + itest_runner:init_game_and_start(test) - teardown(function () - gameapp_update_stub:revert() - itest_runner.update:revert() - end) + local s = assert.spy(gameapp.reset) + s.was_called(1) + s.was_called_with(match.ref(mock_app)) + end) - after_each(function () - gameapp_update_stub:clear() - itest_runner.update:clear() - end) + it('should stop', function () + itest_runner:init_game_and_start(test) - describe('(when state is not running)', function () + local s = assert.spy(itest_runner.stop) + s.was_called(1) + s.was_called_with(match.ref(itest_runner)) + end) - it('should do nothing', function () - itest_runner:update_game_and_test() - assert.spy(gameapp_update_stub).was_not_called() - assert.spy(itest_runner.update).was_not_called() end) - end) - - describe('(when state is running for some actions)', function () + it('should start the gameapp', function () + itest_runner:init_game_and_start(test) - before_each(function () - test:add_action(time_trigger(1.0), function () end, 'some_action') - itest_runner:start(test) + local s = assert.spy(gameapp.start) + s.was_called(1) + s.was_called_with(match.ref(mock_app)) end) - it('should update the gameapp and the passed test', function () - itest_runner:update_game_and_test() - assert.spy(gameapp_update_stub).was_called(1) - assert.spy(gameapp_update_stub).was_called_with() - assert.spy(itest_runner.update).was_called(1) - assert.spy(itest_runner.update).was_called_with(match.ref(itest_runner)) + it('should init a set gameapp and the passed test', function () + itest_runner:init_game_and_start(test) + + local s = assert.spy(itest_runner.start) + s.was_called(1) + s.was_called_with(match.ref(itest_runner), test) end) end) - describe('(when test ends on this update with success)', function () + end) - local log_stub + describe('(with mock app)', function () - before_each(function () - test:add_action(time_trigger(0.017), function () end, 'some_action') - itest_runner:start(test) - end) + before_each(function () + itest_runner.app = mock_app + end) + + describe('update_game_and_test', function () setup(function () - log_stub = stub(_G, "log") + stub(gameapp, "update") + spy.on(itest_runner, "update") end) teardown(function () - log_stub:revert() + gameapp.update:revert() + itest_runner.update:revert() end) after_each(function () - log_stub:clear() + gameapp.update:clear() + itest_runner.update:clear() end) - it('should only log the result', function () - itest_runner:update_game_and_test() - assert.spy(log_stub).was_called() -- we only want 1 call, but we check "at least once" because there are other unrelated logs - assert.spy(log_stub).was_called_with("itest 'character walks' ended with success", "itest") + describe('(when state is not running)', function () + + it('should do nothing', function () + itest_runner:update_game_and_test() + assert.spy(gameapp.update).was_not_called() + assert.spy(itest_runner.update).was_not_called() + end) + end) - end) + describe('(when state is running for some actions)', function () + + before_each(function () + test:add_action(time_trigger(1.0), function () end, 'some_action') + end) - describe('(when test ends on this update with failure)', function () + it('should update the set gameapp and the passed test', function () + itest_runner:start(test) - local log_stub + itest_runner:update_game_and_test() - before_each(function () - test:add_action(time_trigger(0.017), function () end, 'some_action') - test.final_assertion = function () - return false, "character walks failed" - end - itest_runner:start(test) - end) + local s_app = assert.spy(gameapp.update) + s_app.was_called(1) + s_app.was_called_with(match.ref(mock_app)) + local s_runner = assert.spy(itest_runner.update) + s_runner.was_called(1) + s_runner.was_called_with(match.ref(itest_runner)) + end) - setup(function () - log_stub = stub(_G, "log") end) - teardown(function () - log_stub:revert() - end) + describe('(when running, and test ends on this update with success)', function () + + before_each(function () + test:add_action(time_trigger(0.017), function () end, 'some_action') + itest_runner:start(test) + end) + + setup(function () + stub(_G, "log") + end) + + teardown(function () + log:revert() + end) + + after_each(function () + log:clear() + end) + + it('should only log the result', function () + itest_runner:update_game_and_test() + local s = assert.spy(log) + s.was_called() -- we only want 1 call, but we check "at least once" because there are other unrelated logs + s.was_called_with("itest 'character walks' ended with success", "itest") + end) - after_each(function () - log_stub:clear() end) - it('should log the result and failure message', function () - itest_runner:update_game_and_test() - assert.spy(log_stub).was_called() -- we only want 2 calls, but we check "at least twice" because there are other unrelated logs - assert.spy(log_stub).was_called_with("itest 'character walks' ended with failure", "itest") - assert.spy(log_stub).was_called_with("failed: character walks failed", "itest") + describe('(when running, and test ends on this update with failure)', function () + + before_each(function () + test:add_action(time_trigger(0.017), function () end, 'some_action') + test.final_assertion = function () + return false, "character walks failed" + end + itest_runner:start(test) + end) + + setup(function () + stub(_G, "log") + end) + + teardown(function () + log:revert() + end) + + after_each(function () + log:clear() + end) + + it('should log the result and failure message', function () + itest_runner:update_game_and_test() + local s = assert.spy(log) + s.was_called() -- we only want 2 calls, but we check "at least twice" because there are other unrelated logs + s.was_called_with("itest 'character walks' ended with failure", "itest") + s.was_called_with("failed: character walks failed", "itest") + end) + end) end) - end) + describe('draw_game_and_test', function () - describe('draw_game_and_test', function () + setup(function () + stub(gameapp, "draw") + stub(itest_runner, "draw") + end) - setup(function () - gameapp_draw_stub = stub(gameapp, "draw") - itest_runner_draw_stub = stub(itest_runner, "draw") - end) + teardown(function () + gameapp.draw:revert() + itest_runner.draw:revert() + end) - teardown(function () - gameapp_draw_stub:revert() - itest_runner_draw_stub:revert() - end) + after_each(function () + gameapp.draw:clear() + itest_runner.draw:clear() + end) - after_each(function () - gameapp_draw_stub:clear() - itest_runner_draw_stub:clear() - end) + it('should draw the gameapp and the passed test information', function () + itest_runner:draw_game_and_test() + + local s_app = assert.spy(gameapp.draw) + s_app.was_called(1) + s_app.was_called_with(match.ref(mock_app)) + local s_runner = assert.spy(itest_runner.draw) + s_runner.was_called(1) + s_runner.was_called_with(match.ref(itest_runner)) + end) - it('should draw the gameapp and the passed test information', function () - itest_runner:draw_game_and_test() - assert.spy(gameapp_draw_stub).was_called(1) - assert.spy(gameapp_draw_stub).was_called_with() - assert.spy(itest_runner_draw_stub).was_called(1) - assert.spy(itest_runner_draw_stub).was_called_with(match.ref(itest_runner)) end) - end) + end) -- (with mock app) describe('start', function () @@ -417,16 +505,6 @@ describe('itest_runner', function () end) end) - it('should automatically stop before restarting, effectively resetting state vars but the current test and state', function () - itest_runner:start(test) - assert.are_same({0, 0, 1, test_states.running}, { - itest_runner.current_frame, - itest_runner._last_trigger_frame, - itest_runner._next_action_index, - itest_runner.current_state - }) - end) - it('should not call _initialize the second time', function () -- in this specific case, start was called in before_each so we need to clear manually -- just before we call start ourselves to have the correct count @@ -568,20 +646,22 @@ describe('itest_runner', function () describe('(stubbing api.print)', function () setup(function () - api_print_stub = stub(api, "print") + stub(api, "print") end) teardown(function () - api_print_stub:revert() + api.print:revert() end) after_each(function () - api_print_stub:clear() + api.print:clear() end) it('should draw "no itest running"', function () itest_runner:draw() - assert.spy(api_print_stub).was_called(1) + local s = assert.spy(api.print) + s.was_called(1) + s.was_called_with("no itest running", 8, 8, colors.white) end) describe('(when current test is set)', function () @@ -593,7 +673,7 @@ describe('itest_runner', function () it('should draw information on the current test', function () itest_runner:draw() - assert.spy(api_print_stub).was_called(2) + assert.spy(api.print).was_called(2) end) end) diff --git a/src/game/application/gameapp.lua b/src/game/application/picosonic_app.lua similarity index 55% rename from src/game/application/gameapp.lua rename to src/game/application/picosonic_app.lua index a9f8c770..a9f16a54 100644 --- a/src/game/application/gameapp.lua +++ b/src/game/application/picosonic_app.lua @@ -1,7 +1,7 @@ +local gameapp = require("engine/application/gameapp") + local flow = require("engine/application/flow") -local input = require("engine/input/input") local gamestate_proxy = require("game/application/gamestate_proxy") -local gamestate = require("game/application/gamestate") local visual = require("game/resources/visual") --#if log @@ -26,51 +26,29 @@ local profiler = require("engine/debug/profiler") local ui = require("engine/ui/ui") --#endif -local gameapp = {} - --- todo: consider making gameapp a singleton with init like the other modules, --- so we can easily reinit it (implementation would b more a reset than the init --- below, as it would reinit the flow, etc.) - --- in pico8 builds, pass nothing for active_gamestates --- in busted tests, pass active_gamestates so they can be required automatically on gameapp init -function gameapp.init(active_gamestates) ---#ifn pico8 - assert(active_gamestates, "gameapp.init: non-pico8 build requires active_gamestates to define them at runtime") ---#endif - ---#if mouse - ui:set_cursor_sprite_data(visual.sprite_data_t.cursor) ---#endif - ---#ifn pico8 - gamestate_proxy:require_gamestates(active_gamestates) ---#endif +local picosonic_app = derived_class(gameapp) +function picosonic_app:register_gamestates() -- override for state in all({"titlemenu", "credits", "stage"}) do flow:add_gamestate(gamestate_proxy:get(state)) end - flow:query_gamestate_type(gamestate.types.titlemenu) end ---#ifn utest -function gameapp.reinit_modules() +function picosonic_app.on_start() -- override --#if mouse - ui:set_cursor_sprite_data(nil) + ui:set_cursor_sprite_data(visual.sprite_data_t.cursor) --#endif +end ---#ifn pico8 - gamestate_proxy:init() +--#if itest +function picosonic_app.on_reset() -- override +--#if mouse + ui:set_cursor_sprite_data(nil) --#endif - - flow:init() end --#endif -function gameapp.update() - input:process_players_inputs() - flow:update() - +function picosonic_app.on_update() -- override --#if visual_logger vlogger.window:update() --#endif @@ -84,10 +62,7 @@ function gameapp.update() --#endif end -function gameapp.draw() - cls() - flow:render() - +function picosonic_app.on_render() --#if visual_logger vlogger.window:render() --#endif @@ -105,4 +80,4 @@ function gameapp.draw() --#endif end -return gameapp +return picosonic_app diff --git a/src/game/itest_main.lua b/src/game/itest_main.lua index a4d72a66..d0357e79 100644 --- a/src/game/itest_main.lua +++ b/src/game/itest_main.lua @@ -4,7 +4,7 @@ -- to use for the build. other states will be replaced with dummy equivalents. require("engine/test/integrationtest") -local input = require("engine/input/input") +local picosonic_app = require("game/application/picosonic_app") require("game/itests/itest$itest") local gamestate_proxy = require("game/application/gamestate_proxy") @@ -24,6 +24,8 @@ function _init() logging.file_log_stream:clear() --#endif + itest_runner.app = picosonic_app() + -- require only gamestate modules written on first line of the required $itest (pico8-build way) gamestate_proxy:require_gamestates() diff --git a/src/game/itests/itest_dsl.lua b/src/game/itests/itest_dsl.lua index 01462aa0..b30d37d5 100644 --- a/src/game/itests/itest_dsl.lua +++ b/src/game/itests/itest_dsl.lua @@ -545,7 +545,8 @@ function itest_dsl_parser.create_itest(name, dsli) end itest_dsl_parser._itest.teardown = function () - flow:change_gamestate_by_type(dsli.gamestate_type) + -- clear map + -- no need to "unload" the game state, the next test will reset the flow anyway if dsli.gamestate_type == "stage" then if dsli.stage_name == '#' then -- clear tilemap and unload tilemap data diff --git a/src/game/main.lua b/src/game/main.lua index 192be9fc..881b66e3 100644 --- a/src/game/main.lua +++ b/src/game/main.lua @@ -1,5 +1,6 @@ -local gameapp = require("game/application/gameapp") +local picosonic_app = require("game/application/picosonic_app") local gamestate_proxy = require("game/application/gamestate_proxy") +local gamestate = require("game/application/gamestate") --#if log local logging = require("engine/debug/logging") @@ -34,6 +35,8 @@ input:toggle_mouse(true) -- pico-8 functions must be placed at the end to be parsed by p8tool +local app = picosonic_app() + function _init() --#if log -- clear log file on new game session @@ -42,13 +45,14 @@ function _init() -- require all gamestate modules, according to preprocessing step gamestate_proxy:require_gamestates() - gameapp.init() + app.initial_gamestate = gamestate.types.titlemenu + app:start() end function _update60() - gameapp.update() + app:update() end function _draw() - gameapp.draw() + app:draw() end diff --git a/src/game/tests/utestgameapp.lua b/src/game/tests/utestgameapp.lua deleted file mode 100644 index c87f7cf5..00000000 --- a/src/game/tests/utestgameapp.lua +++ /dev/null @@ -1,176 +0,0 @@ -require("engine/test/bustedhelper") -local gameapp = require("game/application/gameapp") - -local flow = require("engine/application/flow") -local input = require("engine/input/input") -local titlemenu = require("game/menu/titlemenu_dummy") -local credits = require("game/menu/credits_dummy") -local stage = require("game/ingame/stage") -local ui = require("engine/ui/ui") -local gamestate_proxy = require("game/application/gamestate_proxy") - -describe('gameapp', function () - - describe('init', function () - - setup(function () - ui_set_cursor_sprite_data_stub = stub(ui, "set_cursor_sprite_data") - spy.on(gamestate_proxy, "require_gamestates") - end) - - teardown(function () - ui_set_cursor_sprite_data_stub:revert() - gamestate_proxy.require_gamestates:revert() - end) - - after_each(function () - gameapp.reinit_modules() - ui_set_cursor_sprite_data_stub:clear() - gamestate_proxy.require_gamestates:clear() - end) - - it('should assert if active_gamestates is nil (for non-pico8 build)', function () - assert.has_error(function () - gameapp.init() - end) - end) - - it('should set the ui cursor sprite data', function () - local visual = require("game/resources/visual") - gameapp.init({}) - assert.spy(ui_set_cursor_sprite_data_stub).was_called(1) - assert.spy(ui_set_cursor_sprite_data_stub).was_called_with(match.ref(ui), match.ref(visual.sprite_data_t.cursor)) - end) - - it('should require active gamestates via gamestate proxy', function () - gameapp.init({'stage'}) - - -- implementation - assert.spy(gamestate_proxy.require_gamestates).was_called(1) - assert.spy(gamestate_proxy.require_gamestates).was_called_with(match.ref(gamestate_proxy), {'stage'}) - end) - - it('should add all gamestates', function () - gameapp.init({'stage'}) -- needed because we require the stage at the top, not a dummy - - -- interface - assert.are_equal(titlemenu.state, flow.gamestates[titlemenu.state.type]) - assert.are_equal(credits.state, flow.gamestates[credits.state.type]) - assert.are_equal(stage.state, flow.gamestates[stage.state.type]) - end) - - it('should query titlemenu as initial state', function () - gameapp.init({}) - assert.are_equal(titlemenu.state, flow.next_state) - end) - - end) - - describe('renit_modules (#utest only)', function () - - setup(function () - ui_set_cursor_sprite_data_stub = stub(ui, "set_cursor_sprite_data") - gamestate_proxy_init = stub(gamestate_proxy, "init") - flow_init_stub = stub(flow, "init") - end) - - teardown(function () - ui_set_cursor_sprite_data_stub:revert() - gamestate_proxy_init:revert() - flow_init_stub:revert() - end) - - after_each(function () - ui_set_cursor_sprite_data_stub:clear() - gamestate_proxy_init:clear() - flow_init_stub:clear() - end) - - it('should reset the ui cursor sprite data', function () - gameapp.reinit_modules() - assert.spy(ui_set_cursor_sprite_data_stub).was_called(1) - assert.spy(ui_set_cursor_sprite_data_stub).was_called_with(match.ref(ui), nil) - end) - - it('should reinit gamestate_proxy', function () - gameapp.reinit_modules() - assert.spy(gamestate_proxy_init).was_called(1) - assert.spy(gamestate_proxy_init).was_called_with(match.ref(gamestate_proxy)) - end) - - it('should reinit flow', function () - gameapp.reinit_modules() - assert.spy(flow_init_stub).was_called(1) - assert.spy(flow_init_stub).was_called_with(match.ref(flow)) - end) - - end) - - describe('update', function () - - local process_players_inputs_stub - local flow_update_stub - - setup(function () - gameapp.init({}) - process_players_inputs_stub = stub(input, "process_players_inputs") - flow_update_stub = stub(flow, "update") - end) - - teardown(function () - process_players_inputs_stub:revert() - flow_update_stub:revert() - end) - - after_each(function () - process_players_inputs_stub:clear() - flow_update_stub:clear() - end) - - it('should update the input', function () - gameapp.update() - assert.spy(process_players_inputs_stub).was_called(1) - assert.spy(process_players_inputs_stub).was_called_with(match.ref(input)) - end) - - it('should update the flow', function () - gameapp.update() - assert.spy(flow_update_stub).was_called(1) - assert.spy(flow_update_stub).was_called_with(match.ref(flow)) - end) - - end) - - describe('draw', function () - - local cls_stub - local flow_render_stub - - setup(function () - gameapp.init({}) - gameapp.update() - cls_stub = stub(_G, "cls") - flow_render_stub = stub(flow, "render") - end) - - teardown(function () - cls_stub:revert() - flow_render_stub:revert() - end) - - after_each(function () - cls_stub:clear() - flow_render_stub:clear() - end) - - it('should clear screen and delegate rendering to flow', function () - gameapp.draw() - assert.spy(flow_render_stub).was_called(1) - assert.spy(flow_render_stub).was_called_with(match.ref(flow)) - assert.spy(cls_stub).was_called(1) - end) - - end) - - -end) diff --git a/src/game/tests/utestheadless_itests.lua b/src/game/tests/utestheadless_itests.lua index 1fccad1d..0bf44a61 100644 --- a/src/game/tests/utestheadless_itests.lua +++ b/src/game/tests/utestheadless_itests.lua @@ -1,6 +1,6 @@ require("engine/test/bustedhelper") require("engine/test/integrationtest") -local gameapp = require("game/application/gameapp") +local picosonic_app = require("game/application/picosonic_app") local gamestate_proxy = require("game/application/gamestate_proxy") -- check options @@ -31,8 +31,11 @@ end describe('headless itest', function () - after_each(function () - gameapp.reinit_modules() + local app = picosonic_app() + + setup(function () + app.initial_gamestate = "titlemenu" + itest_runner.app = app end) -- define a headless unit test for each registered itest so far @@ -41,6 +44,10 @@ describe('headless itest', function () local itest = itest_manager.itests[i] it(itest.name..' should succeed', function () + -- just require the gamestates you need for this itest + -- (in practice, any gamestate module required at least once by an itest will be loaded + -- anyway; this will just redirect untested gamestates to a dummy to avoid useless processing) + gamestate_proxy:require_gamestates(itest.active_gamestates) itest_manager:init_game_and_start_by_index(i) while itest_runner.current_state == test_states.running do diff --git a/src/game/tests/utestitest_dsl.lua b/src/game/tests/utestitest_dsl.lua index 7901275f..64b590ca 100644 --- a/src/game/tests/utestitest_dsl.lua +++ b/src/game/tests/utestitest_dsl.lua @@ -15,8 +15,8 @@ local itest_manager, time_trigger, integration_test = get_members(integratio "itest_manager", "time_trigger", "integration_test") local flow = require("engine/application/flow") local input = require("engine/input/input") -local gameapp = require("game/application/gameapp") local gamestate = require("game/application/gamestate") +local gamestate_proxy = require("game/application/gamestate_proxy") local stage = require("game/ingame/stage") local tilemap = require("engine/data/tilemap") local player_char = require("game/ingame/playercharacter") @@ -836,8 +836,14 @@ expect -- note that most actions depend on the previous one, so we exceptionally -- assert multiple times in chain in a single utest - -- simulate the itest runner behavior by initializing gameapp to inject active gamestates - gameapp.init(test.active_gamestates) + -- simulate the itest runner behavior by initializing active gamestates for this test + gamestate_proxy:require_gamestates(test.active_gamestates) + + -- simulate picosonic_app:register_gamestates once gamestates have been registered, + -- so gamestates are available when starting an itest (we could also use the picosonic_app directly) + for state in all(test.active_gamestates) do + flow:add_gamestate(gamestate_proxy:get(state)) + end -- verify setup callback behavior test.setup() @@ -881,11 +887,18 @@ expect describe('(spying tilemap load)', function () setup(function () - spy.on(tilemap, "load") + stub(tilemap, "load") + stub(tilemap, "clear_map") end) teardown(function () tilemap.load:revert() + tilemap.clear_map:revert() + end) + + after_each(function () + tilemap.load:clear() + tilemap.clear_map:clear() end) it('setup should call setup_map_data and load on the tilemap if custom stage definition', function () @@ -897,17 +910,27 @@ expect local test = itest_dsl_parser.create_itest("test 1", dsli) - gameapp.init(test.active_gamestates) + -- simulate the itest runner behavior by initializing active gamestates for this test + gamestate_proxy:require_gamestates(test.active_gamestates) + + -- simulate picosonic_app:register_gamestates once gamestates have been registered, + -- so gamestates are available when starting an itest (we could also use the picosonic_app directly) + for state in all(test.active_gamestates) do + flow:add_gamestate(gamestate_proxy:get(state)) + end + test.setup() -- interface assert.are_equal(control_modes.puppet, stage.state.player_char.control_mode) -- implementation - assert.spy(setup_map_data).was_called(1) - assert.spy(setup_map_data).was_called_with() - assert.spy(tilemap.load).was_called(1) - assert.spy(tilemap.load).was_called_with(match.ref(dsli.tilemap)) + local s_data = assert.spy(setup_map_data) + s_data.was_called(1) + s_data.was_called_with() + local s_load = assert.spy(tilemap.load) + s_load.was_called(1) + s_load.was_called_with(match.ref(dsli.tilemap)) end) it('teardown should call clear_map and teardown_map_data if custom stage definition', function () @@ -919,14 +942,16 @@ expect local test = itest_dsl_parser.create_itest("test 1", dsli) - gameapp.init(test.active_gamestates) + gamestate_proxy:require_gamestates(test.active_gamestates) test.teardown() -- implementation - assert.spy(teardown_map_data).was_called(1) - assert.spy(teardown_map_data).was_called_with() - assert.spy(tilemap.load).was_called(1) - assert.spy(tilemap.load).was_called_with(match.ref(dsli.tilemap)) + local s_clear = assert.spy(tilemap.clear_map) + s_clear.was_called(1) + s_clear.was_called_with() + local s_teardown = assert.spy(teardown_map_data) + s_teardown.was_called(1) + s_teardown.was_called_with() end) end) diff --git a/src/game/tests/utestpicosonic_app.lua b/src/game/tests/utestpicosonic_app.lua new file mode 100644 index 00000000..8ba88801 --- /dev/null +++ b/src/game/tests/utestpicosonic_app.lua @@ -0,0 +1,181 @@ +require("engine/test/bustedhelper") +local picosonic_app = require("game/application/picosonic_app") + +local flow = require("engine/application/flow") +local codetuner = require("engine/debug/codetuner") +local profiler = require("engine/debug/profiler") +local vlogger = require("engine/debug/visual_logger") +local ui = require("engine/ui/ui") +local gamestate_proxy = require("game/application/gamestate_proxy") +local titlemenu = require("game/menu/titlemenu_dummy") +local credits = require("game/menu/credits_dummy") +local stage = require("game/ingame/stage") +local visual = require("game/resources/visual") + +describe('picosonic_app', function () + + local app + + before_each(function () + app = picosonic_app() + end) + + describe('register_gamestates', function () + + it('should add all gamestates', function () + -- require the real stage (as we required "stage" not "stage_dummy" at the top + -- but leave the other states as dummy + gamestate_proxy:require_gamestates({"stage"}) + picosonic_app:register_gamestates() + + -- interface + assert.are_equal(titlemenu.state, flow.gamestates[titlemenu.state.type]) + assert.are_equal(credits.state, flow.gamestates[credits.state.type]) + assert.are_equal(stage.state, flow.gamestates[stage.state.type]) + end) + + end) + + describe('on_start', function () + + setup(function () + stub(ui, "set_cursor_sprite_data") + end) + + teardown(function () + ui.set_cursor_sprite_data:revert() + end) + + after_each(function () + ui.set_cursor_sprite_data:clear() + end) + + it('should set the ui cursor sprite data', function () + app.on_start() + local s = assert.spy(ui.set_cursor_sprite_data) + s.was_called(1) + s.was_called_with(match.ref(ui), match.ref(visual.sprite_data_t.cursor)) + end) + + end) + + describe('on_reset (#utest only)', function () + + setup(function () + stub(ui, "set_cursor_sprite_data") + end) + + teardown(function () + ui.set_cursor_sprite_data:revert() + end) + + after_each(function () + ui.set_cursor_sprite_data:clear() + end) + + it('should reset the ui cursor sprite data', function () + picosonic_app:on_reset() + local s = assert.spy(ui.set_cursor_sprite_data) + s.was_called(1) + s.was_called_with(match.ref(ui), nil) + end) + + end) + + describe('on_update', function () + + setup(function () + stub(vlogger.window, "update") + stub(profiler.window, "update") + stub(codetuner, "update_window") + end) + + teardown(function () + vlogger.window.update:revert() + profiler.window.update:revert() + codetuner.update_window:revert() + end) + + after_each(function () + vlogger.window.update:clear() + profiler.window.update:clear() + codetuner.update_window:clear() + end) + + it('should update the vlogger window', function () + picosonic_app:on_update() + local s = assert.spy(vlogger.window.update) + s.was_called(1) + s.was_called_with(match.ref(vlogger.window)) + end) + + it('should update the profiler window', function () + picosonic_app:on_update() + local s = assert.spy(profiler.window.update) + s.was_called(1) + s.was_called_with(match.ref(profiler.window)) + end) + + it('should update the codetuner window', function () + picosonic_app:on_update() + local s = assert.spy(codetuner.update_window) + s.was_called(1) + s.was_called_with(match.ref(codetuner)) + end) + + end) + + describe('on_render', function () + + setup(function () + stub(vlogger.window, "render") + stub(profiler.window, "render") + stub(codetuner, "render_window") + stub(ui, "render_mouse") + end) + + teardown(function () + vlogger.window.render:revert() + profiler.window.render:revert() + codetuner.render_window:revert() + ui.render_mouse:revert() + end) + + after_each(function () + vlogger.window.render:clear() + profiler.window.render:clear() + codetuner.render_window:clear() + ui.render_mouse:clear() + end) + + it('should render the vlogger window', function () + picosonic_app:on_render() + local s = assert.spy(vlogger.window.render) + s.was_called(1) + s.was_called_with(match.ref(vlogger.window)) + end) + + it('should render the profiler window', function () + picosonic_app:on_render() + local s = assert.spy(profiler.window.render) + s.was_called(1) + s.was_called_with(match.ref(profiler.window)) + end) + + it('should render the codetuner window', function () + picosonic_app:on_render() + local s = assert.spy(codetuner.render_window) + s.was_called(1) + s.was_called_with(match.ref(codetuner)) + end) + + it('should render the mouse', function () + picosonic_app:on_render() + local s = assert.spy(ui.render_mouse) + s.was_called(1) + s.was_called_with(match.ref(ui)) + end) + + end) + +end) From 83d96af3a5d23d6c9d3b177c326ea68496401cac Mon Sep 17 00:00:00 2001 From: hsandt Date: Tue, 14 May 2019 00:37:52 +0100 Subject: [PATCH 140/140] [BUILD] Preprocess: support blank before --#if/pico8 and closing equivalents --- prebuild/preprocess.py | 10 +++---- prebuild/test_preprocess.py | 58 +++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/prebuild/preprocess.py b/prebuild/preprocess.py index bee86901..58fa8451 100644 --- a/prebuild/preprocess.py +++ b/prebuild/preprocess.py @@ -74,13 +74,13 @@ class ParsingMode(Enum): # tag to enter a pico8-only block (it's a comment block so that busted never runs it but preprocess reactivates it) # unlike normal comment blocks, we expect to match from the line start -pico8_start_pattern = re.compile(r"--\[=*\[#pico8") +pico8_start_pattern = re.compile(r"\s*--\[=*\[#pico8") # closing tag for pico8-only block. Unlike normal comment blocks, we expect to match from the line start and we ignore anything after the block end! -pico8_end_pattern = re.compile(r"--#pico8]=*]") +pico8_end_pattern = re.compile(r"\s*--#pico8]=*]") -if_pattern = re.compile(r"--#if (\w+)") # ! ignore anything after 1st symbol -ifn_pattern = re.compile(r"--#ifn (\w+)") # ! ignore anything after 1st symbol -endif_pattern = re.compile(r"--#endif") +if_pattern = re.compile(r"\s*--#if (\w+)") # ! ignore anything after 1st symbol +ifn_pattern = re.compile(r"\s*--#ifn (\w+)") # ! ignore anything after 1st symbol +endif_pattern = re.compile(r"\s*--#endif") stripped_function_call_patterns_table = {} for config, stripped_functions in stripped_functions_table.items(): # if there is nothing to strip, avoid creating a regex with just "(?:)\(\)" that would match a line starting with brackets diff --git a/prebuild/test_preprocess.py b/prebuild/test_preprocess.py index ed390829..b80b7b93 100644 --- a/prebuild/test_preprocess.py +++ b/prebuild/test_preprocess.py @@ -293,6 +293,30 @@ def test_preprocess_lines_missing_endif_ignored(self): # this will also trigger a warning, but we don't test it self.assertEqual(preprocess.preprocess_lines(test_lines, 'release'), expected_processed_lines) + def test_preprocess_lines_if_after_blank_acknowledged(self): + test_lines = [ + ' --#if log\n', + ' print("debug")\n', + ' --#endif\n', + ] + expected_processed_lines = [ + ' print("debug")\n', + ] + self.assertEqual(preprocess.preprocess_lines(test_lines, 'debug'), expected_processed_lines) + + def test_preprocess_lines_if_after_non_blank_preserved(self): + test_lines = [ + 'text before --#if log\n', + 'print("debug")\n', + 'text before --#endif\n', + ] + expected_processed_lines = [ + 'text before --#if log\n', + 'print("debug")\n', + 'text before --#endif\n', + ] + self.assertEqual(preprocess.preprocess_lines(test_lines, 'release'), expected_processed_lines) + def test_preprocess_lines_pico8_block(self): test_lines = [ 'print("start")\n', @@ -407,6 +431,40 @@ def test_preprocess_lines_missing_end_pico8_ignored(self): # this will also trigger a warning, but we don't test it self.assertEqual(preprocess.preprocess_lines(test_lines, 'release'), expected_processed_lines) + def test_preprocess_lines_pico8_after_blank_acknowledged(self): + test_lines = [ + 'print("start")\n', + ' --[[#pico8 pico8 start\n', + 'real pico8 code\n', + ' --#pico8]] exceptionally ignored\n', + 'print("end")\n', + ] + expected_processed_lines = [ + 'print("start")\n', + 'real pico8 code\n', + 'print("end")\n', + ] + # this will also trigger a warning, but we don't test it + self.assertEqual(preprocess.preprocess_lines(test_lines, 'release'), expected_processed_lines) + + def test_preprocess_lines_pico8_after_non_blank_preserved(self): + test_lines = [ + 'print("start")\n', + 'text --[[#pico8 pico8 start\n', + 'real pico8 code\n', + 'text --#pico8]] exceptionally ignored\n', + 'print("end")\n', + ] + expected_processed_lines = [ + 'print("start")\n', + 'text --[[#pico8 pico8 start\n', + 'real pico8 code\n', + 'text --#pico8]] exceptionally ignored\n', + 'print("end")\n', + ] + # this will also trigger a warning, but we don't test it + self.assertEqual(preprocess.preprocess_lines(test_lines, 'release'), expected_processed_lines) + def test_preprocess_lines_with_unknown_config(self): test_lines = [] self.assertRaises(ValueError, preprocess.preprocess_lines, test_lines, 'unknown')