diff --git a/.gitmodules b/.gitmodules index 4db433d2..04f46fbb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "src/engine/wtk"] - path = src/engine/wtk - url = https://github.com/hsandt/PICO8-WTK.git +[submodule "pico-boots"] + path = pico-boots + url = ../pico-boots.git diff --git a/.travis.yml b/.travis.yml index 12af83f7..cc7b709e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,42 +1,40 @@ -# https://github.com/mpeterv/hererocks +os: linux language: python # Can use any language here, but if it's not 'python' # it becomes necessary to pass '--user' to pip when installing hererocks. -python: - - "3.6" - -sudo: false # Use container-based infrastructure. - -os: - - linux +python: "3.6" env: - - LUA="lua 5.3" BUILD_VERSION="2.3" + - LUA_VERSION=5.3 ENABLE_RENDER=1 cache: - apt: true # private repositories only directories: - - lua5.3 # for hererocks - - tool # remember to "travis cache --delete" from travis CL if p8tool needs upgrade - - $HOME/.cache + # remember to "travis cache --delete" in travis CL each time a custom cached directory needs upgrade (unlike cached packages) + - hererocks # for hererocks (make a parent folder, then install lua inside, to make sure the folder will be used as cache) + # just a small optimization to cache the whole lua install, since the hererocks pip wheel is cached with .cache/pip anyway + - tool # for picotool + - $HOME/.cache # for .cache/luarocks and .cache/pip before_install: # install hererocks if needed - | - if [[ ! -d lua5.3 || ! -x lua5.3/bin/activate ]]; then + if [[ ! -d lua$LUA_VERSION || ! -x lua$LUA_VERSION/bin/activate ]]; then pip install hererocks - hererocks lua5.3 --$LUA -rlatest + pushd hererocks + hererocks lua$LUA_VERSION --lua $LUA_VERSION -rlatest + popd fi - - source lua5.3/bin/activate # Add directory with all installed binaries to PATH. + + # activate Lua + - source hererocks/lua$LUA_VERSION/bin/activate install: # install lua packages for testing - luarocks install busted - luarocks install luacov - # install npm packages for minification - - pushd npm - - npm update - - popd + + # delegate submodule install (for luamin from npm) + - pico-boots/setup.sh before_script: # download picotool if needed @@ -54,18 +52,20 @@ before_script: - ln -s "$(pwd)/tool/picotool-master/p8tool" "$HOME/.local/bin/p8tool" script: - # test - - ./test.sh all all + # test (including rendered headless itests thanks to ENABLE_RENDER=1) + - ./test.sh -m all # coverage - bash <(curl -s https://codecov.io/bash) - # build for deploy - - ./build.sh main release + # build game and itest to make sure everything works fine + - ./build_game.sh debug + - ./build_game.sh release + - ./build_itest.sh deploy: provider: releases - api_key: $DEPLOY_API_KEY - file: "build/sonic-pico8_v${BUILD_VERSION}_release.p8" - skip_cleanup: true + api_key: + secure: bfXQQ0AXGHgXiq0xOxhYQ2AXX/flQnxJh/+eA/HUGfwdoPDq0QTdqFA/3jEMWkJSsFKEBVKDjJGCt24QPxUIjTu91r1wyCNdL2KlNfnogRjWAVutRZxB/OC2HWR3kJtPjkFQBCsOXHBxGI3hMJL7LWr5WfNsSGMbcRMfvphxFT3ER8XBHAUEJY6roITm6noHroqQt8Uye+0+rkGqJ8QslKRqq8qBZMZeOiOrh6SBdlhsGw0KqNno/dMXQxx2ZCrh/VUeWjNvxzXe/mZjfBPbhvyecN7jz+FytEdAhdt1Dy37hhyOAkDfxLGGsH1YAAfinH8uFwoSRo0MH8fuhdXpT7jUXuAgP9/RS0FEiZDdX+J/FdncCbnoDfE9B4Dt3L3srISeiNwxKK5sx2kzyWvftK30pV1+zEgnbVEKGPIIeGb5wYWSCmzHf+CfLMk+bzeznTrpo/irY/vjoRBefNaVWXqLygrNWxM1uIMJae+OA3MYeUSYd1lpCyRw98i3GC7si68M9OaDeLoDjnqOLqvhurB/RmLzCU7mCYipn2kxykAOdevWN73cyx9VhdFy2GPE5VDw6EO6ZQP04KaeYxP2pgR4ts2kYWpVvf1PGg+2yN4QMkVhrWV+6dG2jtUO0BrCqt5Tpw0I3C3aFmBjjzFBBuKsZpr2yUG3roxnu1Dhww0= + file: build/picosonic_v3.0_release.p8 on: - branch: master - tags: true + tags: 'true' + skip_cleanup: 'true' diff --git a/README.md b/README.md index 74a96072..24a8b4ec 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,17 @@ develop [![Build Status](https://travis-ci.org/hsandt/sonic-pico8.svg?branch=develop)](https://travis-ci.org/hsandt/sonic-pico8) [![codecov](https://codecov.io/gh/hsandt/sonic-pico8/branch/develop/graph/badge.svg)](https://codecov.io/gh/hsandt/sonic-pico8) -# Sonic PICO-8 +# PICO-Sonic -A partial clone of classic Sonic the Hedgehog games made with PICO-8 +A partial clone of classic Sonic the Hedgehog games made with PICO-8. It is inspired by the 16-bit games for mechanics, and by a mix of the 8-bit and 16-bit games for graphics and audio. + +This is a fan game distributed for free and is not endorsed by Sega Games Co., Ltd, which owns the Sonic the Hedgehog trademark and retains all copyrights on the original assets. + +It is currently under development. ## Features -Version: 2.3 +Version: 3.0 ### Physics @@ -25,34 +29,25 @@ Version: 2.3 * Character rendered with Idle and Spin static sprite, Run animated sprite * Environment rendered with tilemap -## Build - -### Supported platforms - -Only Linux Ubuntu (and supposedly the Debian family) is fully supported to build the game from sources. Other Linux distributions and UNIX platforms should be able to run most scripts, providing the right tools are installed, but a few references like `gnome-terminal` in `run.sh` would require adaptation. +## Releases -Development environments for Windows such as MinGW and Cygwin have not been tested. +You can directly download a released version of the game on the [releases](Releases) page, and run it with PICO-8 as you would normally with a downloaded cartridge. -### Build dependencies - -#### Python 3.6 - -Prebuild and postbuild scripts are written in Python 3 and use 3.6 features such as formatted strings. +## Build -#### picotool +Follow this if you want to build the game yourself. -A build pipeline for PICO-8 ([GitHub](https://github.com/dansanderson/picotool)) +Soon, you will be able to choose between building the debug or the release version. -#### luamin +### Supported platforms -A Lua minifier ([luamin](https://github.com/mathiasbynens/luamin)) +Mostly UNIX, and specifically Linux for some scripts. -You don't need to install it globally, instead you can: +See Supported platforms in [pico-boots](https://github.com/hsandt/pico-boots) README for more information. -* `cd npm` -* `npm update` +### Build dependencies -It will install `luamin` (along with `luaparse`), which is used in `npm/luamin_file`, itself used inside `build.sh`. `luamin_file` is just a stripped down version of the `luamin` script, which only takes a file path argument, and behaves as if the input always came from a TTY, which avoids stalling while building from a non-terminal environment such as a Sublime Text build system (it's a hack). +See *Build dependencies* in [pico-boots](https://github.com/hsandt/pico-boots) README. ### Build and run @@ -61,22 +56,35 @@ 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` +* `./build_game.sh` +* `./run_game_debug.sh` Instead of the last instruction, you can also enter directly: - -* `pico8 -run build/sonic-pico8_v${BUILD_VERSION}_release.p8` +* `pico8 -run build/picosonic_v${BUILD_VERSION}_debug.p8` where BUILD_VERSION is set in `sonic-2d-tech-demo.sublime-project` as well as `.travis.yml`. -`sonic-2d-tech-demo.sublime-project` contains the most used commands for building the game. If you don't use Sublime Text, you won't be able to run the commands directly, but you can use them directly in a terminal, or copy-paste them to the project configuration of your favorite code editor. +Note, however, that the current debug version is bloated and is likely not to run in a vanilla PICO-8 (due to the cartridge getting over the max token limit). + +To play the release version (no debugging features, but more compact code and more likely to fit into a PICO-8 cartridge): + +* `cd path/to/sonic-pico8-repo` +* `./build_game.sh` release +* `./run_game_release.sh` + +### Run integration tests -All the build and run commands are usage variants around the script `build.sh` and `run.sh`. `build.sh` relies on picotool as well as custom shell and Python scripts in the `prebuild` and `postbuild` folders. +Integration tests consists in game simulations in predetermined scenarios, and are therefore run directly in PICO-8. To build the integration test cartridge and run it: -`build.sh` can take a build configuration as 2nd argument. The complete list of configurations is listed in `sonic-2d-tech-demo.sublime-project` and `prebuild/preprocess.py`. The config of biggest output size is `build` (generally it has too many tokens to even fit in a cartridge), while the config of smallest output size is `release`, and is used to release the final game cartridge. +* `cd path/to/pico-boots-demo` +* `./build_itest.sh` +* `./run_itest.sh` -Finally, `build.sh` takes a file base name as 1st argument. To build the game, passing `main` is enough, but to build rendered integration tests, you need to pass the name of the test, such as `itestplayercharacter`. +### Custom build + +`sonic-2d-tech-demo.sublime-project` contains the most used commands for building the game. If you don't use Sublime Text, you won't be able to run the commands directly, but you can still read this project file to understand how the scripts are used, and do the same in a terminal. You can also copy-paste the commands to the project configuration of your favorite code editor instead. + +All the build and run commands revolve around the scripts `build_game.sh` / `build_itest.sh` and `run_game.sh` / `run_itest.sh`. Once you understand them, you can create your own build and run commands for your specific needs. ## Test @@ -88,34 +96,24 @@ Finally, `build.sh` takes a file base name as 1st argument. To build the game, p ### Test dependencies -#### Lua 5.3 - -Tests run under Lua 5.3, although Lua 5.2 should also have the needed features (in particular the bit32 module). - -#### busted - -A Lua unit test framework ([GitHub](https://github.com/Olivine-Labs/busted)) - -The test script (`test.sh`) only works on Linux (it uses gnome-terminal). +See *Test dependencies* in [pico-boots](https://github.com/hsandt/pico-boots) README. ### Run unit tests and headless integration tests -To test the modules: - -* `cd path/to/sonic-pico8-repo` -* `./test.sh all` or `./test.sh all all` if you want to include `#mute` tests (longer) +To run all the (non-#mute) unit tests: -This will run all the unit tests, as well as headless integration tests. To only run the latter type, use `./test.sh headless_itests`. +* `cd path/to/pico-boots-demo` +* `./test.sh` I try to aim for 100% test coverage before pushing but you can always verify the Travis and CodeCov badges at the top of this README. ### Run rendered integration tests -Those tests need to be built with picotool and run with PICO-8. We recommend the itest_light config, which has no visual logging but increases the odds to have a build that fits into a cartridge. For example: +Integration tests consists in actual game simulations in predetermined scenarios. Therefore, they are built with picotool and run directly in PICO-8. To build the integration test cartridge and run it: -* `cd path/to/sonic-pico8-repo` -* `./build.sh itestplayercharacter itest_light` -* `./run.sh itestplayercharacter itest_light` +* `cd path/to/sonic-pico8-demo` +* `./build_itest.sh` +* `./run_itest.sh` ## Modding @@ -131,42 +129,6 @@ 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. - -## Runtime third-party libraries - -### PICO8-WTK - -[PICO8-WTK](https://github.com/Saffith/PICO8-WTK) has been integrated as a submodule. I use my own fork with a special branch [cleam\n-lua](https://github.com/hsandt/PICO8-WTK/tree/clean-lua), itself derived from the branch [p8tool](https://github.com/hsandt/PICO8-WTK/tree/p8tool). - -* Branch `p8tool` is dedicated to p8tool integration. It exports variables instead of defining global variables to fit the require pattern. - -* Branch `clean-lua` is dedicated to replacing PICO-8 preprocessed expressions like `+=` and `if (...)` with vanilla Lua equivalents. Unfortunately we need this to use external testing libraries running directly on Lua 5.3. - -## Test third-party libraries - -### gamax92/picolove's pico8 API - -pico8api.lua contains vanilla lua equivalents or placeholders for PICO-8 functions. They are necessary to test modules from *busted* which runs under vanilla lua. The file is heavily based on gamax92/picolove's [api.lua](https://github.com/gamax92/picolove/blob/master/api.lua) and [main.lua](https://github.com/gamax92/picolove/blob/master/main.lua) (for the `pico8` table), with the following changes: - -* Removed console commands (ls, cd, etc.) -* Removed unused functions -* Removed wrapping in api table to import functions globally (except for print) -* Remove implementation for LOVE 2D -* Adapted to Lua 5.3 instead of LuaJIT (uses bit32 module) - -Low-level functions have the same behavior as in PICO-8 (add, del, etc.), whereas rendering functions only simulate the behavior by changing the `pico8` table's state (camera, clip, etc.). Pixels rendered on screen are not simulated though, so the actual effect of rendering cannot be tested. - ## References * Classic Sonic games (Sonic the Hedgehog, Sonic the Hedgehog 2, Sonic the Hedgehog 3 & Knuckles) @@ -187,10 +149,6 @@ 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), 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). +Most assets are derivative works of Sonic the Hedgehog, especially the Master System and Mega Drive games. They have been created, either manually or with a conversion tool, for demonstration purpose. I drew the sprites based on the Mega Drive and GBA games, while the 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. Assets that are not derivative works are under CC BY 4.0. diff --git a/build_game.sh b/build_game.sh new file mode 100755 index 00000000..6a4a911b --- /dev/null +++ b/build_game.sh @@ -0,0 +1,97 @@ +#!/bin/bash + +# This is essentially a proxy script for pico-boots/scripts/build_cartridge.sh. +# However, this is also where you define game information and defined symbols per config. + +# Configuration: paths +picoboots_scripts_path="$(dirname "$0")/pico-boots/scripts" +game_src_path="$(dirname "$0")/src" +data_path="$(dirname "$0")/data" +build_output_path="$(dirname "$0")/build" + +# Configuration: cartridge +author="hsandt" +title="pico-sonic" +cartridge_stem="picosonic" +version="3.0" + +help() { + echo "Build a PICO-8 cartridge with the passed config." + usage +} + +usage() { + echo "Usage: test.sh [CONFIG] + +ARGUMENTS + CONFIG Build config. Determines defined preprocess symbols. + (default: 'debug') + + -h, --help Show this help message +" +} + +# Default parameters +config='debug' + +# Read arguments +# https://stackoverflow.com/questions/192249/how-do-i-parse-command-line-arguments-in-bash +roots=() +while [[ $# -gt 0 ]]; do + case $1 in + -h | --help ) + help + exit 0 + ;; + -* ) # unknown option + echo "Unknown option: '$1'" + usage + exit 1 + ;; + * ) # store positional argument for later + positional_args+=("$1") + shift # past argument + ;; + esac +done + +if ! [[ ${#positional_args[@]} -ge 0 && ${#positional_args[@]} -le 1 ]]; then + echo "Wrong number of positional arguments: found ${#positional_args[@]}, expected 0 or 1." + echo "Passed positional arguments: ${positional_args[@]}" + usage + exit 1 +fi + +if [[ ${#positional_args[@]} -ge 1 ]]; then + config="${positional_args[0]}" +fi + +# Define symbols from config +symbols='' + +if [[ $config == 'debug' ]]; then + symbols='assert,deprecated,log,visual_logger,tuner,profiler,mouse,cheat,sandbox' + # lighter config (to remain under 65536 chars) + # symbols='assert,deprecated,log,cheat,sandbox' +elif [[ $config == 'debug-ultrafast' ]]; then + symbols='assert,deprecated,log,cheat,sandbox,ultrafast' +elif [[ $config == 'cheat' ]]; then + symbols='assert,deprecated,cheat' +elif [[ $config == 'ultrafast' ]]; then + symbols='assert,deprecated,ultrafast' +elif [[ $config == 'cheat-ultrafast' ]]; then + symbols='assert,deprecated,cheat,ultrafast' +elif [[ $config == 'sandbox' ]]; then + symbols='assert,deprecated,sandbox' +fi + +# Build from main +"$picoboots_scripts_path/build_cartridge.sh" \ + "$game_src_path" main.lua \ + -d "$data_path/data.p8" -M "$data_path/metadata.p8" \ + -a "$author" -t "$title" \ + -p "$build_output_path" \ + -o "${cartridge_stem}_v${version}" \ + -c "$config" \ + -s "$symbols" \ + --minify-level 2 diff --git a/build_itest.sh b/build_itest.sh new file mode 100755 index 00000000..4cb2a8bb --- /dev/null +++ b/build_itest.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# Build a PICO-8 cartridge for the integration tests. +# This is essentially a proxy script for pico-boots/scripts/build_cartridge.sh with the right parameters. + +# Extra options are passed to build_cartridge.sh (with $@). +# This is useful in particular for --symbols. + +# Configuration: paths +picoboots_scripts_path="$(dirname "$0")/pico-boots/scripts" +game_src_path="$(dirname "$0")/src" +data_path="$(dirname "$0")/data" +build_output_path="$(dirname "$0")/build" + +# Configuration: cartridge +author="hsandt" +title="pico-sonic itests (all)" +cartridge_stem="picosonic_itest_all" +version="3.0" +config='debug' +# symbols='assert,log,visual_logger,tuner,profiler,mouse,itest' +# for now, we don't set extra symbols like cheat to make it lighter, but it's still possible +# to test cheats in headless itests as busted preserves all (non-#pico8) code +symbols='assert,log,itest' + +# Build from itest main for all itests +"$picoboots_scripts_path/build_cartridge.sh" \ + "$game_src_path" itest_main.lua itests \ + -d "$data_path/data.p8" -M "$data_path/metadata.p8" \ + -a "$author" -t "$title" \ + -p "$build_output_path" \ + -o "${cartridge_stem}_v${version}" \ + -c "$config" \ + -s "$symbols" \ + --minify-level 2 diff --git a/build.sh b/build_legacy.sh similarity index 95% rename from build.sh rename to build_legacy.sh index a9dafa01..298c968c 100755 --- a/build.sh +++ b/build_legacy.sh @@ -66,7 +66,7 @@ rm -f "$OUTPUT_FILEPATH" # 3. Apply lua minification to reduce character count echo "Pre-build..." prebuild/copy_source_folder.sh src "intermediate/${config}" && -python3.6 prebuild/preprocess.py "intermediate/${config}" "$config" && +python3.6 prebuild/preprocess_legacy.py "intermediate/${config}" "$config" && python3.6 prebuild/replace_strings.py "intermediate/${config}" $REPLACE_ARG_SUBSTITUTES if [[ $? -ne 0 ]]; then @@ -107,7 +107,7 @@ fi echo "Post-build..." if [[ "$minify" == true ]]; then - python3.6 postbuild/minify.py "$OUTPUT_FILEPATH" + python3.6 pico-boots/scripts/minify.py "$OUTPUT_FILEPATH" if [[ $? -ne 0 ]]; then echo "Minification failed, STOP." @@ -116,7 +116,7 @@ if [[ "$minify" == true ]]; then fi # Don't use add_label_info in add_metadata.py as we have already copied metadata.p8 to reuse its label, so pass "-" -python3.6 postbuild/add_metadata.py "$OUTPUT_FILEPATH" "-" "sonic pico-8" "hsandt" && +python3.6 pico-boots/scripts/add_metadata.py "$OUTPUT_FILEPATH" "-" "sonic pico-8" "hsandt" && if [[ $? -ne 0 ]] then diff --git a/build_pico8_utests.sh b/build_pico8_utests.sh new file mode 100644 index 00000000..072cf581 --- /dev/null +++ b/build_pico8_utests.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# Build a PICO-8 cartridge for the integration tests. +# This is essentially a proxy script for pico-boots/scripts/build_cartridge.sh with the right parameters. + +# Extra options are passed to build_cartridge.sh (with $@). +# This is useful in particular for --symbols. + +# Configuration: paths +picoboots_scripts_path="$(dirname "$0")/pico-boots/scripts" +game_src_path="$(dirname "$0")/src" +data_path="$(dirname "$0")/data" +build_output_path="$(dirname "$0")/build" + +# Configuration: cartridge +author="hsandt" +title="pico-sonic pico8 utests (all)" +cartridge_stem="picosonic_pico8_utests_all" +version="3.0" +config='debug' +symbols='assert,log' + +# Build from itest main for all pico8 utests +# Note that a pico8 utest_main build is much smaller than a normal build, +# so minification is not required in general; however it is useful to spot +# issues in the real build like unprotected sprite animation keys being minified +"$picoboots_scripts_path/build_cartridge.sh" \ + "$game_src_path" utest_main.lua utests \ + -d "$data_path/data.p8" -M "$data_path/metadata.p8" \ + -a "$author" -t "$title" \ + -p "$build_output_path" \ + -o "${cartridge_stem}_v${version}" \ + -c "$config" \ + -s "$symbols" \ + --minify-level 2 diff --git a/config/.luacov_game b/config/.luacov_game new file mode 100644 index 00000000..c5bc705f --- /dev/null +++ b/config/.luacov_game @@ -0,0 +1,11 @@ +return { + + exclude = { + "_utest$", -- needed when testing all (*) modules, as we include whole folders + "/share/lua/", -- needed when testing specific module, to avoid homonymous library modules loaded by busted: + -- /usr/local/share/lua, $HEREROCKS_PATH/lua5.3/share/lua, etc. + "/pico%-boots/src/", -- needed when testing game to avoid covering engine sources too + "/src/test_data", -- some test data are used in utests, but we don't need to use them all either + } + +} diff --git a/data/data.p8 b/data/data.p8 index e216db58..ec094ee3 100644 --- a/data/data.p8 +++ b/data/data.p8 @@ -1,40 +1,40 @@ pico-8 cartridge // http://www.pico-8.com -version 16 +version 27 __lua__ __gfx__ -eeeeeeeee5eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeee575eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -ee7ee7ee5775eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eee77eee57775eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eee77eee577775eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -ee7ee7ee57755eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeee575eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeee5eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeeee5eeeeeeeeeecccccceeceee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +eeeeeeee575eeeeeeeeceeccccccceee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +ee7ee7ee5775eeeeeeeeccffccccceee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +eee77eee57775eeeeeecccfcc7ccceee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +eee77eee577775eeeecccccc770cceee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +ee7ee7ee57755eeeeeceeecc770ceeee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +eeeeeeee575eeeeeeeeeccccf77f0eee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +eeeeeeeee5eeeeeeeee77ffcfffeeeee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +8888888888888888ee7777ccfeee77ee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +8888888888888888eee77ecccfcf77ee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +8888888888888888eeeeeecccc7ceeee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +8888888888888888eeeee7cee788eeee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +8888888888888888eee0877ee8878eee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +8888888888888888eee0888eee788eee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +8888888888888888eeee8778eee08eee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +8888888888888888eeee0888eeeeeeee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 99994444eeeeeee49eeeeeeeeeeeeeee99eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee 99994444eeeeee4499eeeeeeeeeeeeee999ee4e4eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee 99994444eeeee444999eeeeeeeeeeeee99994444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee @@ -51,22 +51,22 @@ eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee 77777777007777777777770000777777777777770000000077777777000077770000000000000000000000000000000000000000000000000000000000000000 77777777077777777777777077777777777777770000000077777777000077777777777700000000000000000000000000000000000000000000000000000000 77777777777777777777777777777777777777770000000077777777000077777777777700000000000000000000000000000000000000000000000000000000 -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 eeeeccccccee1eeeeeeeccccceeeeeeeeeeeccccceeeeeeeeeeeccccceeeeeeeeeeeccccceeeeeeeeeeecccccceeceeeeeeecccccceeceeeeeeecccccceeceee eeeceeccccccceeeeeececccccceeeeeeeececccccceeeeeeeececccccceeeeeeeececccccceeeeeeeeceeccccccceeeeeeceeccccccceeeeeeceeccccccceee eeeeccffccccceeeeeeeccffcccceeeeeeeeccffcccceeeeeeeeccffcccceeeeeeeeccffcccceeeeeeeeccffccccceeeeeeeccffccccceeeeeeeccffccccceee @@ -83,90 +83,90 @@ eeeeeeeececeeeeeeeee7ccee888eeeeee87ccceeec7870ee07887cceecceeeeeee087ccceeeeeee eeeeeeee7e7eeeeeeee877eeee877eeee787eeeeee7880eee8787cceeee778eeee077eee7eeeeeeeeeeeeec087eeeeeeeee0888eee788eeeee087eeeeee00eee eeeeeee0872088eeeee88878eee88eeee87eeeeeeee80eeeeeeeeeeeeee8878eee08eeee8788eeeeeeeee870788eeeeeeeee8778eee08eeeee888eeeeeeeeeee eeeeeee27888028eeeee87888eeeeeeee88eeeeeeeeeeeeeeeeeeeeeeeee8788ee88eeee07088eeeeeeee8880888eeeeeeee0888eeeeeeeeee0888eeeeeeeeee -eeeecccccceeceeeeeeecccccceeceeeeeeecccccceeceeeeeeeccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeceeccccccceeeeeeceeccccccceeeeeeceeccccccceeeeeececccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeccffccccceeeeeeeccffccccceeeeeeeccffccccceeeeeeeccffcccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeecccfcc7ccceeeeeecccfcc7ccceeeeeecccfcc7ccceeeeeecccfcc7ccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eecccccc771cceeeeecccccc771cceeeeecccccc771cceeeeeccccccc70cceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeceeecc771ceeeeeeceeecc771ceeeeeeceeecc771ceeeeeeceecccc70ceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeccccf77f0eeeeeeeccccf77f0eeeeeeeccccf77f0eeeeeeeccccf7ff0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeecefccfeeeeeeeeeecefccfeeeeeeeeeecffccffeeeeeeeeceecccffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeefccffe77eeeeeeeefcccfeeeeeeeeeefcccfeeeeeeeeeeeeccffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeefecccff77eeeeeeee77ccf77eeeeeee77eecfeeeeeeeeeeee77cfe77eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eee77eeccceeeeeeeeeee77ccc77eeeeeee77eeccf77eeeeeeeee77ccf77eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eee77ecceccee88eee000eececceeeeeeeeeeeecce77eeeeeeeeeeececeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -ee80ccceeec7870ee07887cceecceeeeeee087ccceeeeeeeeeeeeeec07eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -e787eeeeee7880eee8787cceeee778eeee077eee7eeeeeeeeeeeee7c88eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -e87eeeeeeee80eeeeeeeeeeeeee8878eee08eeee8788eeeeeeeee08700eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -e88eeeeeeeeeeeeeeeeeeeeeeeee8788ee88eeee07088eeeeeeee078880eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeecccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -ecccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eccccc7cceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -ccccc777cceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -cccccc7ccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -cccccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -cccccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -ecccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -ecccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeecccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeecccccceeceeeeeeecccccceeceeeeeeecccccceeceeeeeeeccccceeeeeee8888888888888888888888888888888888888888888888888888888888888888 +eeeceeccccccceeeeeeceeccccccceeeeeeceeccccccceeeeeececccccceeeee8888888888888888888888888888888888888888888888888888888888888888 +eeeeccffccccceeeeeeeccffccccceeeeeeeccffccccceeeeeeeccffcccceeee8888888888888888888888888888888888888888888888888888888888888888 +eeecccfcc7ccceeeeeecccfcc7ccceeeeeecccfcc7ccceeeeeecccfcc7ccceee8888888888888888888888888888888888888888888888888888888888888888 +eecccccc771cceeeeecccccc771cceeeeecccccc771cceeeeeccccccc70cceee8888888888888888888888888888888888888888888888888888888888888888 +eeceeecc771ceeeeeeceeecc771ceeeeeeceeecc771ceeeeeeceecccc70ceeee8888888888888888888888888888888888888888888888888888888888888888 +eeeeccccf77f0eeeeeeeccccf77f0eeeeeeeccccf77f0eeeeeeeccccf7ff0eee8888888888888888888888888888888888888888888888888888888888888888 +eeeecefccfeeeeeeeeeecefccfeeeeeeeeeecffccffeeeeeeeeceecccffeeeee8888888888888888888888888888888888888888888888888888888888888888 +eeeeefccffe77eeeeeeeefcccfeeeeeeeeeefcccfeeeeeeeeeeeeccffeeeeeee8888888888888888888888888888888888888888888888888888888888888888 +eeeefecccff77eeeeeeee77ccf77eeeeeee77eecfeeeeeeeeeeee77cfe77eeee8888888888888888888888888888888888888888888888888888888888888888 +eee77eeccceeeeeeeeeee77ccc77eeeeeee77eeccf77eeeeeeeee77ccf77eeee8888888888888888888888888888888888888888888888888888888888888888 +eee77ecceccee88eee000eececceeeeeeeeeeeecce77eeeeeeeeeeececeeeeee8888888888888888888888888888888888888888888888888888888888888888 +ee80ccceeec7870ee07887cceecceeeeeee087ccceeeeeeeeeeeeeec07eeeeee8888888888888888888888888888888888888888888888888888888888888888 +e787eeeeee7880eee8787cceeee778eeee077eee7eeeeeeeeeeeee7c88eeeeee8888888888888888888888888888888888888888888888888888888888888888 +e87eeeeeeee80eeeeeeeeeeeeee8878eee08eeee8788eeeeeeeee08700eeeeee8888888888888888888888888888888888888888888888888888888888888888 +e88eeeeeeeeeeeeeeeeeeeeeeeee8788ee88eeee07088eeeeeeee078880eeeee8888888888888888888888888888888888888888888888888888888888888888 +eeecccceeeeeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +ecccccccceeeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +eccccc7cceeeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +ccccc777cceeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +cccccc7ccceeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +cccccccccceeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +cccccccccceeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +ecccccccceeeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +ecccccccceeeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +eeecccceeeeeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +eeeeeeeeeeeeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +eeeeeeeeeeeeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +eeeeeeeeeeeeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +eeeeeeeeeeeeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +eeeeeeeeeeeeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +eeeeeeeeeeeeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 __gff__ 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001010101010001010100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 __map__ -45454545eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4545454545454545454510eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -45454545eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4545454545454545454510eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000000000000000000000000000000000 -45454545eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4545454545454545454510eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000000000000000000000000000000000 -45454545eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4545454545454545454510eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000000000000000000000000000000000 -45454512eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4545454545454545454510eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000000000000000000000000000000000 -45454545eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4545454545454545454510eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000000000000000000000000000000000 -45454545eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4545454545454545454510eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000000000000000000000000000000000 -45454545eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee45454545454545454545454545454545eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000000000000000000000000000000000 -45454545eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee45454545454545454545454545eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000000000000000000000000000000000 -45454545eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4545454545454545454510eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee45eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee100000000000000000000000000000000000000000000000000000 -45454545eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4545454545454545454545eeeeeeeeeeee454540404040404040404040eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee100000000000000000000000000000000000000000000000000000 -45454545eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4545454545454545454545eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee100000000000000000000000000000000000000000000000000000 -45454545eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4545454545454545454545454545454545454545eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee100000000000000000000000000000000000000000000000000000 -45454545eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4545454545454545454545eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee100000000000000000000000000000000000000000000000000000 -10101010eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee40404045454545454545454545eeee40404040eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee100000000000000000000000000000000000000000000000000000 -4010101010101045454545454545454545454545454545454545454545454545454545eeeeeeeeeeeeeeeeeeeeeeeeee45454545454545454545eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee100000000000000000000000000000000000000000000000000000 -4010101010101045454545454545454545454545454545454545454545454545454545eeeeee1010101010104040404045454545454545454545eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee100000000000000000000000000000000000000000000000000000 -4010101010101045454545454545454545454545454545454545454545454545454545eeeeee1010101010104040404045454545454545454545eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee100000000000000000000000000000000000000000000000000000 -40101010101010454545454545454545454545454545454545454545454545454540404040401010101010104040404010101010101010101010eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee100000000000000000000000000000000000000000000000000000 -4010101210101045454545454545454646464646454545454545454545454545454545eeeeee1010101010461010101010101010101010101010eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee100000000000000000000000000000000000000000000000000000 -4010101010101045454545454545454545454545454545454545454545454545454545eeeeee1010414040401010101010101010101010101010eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee100000000000000000000000000000000000000000000000000000 -4010101010101045454545454545454545454545454545454545454545454545454545eeeeee1010404040401010101010101040404040101010eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee100000000000000000000000000000000000000000000000000000 -4010101010101045464646464645454545454545454545454545454545454545454545eeeeee1010101010101010101010101040404040101010eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee100000000000000000000000000000000000000000000000000000 -4010101010101045454545454545454545454545454545454545454545454545454545eeeeee1010101010101010404040404010101010101010eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee100000000000000000000000000000000000000000000000000000 -4010101010101045454545454545454545454545454545eeeeee414040404040eeeeeeeeeeee1010101010101010404040404010101010101010eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000000000000000000000000000000 -4040404040404040424545454545454545454545454545eeee41404040404040eeeeeeeeeeee104140404040404040eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000000000000000000000000000000 -4040404040404040404245454545454545454545454545ee4140404040404040eeeeeeeeeeee414040404040404040eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000000000000000000000000000000 -404040404040404040404245454545454545454545454541404040404040404040404040404040404040404040eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000000000000000000000000000000 -4040404040404040404040424545454545454545454541404040404040404040eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000000000000000000000000000000 -1010101010101010104040404210101010451045104140404040404040404040eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000000000000000000000000000000 -1010101010101010101040404040404040404040404040404040404040404040eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000000000000000000000000000000 -101010101010101010101010101010101010101010101040404040eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000000000000000000000000000000 +4545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4545454545454545454545454545454545454545454545454545454545454545454545454545454545454540404040404040404040454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4545454545454545454545454545454545454545454540404045454545454545454545454540404040454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4045454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4045454545454545454545454545454545454545454545454545454545454545454545454545454545454545404040404545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4045454545454545454545454545454545454545454545454545454545454545454545454545454545454545404040404545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4045454545454545454545454545454545454545454545454545454545454545454040404040454545454545404040404545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4045454545454545454545454545454646464646454545454545454545454545454545454545454545454546454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4045454545454545454545454545454545454545454545454545454545454545454545454545454541404040454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4045454545454545454545454545454545454545454545454545454545454545454545454545454540404040454545454545454040404045454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4045454545454545464646464645454545454545454545454545454545454545454545454545454545454545454545454545454040404045454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4045454545454545454545454545454545454545454545454545454545454545454545454545454545454545454540404040404545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4045454545454545454545454545454545454545454545454545414040404040454545454545454545454545454540404040404545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4040404040404040424545454545454545454545454545454541404040404040454545454545454140404040404040454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4040404040404040404245454545454545454545454545454140404040404040454545454545414040404040404040454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4040404040404040404042454545454545454545454545414040404040404040404040404040404040404040404545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4040404040404040404040424545454545454545454541404040404040404040454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4545454545454545454040404245454545454545454140404040404040404040454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4545454545454545454540404040404040404040404040404040404040404040454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +4545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 __sfx__ 010c00002d340293402d340293402f3402b3402f3402b340303402d340303402d340323402f340323402f34023340233403062500000306250000021340213403062500000306250000023340233403062500000 010c000030625000000934009345153401534509340093450a3400a34516340163450a3400a34517340173450c3400c3450c3400c3450c3400c3450c3400c3450c3400c3450c3400c3450c3400c3450c3400c345 diff --git a/data/metadata.p8 b/data/metadata.p8 index b5ccf19e..619e9b9b 100644 --- a/data/metadata.p8 +++ b/data/metadata.p8 @@ -4,131 +4,131 @@ __lua__ -- sonic pico-8 -- by hsandt __label__ -55222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -56662666266622662666262622222222222222222222266626662622222226662662262226262222222222222222222222222222222222222222222222222222 -26662622266626262626262622222222222222222222222626262622222222262262262226262222222222222222222222222222222222222222222222222222 -26262662262626262662266622222222222222222222266626262666222222662262266626662222222222222222222222222222222222222222222222222222 -26262622262626262626222622222222222222222222262226262626222222262262262622262222222222222222222222222222222222222222222222222222 -26262666262626622626266622222222222222222222266626662666226226662666266622262222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -26662266266626662622222222662666262622222222266622222666266626662666222222222222222222222222222222222222222222222222222222222222 -22622626226226262622222226222626262622222222262622222626262626262626222222222222222222222222222222222222222222222222222222222222 -22622626226226662622222226222666262622222222262622222626266626662666222222222222222222222222222222222222222222222222222222222222 -22622626226226262622222226222622262622222222262622222626222622262226222222222222222222222222222222222222222222222222222222222222 -22622662226226262666222222662622226622222222266622622666222622262226222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22662626226626662666266622222266266626262222266622222666266626662666222222222222222222222222222222222222222222222222222222222222 -26222626262222622622266622222622262626262222262622222626262622262622222222222222222222222222222222222222222222222222222222222222 -26662666266622622662262622222622266626262222262622222626266622262666222222222222222222222222222222222222222222222222222222222222 -22262226222622622622262622222622262226262222262622222626262622262226222222222222222222222222222222222222222222222222222222222222 -26622666266222622666262622222266262222662222266622622666266622262666222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -26662666226622222222222222222222222222222222262226662222222222222222222222222222222222222222222222222222222222222222222222222222 -26222626262222222222222222222222222222222222262226262222222222222222222222222222222222222222222222222222222222222222222222222222 -26622666266622222222222222222222222222222222266626262222222222222222222222222222222222222222222222222222222222222222222222222222 -26222622222622222222222222222222222222222222262626262222222222222222222222222222222222222222222222222222222222222222222222222222 -26222622266222222222222222222222222222222222266626662222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -26662666266622662666266622222666266622662222262226662222222222222222222222222222222222222222222222222222222222222222222222222222 -22622626262626222622226222222622262626222222262226262222222222222222222222222222222222222222222222222222222222222222222222222222 -22622666266226222662226222222662266626662222266626262222222222222222222222222222222222222222222222222222222222222222222222222222 -22622626262626262622226222222622262222262222262626262222222222222222222222222222222222222222222222222222222222222222222222222222 -22622626262626662666226222222622262226622222266626662222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -26662266266626262666262222222666266622662222262226662222222222222222222222222222222222222222222222222222222222222222222222222222 -26262622226226262626262222222622262626222222262226262222222222222222222222222222222222222222222222222222222222222222222222222222 -26662622226226262666262222222662266626662222266626262222222222222222222222222222222222222222222222222222222222222222222222222222 -26262622226226262626262222222622262222262222262626262222222222222222222222222222222222222222222222222222222222222222222222222222 -26262266226222662626266622222622262226622222266626662222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222277777777222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222277777777222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222277777777222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222277777777222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222277777777222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222277777777222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222277777777222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222277777777222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222277777777222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222277777777222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222277777777222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222277777777222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222299994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444492222222222222 -22222222222222222299994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499222222222222 -22222222222222222299994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499922222222222 -22222222222222222299994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499992222222222 -22222222222222222244449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944449222222222 -22222222222222222244449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944449922222222 -22222222222222222244449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944449992222222 -22222222222222222244449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999222222 -22222222222222222299994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444922222 -22222222222222222299994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444992222 -22222222222222222299994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999222 -22222222222222222299994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999922 -22222222222222222244449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444492 -22222222222222222244449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499 -22222222222222222244449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499 -22222222222222222244449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499 -22222222222222222299994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944 -22222222222222222299994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944 -22222222222222222299994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944 -22222222222222222299994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944 -22222222222222222244449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499 -22222222222222222244449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499 -22222222222222222244449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499 -22222222222222222244449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499 -22222222222222222299994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944 -22222222222222222299994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944 -22222222222222222299994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944 -22222222222222222299994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944 -22222222222222222244449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499 -22222222222222222244449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499 -22222222222222222244449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499 -22222222222222222244449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +44999944449999444499994444999911111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +44999944449999444499994444999911111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +44999944449999444499994444999911111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +44999944449999444499994444999911111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111114 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111144 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111499 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111114499 +91111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111144499 +99111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111444499 +44911111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111114999944 +44991111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111144999944 +44999111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111444999944 +44999911111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111114444999944 +99444491111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111149999444499 +99444499111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111449999444499 +99444499911111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111114449999444499 +99444499991111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111144449999444499 +44999944449111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111499994444999944 +44999944449911111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111114499994444999944 +44999944449991111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111144499994444999944 +44999944449999111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111444499994444999944 +994444999944449111111111111111111111111111111111111111111cccccc11c11111111111111111111111111111111111111111114999944449999444499 +99444499994444991111111111111111111111111111111111111111c11ccccccc11111111111111111111111111111111111111111144999944449999444499 +994444999944449991111111111111111111111111111111111111111ccffccccc11111111111111111111111111111111111111111444999944449999444499 +99444499994444999911111111111111111111111111111111111111cccfcc7ccc11111111111111111111111111111111111111114444999944449999444499 +4499994444999944449111111111111111111111111111111111111cccccc770cc11111111111111111111111111111111111111149999444499994444999944 +4499994444999944449911111111111111111111111111111111111c111cc770c111111111111111111111111111111111111111449999444499994444999944 +449999444499994444999111111111111111111111111111111111111ccccf77f011111111111111111111111111111111111114449999444499994444999944 +4499994444999944449999111111111111111111111111111111111177ffcfff1111111111111111111111111111111111111144449999444499994444999944 +99444499994444999944449111111111111111111111111111111117777ccf111771111111111111111111111111111111111499994444999944449999444499 +99444499994444999944449911111111111111111111111111111111771cccfcf771111111111111111111111111111111114499994444999944449999444499 +99444499994444999944449991111111111111111111111111111111111cccc7c111111111111111111111111111111111144499994444999944449999444499 +99444499994444999944449999111111111111111111111111111111117c11788111111111111111111111111111111111444499994444999944449999444499 +44999944449999444499994444911111111111111111111111111111087711887811111111111111111111111111111114999944449999444499994444999944 +44999944449999444499994444991111111111111111111111111111088811178811111111111111111111111111111144999944449999444499994444999944 +44999944449999444499994444999111111111111111111111111111187781110811111111111111111111111111111444999944449999444499994444999944 +44999944449999444499994444999911111111111111111111111111108881111111111111111111111111111111114444999944449999444499994444999944 +11111199994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499 +11111199994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499 +11111199994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499 +11111199994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499 +11111144449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944 +11111144449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944 +11111144449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944 +11111144449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944449999444499994444999944 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111999944449999444499 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111999944449999444499 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111999944449999444499 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111999944449999444499 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111444499994444999944 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111444499994444999944 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111444499994444999944 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111444499994444999944 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 +11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 diff --git a/data/metadata.p8.png b/data/metadata.p8.png index b8d13dbd..36914c19 100644 Binary files a/data/metadata.p8.png and b/data/metadata.p8.png differ diff --git a/edit_data.sh b/edit_data.sh new file mode 100644 index 00000000..8cee3206 --- /dev/null +++ b/edit_data.sh @@ -0,0 +1,3 @@ +#!/bin/bash +# Open data.p8 for edit and save +pico8 -run data/data.p8 $@ diff --git a/edit_metadata.sh b/edit_metadata.sh new file mode 100644 index 00000000..545b584c --- /dev/null +++ b/edit_metadata.sh @@ -0,0 +1,3 @@ +#!/bin/bash +# Open metadata.p8 for edit and save +pico8 -run data/metadata.p8 $@ diff --git a/export_cartridge_release.p8 b/export_cartridge_release.p8 new file mode 100644 index 00000000..34d51581 --- /dev/null +++ b/export_cartridge_release.p8 @@ -0,0 +1,23 @@ +pico-8 cartridge // http://www.pico-8.com +version 16 +__lua__ +-- Run this commandline script with: +-- $ pico8 -x export_cartridge.p8 + +-- It will export .bin and .p8.png for the current game release +-- Make sure to ./build_game release && ./install_cartridge_linux.sh first +-- Note that it will note warn if cartridge is not found. +-- Paths are relative to PICO-8 carts directory. + +cd("picosonic") +load("picosonic_v3.0_release.p8") +-- png cartridge export is done via SAVE +-- the metadata label is used automatically +save("picosonic_v3.0_release.p8.png") +-- other exports are done via EXPORT, and can use an icon +-- instead of the .p8.png label +-- icon is a 16x16 square => -s 2 tiles wide +-- with top-left at sprite 2 => -i 2 +-- on pink (color 14) background => -c 14 +export("picosonic_v3.0_release.bin -i 2 -s 2 -c 14") +export("picosonic_v3.0_release.html -i 2 -s 2 -c 14") diff --git a/export_cartridge_release.sh b/export_cartridge_release.sh new file mode 100644 index 00000000..b55f260c --- /dev/null +++ b/export_cartridge_release.sh @@ -0,0 +1,2 @@ +#!/bin/bash +pico8 -x export_cartridge_release.p8 diff --git a/helper/config_helper.sh b/helper/config_helper_legacy.sh similarity index 95% rename from helper/config_helper.sh rename to helper/config_helper_legacy.sh index 0c248a60..7b017d51 100644 --- a/helper/config_helper.sh +++ b/helper/config_helper_legacy.sh @@ -57,18 +57,19 @@ function define_build_vars { # OUTPUT_BASENAME is isolated because run.sh only needs this one function define_output_basename { + if [[ -z $BUILD_VERSION ]]; then + VERSION_SUFFIX="" + else + VERSION_SUFFIX="_v${BUILD_VERSION}" + fi + if [[ ${1::5} = "itest" ]] ; then - OUTPUT_BASENAME="$1" + OUTPUT_BASENAME="$1${VERSION_SUFFIX}" elif [[ ${1::5} = "utest" ]] ; then - OUTPUT_BASENAME="$1" + OUTPUT_BASENAME="$1${VERSION_SUFFIX}" elif [[ $1 = "sandbox" ]]; then OUTPUT_BASENAME="sandbox" else - if [[ -z $BUILD_VERSION ]]; then - VERSION_SUFFIX="" - else - VERSION_SUFFIX="_v${BUILD_VERSION}" - fi OUTPUT_BASENAME="sonic-pico8${VERSION_SUFFIX}" fi } diff --git a/helper/path_helper.sh b/helper/path_helper.sh index 3edd17f5..3d943efa 100755 --- a/helper/path_helper.sh +++ b/helper/path_helper.sh @@ -1,4 +1,4 @@ # make sure paths passed are not absolute or backward relative to avoid deletion of upper paths function is_unsafe_path { - [[ $1 == /* ]] || [[ $1 == *..* ]] + [[ $1 == /* ]] || [[ $1 == .. ]] || [[ $1 == ../* ]] || [[ $1 == */.. ]] || [[ $1 == */../* ]] } diff --git a/install.sh b/install.sh deleted file mode 100755 index 40ad496c..00000000 --- a/install.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash -# Install a built game to the default Linux PICO-8 carts location -# $1: game file path (debug or release, .p8 pr .p8.png) - -# check that source and output paths have been provided -if [[ $# -lt 1 ]] ; then - echo "build.sh takes 1 param, provided $#: - \$1: config ('debug' or 'release')" - exit 1 -fi - -# option "png" will export the png cartridge -if [[ $2 = "png" ]] ; then - SUFFIX=".png" -else [[ $2 = "solo" ]] - SUFFIX="" -fi - -. helper/config_helper.sh - -# will define: MAIN_SOURCE_BASENAME, OUTPUT_BASENAME, REPLACE_ARG_SUBSTITUTES -define_build_vars "main" - -if [[ $? -ne 0 ]]; then - echo "define_build_vars failed, STOP." - exit 1 -fi - -. helper/path_helper.sh - -if is_unsafe_path "$OUTPUT_BASENAME"; then - echo "$0: build path is unsafe: '$OUTPUT_BASENAME'" - exit 1 -fi - -BUILT_GAME_FILEPATH="build/${OUTPUT_BASENAME}_$1.p8${SUFFIX}" -CARTS_DIRPATH="$HOME/.lexaloffle/pico-8/carts" - -if [[ ! -f "${BUILT_GAME_FILEPATH}" ]]; then - echo "File ${BUILT_GAME_FILEPATH} could not be found, cannot install. Make sure you built it first." - exit 1 -fi - -echo "Installing ${BUILT_GAME_FILEPATH} to ${CARTS_DIRPATH} ..." -cp "${BUILT_GAME_FILEPATH}" "${CARTS_DIRPATH}" diff --git a/install_cartridge_linux.sh b/install_cartridge_linux.sh new file mode 100644 index 00000000..47315108 --- /dev/null +++ b/install_cartridge_linux.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# Install a built game to the default Linux PICO-8 carts location +# $1: game file path (debug or release, .p8 pr .p8.png) + +# check that source and output paths have been provided +if [[ $# -lt 1 ]] ; then + echo "build.sh takes 1 or 2 params, provided $#: + \$1: config ('debug' or 'release') + \$2: optional suffix ('png' for .png cartridge install)" + exit 1 +fi + +# Configuration: cartridge +cartridge_stem="picosonic" +version="3.0" +config="$1"; shift + +# option "png" will export the png cartridge +if [[ $2 = "png" ]] ; then + suffix=".png" +else + suffix="" +fi + +cartridge_filepath="build/${cartridge_stem}_v${version}_${config}.p8${suffix}" +carts_dirpath="$HOME/.lexaloffle/pico-8/carts" +install_dirpath="${carts_dirpath}/picosonic" + +if [[ ! -f "${cartridge_filepath}" ]]; then + echo "File ${cartridge_filepath} could not be found, cannot install. Make sure you built it first." + exit 1 +fi + +echo "Installing ${cartridge_filepath} in ${install_dirpath} ..." +mkdir -p "${install_dirpath}" +# trailing slash just to make sure we copy to a directory +cp "${cartridge_filepath}" "${install_dirpath}/" +echo "Done." diff --git a/npm/.gitignore b/npm/.gitignore deleted file mode 100644 index c093d3cf..00000000 --- a/npm/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Dependency directories -node_modules/ diff --git a/npm/LICENSE-MIT.txt b/npm/LICENSE-MIT.txt deleted file mode 100644 index ad73e022..00000000 --- a/npm/LICENSE-MIT.txt +++ /dev/null @@ -1,21 +0,0 @@ -Original work Copyright Mathias Bynens -Modified work Copyright 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 in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/npm/luamin_file b/npm/luamin_file deleted file mode 100755 index c7f45c45..00000000 --- a/npm/luamin_file +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env node - -// based on luamin/bin/luamin -// - only takes 1+ filepath arguments, no -f option -// - handles non-TTY input stream -(function() { - - var fs = require('fs'); - var luamin = require('luamin/luamin.js'); - var minify = luamin.minify; - var snippets = process.argv.splice(2); - var stdin = process.stdin; - var data; - var log = console.log; - var main = function() { - if (!snippets.length) { - log('Error: luamin_file` requires a filepath argument.', option); - return process.exit(1); - } - - snippets.forEach(function(snippet) { - var result; - try { - snippet = fs.readFileSync(snippet, 'utf8'); - } catch(error) { - log('Error: no such file. (`%s`)', snippet); - return process.exit(1); - } - try { - result = minify(snippet); - log(result); - } catch(error) { - log(error.message + '\n'); - log('Error: failed to minify. Make sure the Lua code is valid.'); - log('If you think this is a bug in luamin, please report it:'); - log('https://github.com/mathiasbynens/luamin/issues/new'); - log( - '\nStack trace using luamin@%s and luaparse@%s:\n', - luamin.version, - require('luaparse').version - ); - log(error.stack); - return process.exit(1); - } - }); - // Return with exit status 0 outside of the `forEach` loop, in case - // multiple snippets or files were passed in. - return process.exit(0); - - }; - - // hack: handle shell arguments, even for non-TTY input stream - // to avoid stalling while trying to handle pipes in build scripts - main(); - -}()); diff --git a/npm/package-lock.json b/npm/package-lock.json deleted file mode 100644 index ac0b556b..00000000 --- a/npm/package-lock.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "sonic-pico8-luamin", - "version": "1.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "luamin": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/luamin/-/luamin-1.0.4.tgz", - "integrity": "sha1-lEUptY/G+k0x6s4uA1PUEhDw49M=", - "requires": { - "luaparse": "0.2.1" - } - }, - "luaparse": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/luaparse/-/luaparse-0.2.1.tgz", - "integrity": "sha1-qo9WEysN6X0388mRqd9C4OF/ZWw=" - } - } -} diff --git a/npm/package.json b/npm/package.json deleted file mode 100644 index 758b1e1e..00000000 --- a/npm/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "sonic-pico8-luamin", - "version": "1.0.0", - "description": "Wrapper around luamin to support non-tty scripts", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "hsandt", - "license": "MIT", - "dependencies": { - "luamin": "^1.0.4" - } -} diff --git a/pico-boots b/pico-boots new file mode 160000 index 00000000..d93d71ca --- /dev/null +++ b/pico-boots @@ -0,0 +1 @@ +Subproject commit d93d71ca3b9d827e22c424406c0b484d911e5dc7 diff --git a/postbuild/__init__.py b/postbuild/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/postbuild/add_metadata.py b/postbuild/add_metadata.py deleted file mode 100644 index a51dfec9..00000000 --- a/postbuild/add_metadata.py +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env python3.6 -# -*- coding: utf-8 -*- -import argparse -import os -import shutil, tempfile -import sys - -# This script does 3 things: -# 1. Add game title and author at the top of source code for .p8.png -# 2. Add __label__ section from separate file for .p8.png if label_filepath is not '-' (to make up for the lack of --label option in p8tool) -# 3. Fix pico-8 version to 16 (instead of 8 with current p8tool behavior) - -# Usage: -# add_metadata.py filepath label_filepath -# filepath: built game path -# label_filepath: path of file containing label data (pass '-' to preserve label from overwritten built game file if any) - - -def add_title_author_info(filepath, title, author): - """ - Add game title and author at the top of source code - Additionally it fixes the version to 16 (not required to save the game with correct metadata) - - test.p8: - pico-8 cartridge // http://www.pico-8.com - version 8 - __lua__ - package={loaded={},_c={}} - package._c["module"]=function() - - >>> add_title_author_info('test.p8', 'test game', 'tas') - - test.txt: - pico-8 cartridge // http://www.pico-8.com - version 16 - __lua__ - -- test game - -- by tas - package={loaded={},_c={}} - package._c["module"]=function() - - """ - with open(filepath, 'r') as f: - # create a temporary file with the modified content before it replaces the original file - temp_dir = tempfile.mkdtemp() - try: - temp_filepath = os.path.join(temp_dir, 'test.p8') - with open(temp_filepath, 'w') as temp_f: - for line in f: - if line.strip() == 'version 8': - temp_f.write('version 16\n') - continue - temp_f.write(line) - if line.strip() == '__lua__': - # lua block detected, add title and author after the tag line - temp_f.write(f'-- {title}\n') - temp_f.write(f'-- by {author}\n') - shutil.copy(temp_filepath, filepath) - finally: - shutil.rmtree(temp_dir) - - -# This function is currently unused because preserving label from metadata template is easier -def add_label_info(filepath, label_filepath): - """ - Replace label content inside the file with content from another line - - test.p8: - __label__ - 0000 - - label.p8: - __label__ - 1234 - - >>> add_label_info('test.p8', 'label.p8') - - test.p8: - __label__ - 1234 - - """ - label_lines = [] - with open(label_filepath, 'r') as f: - inside_label = False - for line in f: - stripped_line = line.strip() - if not inside_label and stripped_line == '__label__': - inside_label = True - elif inside_label: - # stop if blank line or next section starts - if not stripped_line or line.startswith('__'): - break - # save label content (in case it's the last line, force newline) - label_lines.append(f'{stripped_line}\n') - - with open(filepath, 'r') as f: - # create a temporary file with the modified content before it replaces the original file - temp_dir = tempfile.mkdtemp() - try: - temp_filepath = os.path.join(temp_dir, 'test.p8') - with open(temp_filepath, 'w') as temp_f: - inside_label = False - for line in f: - stripped_line = line.strip() - if inside_label: - # reset inside_label if blank line or next section starts - if not stripped_line or line.startswith('__'): - inside_label = False - else: - temp_f.write(line) - if stripped_line == '__label__': - inside_label = True - # immediately print all label lines - for label_line in label_lines: - temp_f.write(label_line) - - shutil.copy(temp_filepath, filepath) - finally: - shutil.rmtree(temp_dir) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Add metadata on a p8tool output file.') - parser.add_argument('filepath', type=str, help='path of the file to process (.p8)') - parser.add_argument('label_filepath', type=str, help='path of the file containing the label content to copy') - parser.add_argument('title', type=str, help='game title') - parser.add_argument('author', type=str, help='author') - args = parser.parse_args() - add_title_author_info(args.filepath, args.title, args.author) - if args.label_filepath != '-': - add_label_info(args.filepath, args.label_filepath) - print(f"Added metadata (title: {args.title}, author: {args.author}) to {args.filepath} based on label {args.label_filepath}.") diff --git a/postbuild/minify.py b/postbuild/minify.py deleted file mode 100755 index 17486472..00000000 --- a/postbuild/minify.py +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env python3.6 -# -*- coding: utf-8 -*- -import argparse -import logging -import os -import shutil, tempfile -import re -from enum import Enum -from subprocess import Popen, PIPE - - -# This script minifies the __lua__ section of a cartridge {game}.p8: -# 1. It uses p8tool listlua A.p8 to quickly extract the __lua__ code into {game}.lua -# 2. Convert remaining bits of pico8 lua (generated by p8tool) into clean lua -# 3. It applies luamin to {game}.lua and outputs to {game}_min.lua -# 4. It reads the header (before __lua__) of {game}.p8 and copies it into {game}_min.p8 -# 5. It appends {game}_min.lua's content to {game}_min.p8 -# 6. It finishes reading {game}.p8's remaining sections and appends them into {game}_min.p8 -# 7. It replaces {game}.p8 with {game}_min.p8 - - -LUA_HEADER = b"__lua__\n" -# note that this pattern captures 1. condition 2. result of a "one-line if" if it is, -# but that it also matches a normal if-then, requiring a check before using the pattern -PICO8_ONE_LINE_IF_PATTERN = re.compile(r"if \(([^)]*)\) (.*)") - - -class Phase(Enum): - CARTRIDGE_HEADER = 1 # copying header, from "pico-8 cartridge..." to "__lua__" - LUA_SECTION = 2 # found "__lua__", still copy the 2 author/version comment lines then appending minified lua all at once - LUA_CATCHUP = 3 # skipping the unused unminified lua until we reach the other sections - OTHER_SECTIONS = 4 # copying the last sections - - -def minify_lua_in_p8(cartridge_filepath): - """ - Minifies the __lua__ section of a p8 cartridge, using luamin. - - """ - logging.debug(f"Minifying lua in cartridge {cartridge_filepath}...") - - root, ext = os.path.splitext(cartridge_filepath) - if not ext.endswith(".p8"): - logging.error(f"Cartridge filepath '{cartridge_filepath}' does not end with '.p8'") - return - - min_cartridge_filepath = f"{root}_min.p8" - lua_filepath = f"{root}.lua" - min_lua_filepath = f"{root}_min.lua" - - # Step 1: extract lua code - with open(lua_filepath, 'w') as lua_file: - extract_lua(cartridge_filepath, lua_file) - - # Step 2: clean lua - with open(lua_filepath, 'r') as lua_file: - # create temporary file object (we still need to open it with mode to get file descriptor) - temp_file_object, temp_filepath = tempfile.mkstemp() - original_char_count = sum(len(line) for line in lua_file) - print(f"Original lua code has {original_char_count} characters") - lua_file.seek(0) - clean_lua(lua_file, os.fdopen(temp_file_object, 'w')) - os.remove(lua_filepath) - shutil.move(temp_filepath, lua_filepath) - - # Step 3: apply luamin - with open(min_lua_filepath, 'w+') as min_lua_file: - minify_lua(lua_filepath, min_lua_file) - 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 - with open(cartridge_filepath, 'r') as source_file, \ - open(min_cartridge_filepath, 'w') as target_file, \ - open(min_lua_filepath, 'r') as min_lua_file: - inject_minified_lua_in_p8(source_file, target_file, min_lua_file) - - # Step 7: replace original p8 with minified p8, clean up intermediate files - os.remove(cartridge_filepath) - os.remove(lua_filepath) - os.remove(min_lua_filepath) - shutil.move(min_cartridge_filepath, cartridge_filepath) - -def extract_lua(source_filepath, lua_file): - """ - Extract lua from source_filepath (string) to lua_file (file descriptor: write) - - """ - # p8tool listrawlua has a bug (https://github.com/dansanderson/picotool/issues/59) - # which prevents me from using it until it is fixed. listlua is safer, - # but will take ~1s to parse the game .p8 - # note: p8tool listlua doesn't spawn a Zombie process, but we prefer to communicate() - Popen(["p8tool", "listlua", source_filepath], stdout=lua_file, stderr=lua_file).communicate() - - -def clean_lua(lua_file, clean_lua_file): - """ - Convert PICO-8 specific lines from to lua_file (file descriptor: read) - to native Lua in clean_lua_file (file descriptor: write) - - """ - for line in lua_file: - # we simplify things a lot thanks to our assumptions on the generated code - # we know that the only pico8 one-line if will be generated for the require function - # and have the pattern "if (condition) [result]" without "then", - # and there are no edge cases like embedded conditions or continuing line with "\" - if line.startswith("if (") and "then" not in line: - # convert to "if [condition] then [result] end" - clean_lua_file.write(PICO8_ONE_LINE_IF_PATTERN.sub("if \\1 then \\2 end", line)) - else: - clean_lua_file.write(line) - - - -def minify_lua(lua_filepath, min_lua_file): - """ - Minify lua from lua_filepath (string) - and send output to min_lua_file (file descriptor: write) - - """ - Popen(["npm/luamin_file", lua_filepath], stdout=min_lua_file, stderr=min_lua_file).communicate() - - -def inject_minified_lua_in_p8(source_file, target_file, min_lua_file): - """ - Inject minified lua from min_lua_file (file descriptor: read) - into a copy of source_file (file descriptor: read) - producing target_file (file descriptor: write) - - """ - phase = Phase.CARTRIDGE_HEADER - for line in source_file: - if phase is Phase.CARTRIDGE_HEADER: - # Step 4: copy header (also copy the "__lua__" line just after) - target_file.write(line) - if line == "__lua__\n": - # enter lua section - phase = Phase.LUA_SECTION - - elif phase is Phase.LUA_SECTION: - # Step 5: copy minified lua - target_file.writelines(min_lua_file.readlines()) - target_file.write("\n") # newline required before other sections - phase = Phase.LUA_CATCHUP - - elif phase is Phase.LUA_CATCHUP: - # skip all lines until __gfx__ - if line == "__gfx__\n": - # copy the __gfx__ line itself - target_file.write(line) - phase = Phase.OTHER_SECTIONS - - else: # phase is Phase.CARTRIDGE_HEADER - # Step 6: copy remaining sections - target_file.write(line) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Minify lua code in cartridge.') - parser.add_argument('path', type=str, help='path containing cartridge file to minify') - args = parser.parse_args() - - logging.basicConfig(level=logging.INFO) - print(f"Minifying lua code in {args.path}...") - - minify_lua_in_p8(args.path) - - print(f"Minified lua code in {args.path}") diff --git a/postbuild/test_add_metadata.py b/postbuild/test_add_metadata.py deleted file mode 100644 index b7dc3e9c..00000000 --- a/postbuild/test_add_metadata.py +++ /dev/null @@ -1,75 +0,0 @@ -import unittest -from . import add_metadata - -from os import path -import shutil, tempfile - - -class TestAddMetadata(unittest.TestCase): - - def setUp(self): - # Create a temporary directory - self.test_dir = tempfile.mkdtemp() - - def tearDown(self): - # Remove the directory after the test - shutil.rmtree(self.test_dir) - - def test_add_title_author_info(self): - test_lines = [ - 'pico-8 cartridge // http://www.pico-8.com', - 'version 8', - '__lua__', - 'package={loaded={},_c={}}', - 'package._c["module"]=function()' - ] - expected_new_lines = [ - 'pico-8 cartridge // http://www.pico-8.com', - 'version 16', - '__lua__', - '-- test game', - '-- by tas', - 'package={loaded={},_c={}}', - 'package._c["module"]=function()' - ] - test_filepath = path.join(self.test_dir, 'test.p8') - with open(test_filepath, 'w') as f: - f.write('\n'.join(test_lines)) - add_metadata.add_title_author_info(test_filepath, 'test game', 'tas') - with open(test_filepath, 'r') as f: - self.assertEqual(f.read(), '\n'.join(expected_new_lines)) - - def test_add_label_info(self): - test_lines = [ - 'before', - '__label__', - '0000', - '0000', - '', - '__gff__' - ] - label_lines = [ - '__label__', - '1234', - '5678' - ] - expected_new_lines = [ - 'before', - '__label__', - '1234', - '5678', - '__gff__' - ] - test_filepath = path.join(self.test_dir, 'test.p8') - with open(test_filepath, 'w') as f: - f.write('\n'.join(test_lines)) - label_filepath = path.join(self.test_dir, 'label.p8') - with open(label_filepath, 'w') as f: - f.write('\n'.join(label_lines)) - add_metadata.add_label_info(test_filepath, label_filepath) - with open(test_filepath, 'r') as f: - self.assertEqual(f.read(), '\n'.join(expected_new_lines)) - - -if __name__ == '__main__': - unittest.main() diff --git a/postbuild/test_minify.py b/postbuild/test_minify.py deleted file mode 100644 index 6d4826dd..00000000 --- a/postbuild/test_minify.py +++ /dev/null @@ -1,108 +0,0 @@ -import unittest -from . import minify - -from os import path -import shutil, tempfile - - -class TestMinify(unittest.TestCase): - - def setUp(self): - # Create a temporary directory - self.test_dir = tempfile.mkdtemp() - - def tearDown(self): - # Remove the directory after the test - shutil.rmtree(self.test_dir) - - def test_clean_lua(self): - lua_code = """if true then print("ok") end -if true then - print("ok") -end -if (l[p]==nil) l[p]=package._c[p]() -if (l[p]==nil) l[p]=true - -""" - - expected_clean_lua_code = """if true then print("ok") end -if true then - print("ok") -end -if l[p]==nil then l[p]=package._c[p]() end -if l[p]==nil then l[p]=true end - -""" - lua_filepath = path.join(self.test_dir, 'lua.p8') - clean_lua_filepath = path.join(self.test_dir, 'clean_lua.p8') - with open(lua_filepath, 'w') as l: - l.write(lua_code) - - with open(lua_filepath, 'r') as l, open(clean_lua_filepath, 'w') as cl: - minify.clean_lua(l, cl) - - with open(clean_lua_filepath, 'r') as cl: - self.assertEqual(cl.read(), expected_clean_lua_code) - - def test_inject_minified_lua_in_p8(self): - source_text = """pico-8 cartridge // http://www.pico-8.com -version 8 -__lua__ -package={loaded={},_c={}} -package._c["module"]=function() -require("another_module") -local long_name = 5 -end -__gfx__ -eeeeeeeee5eeeeeeeeee -__label__ -55222222222222222222 -__gff__ -00000000000000000000 -__map__ -45454545eeeeeeeeeeee -__sfx__ -010c00002d340293402d -__music__ -01 00010203 - -""" - - min_lua_code = """package={loaded={},_c={}} package._c["module"]=function()require("another_module")local a=5 end""" - - expected_target_text = """pico-8 cartridge // http://www.pico-8.com -version 8 -__lua__ -package={loaded={},_c={}} package._c["module"]=function()require("another_module")local a=5 end -__gfx__ -eeeeeeeee5eeeeeeeeee -__label__ -55222222222222222222 -__gff__ -00000000000000000000 -__map__ -45454545eeeeeeeeeeee -__sfx__ -010c00002d340293402d -__music__ -01 00010203 - -""" - - source_filepath = path.join(self.test_dir, 'source.p8') - target_filepath = path.join(self.test_dir, 'target.p8') - min_lua_filepath = path.join(self.test_dir, 'min_lua.lua') - with open(source_filepath, 'w') as s: - s.write(source_text) - with open(min_lua_filepath, 'w') as l: - l.write(min_lua_code) - - with open(source_filepath, 'r') as s, open(target_filepath, 'w') as t, open(min_lua_filepath, 'r') as l: - minify.inject_minified_lua_in_p8(s, t, l) - - with open(target_filepath, 'r') as t: - self.assertEqual(t.read(), expected_target_text) - - -if __name__ == '__main__': - unittest.main() diff --git a/prebuild/copy_source_folder.sh b/prebuild/copy_source_folder.sh deleted file mode 100755 index 1da57677..00000000 --- a/prebuild/copy_source_folder.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash -# Copy the source folder content to another (intermediate) location -# $1: source folder (from project root) -# $2: copy output folder (from project root) - -if [[ $# -lt 2 ]] ; then - echo 'copy_source_folder.sh takes 2 params, provided $#: - $1: source folder (from project root) - $2: copy output folder (from project root)' - exit 1 -fi - -. helper/path_helper.sh - -if is_unsafe_path "$1"; then - echo "$0: source folder path is unsafe: '$1'" - exit 1 -fi - -if is_unsafe_path "$2"; then - echo "$0: copy output folder path is unsafe: '$2'" - exit 1 -fi - -# clean any previous output folder and make a new one -# note that rm -r $2/* doesn't work inside a script like this, it will consider file "not found" -rm -rf "$2" -mkdir -p "$2" -# we need to enter the source folder first to avoid copying the source folder itself too -pushd "$1" > /dev/null -# activate recursive ** globbing (will be reset when leaving this script) -# https://stackoverflow.com/questions/9622883/recursive-copy-of-specific-files-in-unix-linux -shopt -s globstar -# copy the source folder content to the output location -# -u will only copy if the timestamp has changed, which is enough to check in our case -cp -u --parents **/*.lua "../$2" && -echo "Copied folder '$1' to '$2'." -popd > /dev/null diff --git a/prebuild/preprocess.py b/prebuild/preprocess.py deleted file mode 100644 index 58fa8451..00000000 --- a/prebuild/preprocess.py +++ /dev/null @@ -1,244 +0,0 @@ -#!/usr/bin/env python3.6 -# -*- coding: utf-8 -*- -import argparse -import logging -import os -import re -from enum import Enum - -# This script applies preprocessing and stripping to the code: -# 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, -# 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. - -# 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', 'assert', 'log', 'itest', 'cheat'], - # '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'], - 'log': ['pico8', 'log'], - 'cheat': ['pico8', 'cheat'], - 'release': ['pico8'] -} - -# 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 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': [], - 'assert': [], - 'itest': [], - 'itest_light': ['assert'], - 'profiler': ['assert'], - 'visual_log': ['assert'], - 'pico8_utest': [], - 'log': ['assert'], - 'cheat': ['assert', 'log'], - 'release': ['assert', 'log', 'warn', 'err'] -} - -# Parsing mode of each individual #if block -class IfBlockMode(Enum): - ACCEPTED = 1 # the condition was true - REFUSED = 2 # the condition was false - IGNORED = 3 # we were inside a false condition so we don't care, we are just waiting for #endif - -# Parsing state machine modes -class ParsingMode(Enum): - ACTIVE = 1 # we are copying each line - IGNORING = 2 # we are ignoring all content in the current if block - -# Regex patterns - -# 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"\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"\s*--#pico8]=*]") - -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 - 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""" - for root, dirs, files in os.walk(dirpath): - for file in files: - if file.endswith(".lua"): - preprocess_file(os.path.join(root, file), config) - -def preprocess_file(filepath, config): - """ - Apply preprocessor directives to a single file, for the given config - - test.lua: - print("always") - --#if log - print("debug") - --#endif - if true: - print("hello") - - >>> preprocess_file('test.lua', 'debug') - - test.lua: - print("always") - print("debug") - if true: - print("hello") - - or - - >>> preprocess_file('test.lua', 'release') - - test.lua: - print("always") - if true: - print("hello") - - """ - with open(filepath, 'r+') as f: - logging.debug(f"Preprocessing file {filepath}...") - preprocessed_lines = preprocess_lines(f, config) - # replace file content (truncate as the new content may be shorter) - f.seek(0) - f.truncate() - f.writelines(preprocessed_lines) - -def preprocess_lines(lines, config): - """ - Apply stripping and preprocessor directives to iterable lines of source code, for the given config - It is possible to pass a file as lines iterator - - """ - if config not in defined_symbols_table: - raise ValueError(f"config '{config}' is not a key in defined_symbols_table ({list(defined_symbols_table.keys())})") - defined_symbols = defined_symbols_table[config] - - preprocessed_lines = [] - - inside_pico8_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)] - current_mode = ParsingMode.ACTIVE # it is ParsingMode.ACTIVE iff if_block_modes_stack is empty or if_block_modes_stack[-1] == IfBlockMode.ACCEPTED - - for line in lines: - # 3. preprocess directives - opt_match = None # if or ifn match depending on which one succeeds, None if both fail - negative_if = False # True if we have #ifn, False else - - if_boundary_match = if_pattern.match(line) - if not if_boundary_match: - if_boundary_match = ifn_pattern.match(line) - if if_boundary_match: - negative_if = True - - if if_boundary_match: - if current_mode is ParsingMode.ACTIVE: - symbol = if_boundary_match.group(1) - # for #if, you need to have symbol defined, for #ifn, you need to have it undefined - if (symbol in defined_symbols) ^ negative_if: - # symbol is defined, so remain active and add that to the stack - if_block_modes_stack.append(IfBlockMode.ACCEPTED) - # still strip the preprocessor directives themselves (don't add it to accepted lines) - else: - # symbol is not defined, enter ignoring mode and add that to the stack - if_block_modes_stack.append(IfBlockMode.REFUSED) - current_mode = ParsingMode.IGNORING - else: - # we are already in an unprocessed block so we don't care whether that subblock verifies the condition or not - # continue ignoring lines but push to the stack so we can wait for #endif - if_block_modes_stack.append(IfBlockMode.IGNORED) - elif endif_pattern.match(line): - if current_mode is ParsingMode.ACTIVE: - # check that we had some #if in the stack - if if_block_modes_stack: - # go one level up, remain active - if_block_modes_stack.pop() - else: - logging.warning('an --#endif was encountered outside an --#if block. Make sure the block starts with an --#if directive') - else: - last_mode = if_block_modes_stack.pop() - # if we left the refusing block, then the new last mode is ACCEPTED and we should be active again - # otherwise, we have simply left an IGNORED mode and we remain IGNORING - if last_mode is IfBlockMode.REFUSED: - current_mode = ParsingMode.ACTIVE - elif current_mode is ParsingMode.ACTIVE: - if pico8_start_pattern.match(line): - # 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 - else: - 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: - inside_pico8_block = False - else: - logging.warning('a pico8 block end was encountered outside a pico8 block. It will be ignored') - else: - 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') - 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 - - -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__': - parser = argparse.ArgumentParser(description='Apply preprocessor directives.') - parser.add_argument('path', type=str, help='path containing source files to preprocess') - parser.add_argument('config', type=str, help="config used: 'debug' or 'release'") - args = parser.parse_args() - - logging.basicConfig(level=logging.INFO) - preprocess_dir(args.path, args.config) - print(f"Preprocessed all files in {args.path} with config {args.config}.") diff --git a/prebuild/replace_strings.py b/prebuild/replace_strings.py index 2d458381..ae5596c4 100755 --- a/prebuild/replace_strings.py +++ b/prebuild/replace_strings.py @@ -236,6 +236,8 @@ def get_substitute(match): # in general, we should substitute all members of a namespace, especially enums 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 + # note that we should normally escape quotes in original_symbol, but we rely on the fact that + # symbols should not contain quotes return f'assert(false, "UNSUBSTITUTED {original_symbol}")' return get_substitute diff --git a/prebuild/test_preprocess.py b/prebuild/test_preprocess.py deleted file mode 100644 index b80b7b93..00000000 --- a/prebuild/test_preprocess.py +++ /dev/null @@ -1,655 +0,0 @@ -import unittest -from . import preprocess - -import logging -from os import path -import shutil, tempfile - - -class TestPreprocessLines(unittest.TestCase): - - 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_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_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_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_is_function_call_to_strip_log_in_release_true(self): - test_line = 'log("character moves", "[character]")\n' - self.assertTrue(preprocess.is_function_call_to_strip(test_line, 'release')) - - def test_is_function_call_to_strip_tricky_bracket(self): - test_line = 'log("inside quotes )", "[character]")\n' - self.assertTrue(preprocess.is_function_call_to_strip(test_line, 'release')) - - def test_is_function_call_to_strip_embedded_brackets(self): - test_line = 'log(value.evaluate(with.style()), "[character]")\n' - self.assertTrue(preprocess.is_function_call_to_strip(test_line, 'release')) - - def test_is_function_call_to_strip_tricky_quotes(self): - test_line = 'log("inside quotes )\\"", "[character]")\n' - 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.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.assertFalse(preprocess.is_function_call_to_strip(test_line, 'release')) - - def test_is_function_call_to_strip_not_alone(self): - test_line = 'log("inside quotes", "[character]") or a = 3\n' - self.assertFalse(preprocess.is_function_call_to_strip(test_line, 'release')) - - def test_is_function_call_to_strip_not_alone2(self): - test_line = 'log("inside quotes )\\"", "[character]") or a = 3\n' - 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_is_function_call_to_strip_not_alone_end_bracket(self): - test_line = 'log("inside quotes )\\"", "[character]") or fancy_side_effect()\n' - self.assertFalse(preprocess.is_function_call_to_strip(test_line, 'release')) - - 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', - ] - self.assertEqual(preprocess.preprocess_lines(test_lines, 'debug'), test_lines) - - def test_preprocess_lines_if_log_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', - '\n', - 'print("debug")\n', - '\n', - 'if true:\n', - ' print("hello") -- prints hello\n', - ] - self.assertEqual(preprocess.preprocess_lines(test_lines, 'debug'), expected_processed_lines) - - def test_preprocess_lines_if_log_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', - '\n', - '\n', - 'if true:\n', - ' print("hello") -- prints hello\n', - ] - self.assertEqual(preprocess.preprocess_lines(test_lines, 'release'), expected_processed_lines) - - def test_preprocess_lines_2nd_if_refused(self): - test_lines = [ - '--#if log\n', - 'print("debug")\n', - '--#if never\n', - 'print("never")\n', - '--#endif\n', - '\n', - 'if true:\n', - ' print("hello") -- prints hello\n', - '--#endif\n', - ] - expected_processed_lines = [ - 'print("debug")\n', - '\n', - 'if true:\n', - ' print("hello") -- prints hello\n', - ] - self.assertEqual(preprocess.preprocess_lines(test_lines, 'debug'), expected_processed_lines) - - def test_preprocess_lines_3rd_if_still_ignore(self): - test_lines = [ - '--#if log\n', - 'print("debug")\n', - '--#if never\n', - 'print("never")\n', - '--#if never\n', - 'print("never2")\n', - '--#endif\n', - 'print("never3")\n', - '--#endif\n', - '\n', - 'if true:\n', - ' print("hello") -- prints hello\n', - '--#endif\n', - ] - expected_processed_lines = [ - 'print("debug")\n', - '\n', - 'if true:\n', - ' print("hello") -- prints hello\n', - ] - self.assertEqual(preprocess.preprocess_lines(test_lines, 'debug'), expected_processed_lines) - - def test_preprocess_lines_3rd_if_ignored_even_if_true(self): - test_lines = [ - '--#if log\n', - 'print("debug")\n', - '--#if never\n', - 'print("never")\n', - '--#if log\n', - 'print("debug2")\n', - '--#endif\n', - 'print("never3")\n', - '--#endif\n', - '\n', - 'if true:\n', - ' print("hello") -- prints hello\n', - '--#endif\n', - ] - expected_processed_lines = [ - 'print("debug")\n', - '\n', - 'if true:\n', - ' print("hello") -- prints hello\n', - ] - self.assertEqual(preprocess.preprocess_lines(test_lines, 'debug'), expected_processed_lines) - - def test_preprocess_lines_ifn(self): - test_lines = [ - 'print("always")\n', - '--#ifn log\n', - 'print("no log")\n', - '--#endif\n', - 'print("hello")\n', - ] - expected_processed_lines = [ - 'print("always")\n', - 'print("hello")\n', - ] - self.assertEqual(preprocess.preprocess_lines(test_lines, 'debug'), expected_processed_lines) - - def test_preprocess_lines_if_and_ifn(self): - test_lines = [ - 'print("always")\n', - '--#if log\n', - 'print("log")\n', - '--#endif\n', - '--#ifn log\n', - 'print("no log")\n', - '--#endif\n', - 'print("hello")\n', - ] - expected_processed_lines = [ - 'print("always")\n', - 'print("log")\n', - 'print("hello")\n', - ] - self.assertEqual(preprocess.preprocess_lines(test_lines, 'debug'), expected_processed_lines) - - def test_preprocess_lines_ifn_inside_if(self): - test_lines = [ - 'print("always")\n', - '--#if log\n', - 'print("log")\n', - '--#ifn log\n', - 'print("no log")\n', - '--#endif\n', - 'print("log 2")\n', - '--#endif\n', - 'print("hello")\n', - ] - expected_processed_lines = [ - 'print("always")\n', - 'print("log")\n', - 'print("log 2")\n', - 'print("hello")\n', - ] - self.assertEqual(preprocess.preprocess_lines(test_lines, 'debug'), expected_processed_lines) - - def test_preprocess_lines_if_inside_ifn(self): - test_lines = [ - 'print("always")\n', - '--#ifn log\n', - 'print("no log")\n', - '--#if log\n', - 'print("log")\n', - '--#endif\n', - 'print("no log 2")\n', - '--#endif\n', - 'print("hello")\n', - ] - expected_processed_lines = [ - 'print("always")\n', - 'print("hello")\n', - ] - self.assertEqual(preprocess.preprocess_lines(test_lines, 'debug'), expected_processed_lines) - - 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', - ] - expected_processed_lines = [ - 'print("always")\n', - 'print("no log")\n', - 'print("hello")\n', - ] - self.assertEqual(preprocess.preprocess_lines(test_lines, 'release'), expected_processed_lines) - - def test_preprocess_lines_immediate_endif_ignored(self): - test_lines = [ - '--#endif\n', - 'if true:\n', - ' print("hello") -- prints hello\n', - ] - expected_processed_lines = [ - 'if true:\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_lines_missing_endif_ignored(self): - test_lines = [ - '--#if log\n', - 'print("debug")\n', - '\n', - 'if true:\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_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', - '--[[#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', - ] - self.assertEqual(preprocess.preprocess_lines(test_lines, 'release'), expected_processed_lines) - - def test_preprocess_lines_stop_pico8_outside_pico8_block(self): - test_lines = [ - '--#pico8]]\n', # warning here, ignored - 'code\n', - ] - expected_processed_lines = [ - 'code\n', - ] - self.assertEqual(preprocess.preprocess_lines(test_lines, 'release'), expected_processed_lines) - - def test_preprocess_lines_refused_if_inside_pico8_block(self): - test_lines = [ - 'print("start")\n', - '--[=[#pico8 pico8 start\n', - 'real pico8 code\n', - '--#if log\n', - 'log only\n', - '--#endif\n', - '--#pico8]=] exceptionally ignored\n', - 'print("end")\n', - ] - expected_processed_lines = [ - 'print("start")\n', - 'real pico8 code\n', - 'print("end")\n', - ] - self.assertEqual(preprocess.preprocess_lines(test_lines, 'release'), expected_processed_lines) - - def test_preprocess_lines_accepted_if_inside_pico8_block(self): - test_lines = [ - 'print("start")\n', - '--[[#pico8 pico8 start\n', - 'real pico8 code\n', - '--#if log\n', - 'log only\n', - '--#endif\n', - '--#pico8]] exceptionally ignored\n', - 'print("end")\n', - ] - expected_processed_lines = [ - 'print("start")\n', - 'real pico8 code\n', - 'log only\n', - 'print("end")\n', - ] - self.assertEqual(preprocess.preprocess_lines(test_lines, 'debug'), expected_processed_lines) - - def test_preprocess_lines_refused_ifn_inside_pico8_block(self): - test_lines = [ - 'print("start")\n', - '--[==[#pico8 pico8 start\n', - 'real pico8 code\n', - '--#ifn log\n', - 'release only\n', - '--#endif\n', - '--#pico8]==] exceptionally ignored\n', - 'print("end")\n', - ] - expected_processed_lines = [ - 'print("start")\n', - 'real pico8 code\n', - 'print("end")\n', - ] - self.assertEqual(preprocess.preprocess_lines(test_lines, 'debug'), expected_processed_lines) - - def test_preprocess_lines_accepted_ifn_inside_pico8_block(self): - test_lines = [ - 'print("start")\n', - '--[[#pico8 pico8 start\n', - 'real pico8 code\n', - '--#ifn log\n', - 'release only\n', - '--#endif\n', - '--#pico8]] exceptionally ignored\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) - - 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_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') - - -class TestPreprocessFile(unittest.TestCase): - - def setUp(self): - # Create a temporary directory - self.test_dir = tempfile.mkdtemp() - - def tearDown(self): - # Remove the directory after the test - shutil.rmtree(self.test_dir) - - def test_preprocess_file_in_debug(self): - 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(test_code) - preprocess.preprocess_file(test_filepath, 'debug') - with open(test_filepath, 'r') as f: - self.assertEqual(f.read(), expected_processed_code) - - def test_preprocess_file_in_release(self): - 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(test_code) - preprocess.preprocess_file(test_filepath, 'release') - with open(test_filepath, 'r') as f: - self.assertEqual(f.read(), expected_processed_code) - -class TestPreprocessDir(unittest.TestCase): - - def setUp(self): - # Create a temporary directory - self.test_dir = tempfile.mkdtemp() - - def tearDown(self): - # Remove the directory after the test - shutil.rmtree(self.test_dir) - - def test_preprocess_dir_in_debug(self): - 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(test_code1) - test_filepath2 = path.join(self.test_dir, 'test2.lua') - with open(test_filepath2, 'w') as f2: - f2.write(test_code2) - preprocess.preprocess_dir(self.test_dir, 'debug') - with open(test_filepath1, 'r') as f1: - self.assertEqual(f1.read(), expected_processed_code1) - with open(test_filepath2, 'r') as f2: - self.assertEqual(f2.read(), expected_processed_code2) - - def test_preprocess_dir_in_debug(self): - 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(test_code1) - test_filepath2 = path.join(self.test_dir, 'test2.lua') - with open(test_filepath2, 'w') as f2: - f2.write(test_code2) - preprocess.preprocess_dir(self.test_dir, 'release') - with open(test_filepath1, 'r') as f1: - self.assertEqual(f1.read(), expected_processed_code1) - with open(test_filepath2, 'r') as f2: - self.assertEqual(f2.read(), expected_processed_code2) - -if __name__ == '__main__': - logging.basicConfig(level=logging.ERROR) - unittest.main() diff --git a/prebuild/test_replace_strings.py b/prebuild/test_replace_strings.py index 91df7e6f..3ed835cf 100644 --- a/prebuild/test_replace_strings.py +++ b/prebuild/test_replace_strings.py @@ -36,7 +36,7 @@ def test_replace_all_symbols_in_string_enum(self): 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') + self.assertEqual(replace_strings.replace_all_symbols_in_string(test_string), 'local c = assert(false, "UNSUBSTITUTED colors.unknown")') def test_replace_all_args_in_string(self): test_string = 'require("itest_$itest")' diff --git a/reload.sh b/reload.sh deleted file mode 100755 index 419ff5a0..00000000 --- a/reload.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -# --sync makes sure the window is active before sending the key, but it gets stuck -# if no matching window is found, so timeout makes sure it doesn't happen -timeout 0.15 xdotool search --sync --class pico8 windowactivate key ctrl+r && -echo "Reloaded pico8" diff --git a/run_game.sh b/run_game.sh new file mode 100755 index 00000000..2c7ae77e --- /dev/null +++ b/run_game.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# Run game.p8 with PICO-8 executable +# Pass any extra arguments to pico8 + +# Configuration: cartridge +cartridge_stem="picosonic" +version="3.0" +config="$1"; shift + +run_cmd="pico8 -run build/${cartridge_stem}_v${version}_${config}.p8 -screenshot_scale 4 -gif_scale 4 $@" + +# Support UNIX platforms without gnome-terminal by checking if the command exists +# If you `reload.sh` the game, the separate terminal allows you to keep watching the program output, +# but depending on your work environment it may not be needed (it is useful with Sublime Text as the output +# panel would get cleared on reload). +# https://stackoverflow.com/questions/592620/how-to-check-if-a-program-exists-from-a-bash-script +if hash gnome-terminal 2>/dev/null; then + # gnome-terminal exists + echo "> gnome-terminal -- bash -x -c \"$run_cmd\"" + gnome-terminal -- bash -x -c "$run_cmd" +else + echo "> $run_cmd" + bash -c "$run_cmd" +fi diff --git a/run_itest.sh b/run_itest.sh new file mode 100755 index 00000000..bd3036e9 --- /dev/null +++ b/run_itest.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Run itest with PICO-8 executable (itests only work in debug config) +# Pass any extra arguments to pico8 + +# Configuration: cartridge +cartridge_stem="picosonic_itest_all" +version="3.0" + +run_cmd="pico8 -run build/${cartridge_stem}_v${version}_debug.p8 -screenshot_scale 4 -gif_scale 4 $@" + +# Support UNIX platforms without gnome-terminal by checking if the command exists +# If you `reload.sh` the game, the separate terminal allows you to keep watching the program output, +# but depending on your work environment it may not be needed (it is useful with Sublime Text as the output +# panel would get cleared on reload). +# https://stackoverflow.com/questions/592620/how-to-check-if-a-program-exists-from-a-bash-script +if hash gnome-terminal 2>/dev/null; then + # gnome-terminal exists + echo "> gnome-terminal -- bash -x -c \"$run_cmd\"" + gnome-terminal -- bash -x -c "$run_cmd" +else + echo "> $run_cmd" + bash -c "$run_cmd" +fi diff --git a/run.sh b/run_legacy.sh similarity index 100% rename from run.sh rename to run_legacy.sh diff --git a/run_pico8_utests.sh b/run_pico8_utests.sh new file mode 100644 index 00000000..a96d4868 --- /dev/null +++ b/run_pico8_utests.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Run itest with PICO-8 executable (itests only work in debug config) +# Pass any extra arguments to pico8 + +# Configuration: cartridge +cartridge_stem="picosonic_pico8_utests_all" +version="3.0" + +run_cmd="pico8_0.1.11g_dev -run build/${cartridge_stem}_v${version}_debug.p8 -screenshot_scale 4 -gif_scale 4 $@" + +# Support UNIX platforms without gnome-terminal by checking if the command exists +# If you `reload.sh` the game, the separate terminal allows you to keep watching the program output, +# but depending on your work environment it may not be needed (it is useful with Sublime Text as the output +# panel would get cleared on reload). +# https://stackoverflow.com/questions/592620/how-to-check-if-a-program-exists-from-a-bash-script +if hash gnome-terminal 2>/dev/null; then + # gnome-terminal exists + echo "> gnome-terminal -- bash -x -c \"$run_cmd\"" + gnome-terminal -- bash -x -c "$run_cmd" +else + echo "> $run_cmd" + bash -c "$run_cmd" +fi diff --git a/sonic-2d-tech-demo.sublime-project b/sonic-2d-tech-demo.sublime-project index b821b306..62ef8b1a 100644 --- a/sonic-2d-tech-demo.sublime-project +++ b/sonic-2d-tech-demo.sublime-project @@ -2,445 +2,462 @@ "build_systems": [ { - "name": "p8tool: build context", + "name": "Dev: analyze engine scripts (intermediate release)", + "shell": true, "working_dir": "${project_path}", + "cmd": ["pico-boots/scripts/analyze.py intermediate/release/pico-boots"], + "file_regex": "\"(.*\\.py)\"(?:, line )([\\d]+)(?:\\:)?([\\d]+)?(.*)", + "syntax": "Packages/Python/Python.sublime-syntax" + }, + { + "name": "Dev: analyze game scripts (intermediate release)", + "shell": true, + "working_dir": "${project_path}", + "cmd": ["pico-boots/scripts/analyze.py intermediate/release/src"], + "file_regex": "\"(.*\\.py)\"(?:, line )([\\d]+)(?:\\:)?([\\d]+)?(.*)", + "syntax": "Packages/Python/Python.sublime-syntax" + }, + { + "name": "Engine: setup", + "working_dir": "${project_path}/pico-boots", "shell": true, - "cmd": ["./build.sh $file_base_name debug"], + "cmd": ["./setup.sh"], + "syntax": "Packages/Python/Python.sublime-syntax", + }, + { + "name": "Game: edit data", + "shell": true, + "cmd": ["./edit_data.sh"], + "working_dir": "${project_path}" + }, + { + "name": "Game: edit metadata", + "shell": true, + "cmd": ["./edit_metadata.sh"], + "working_dir": "${project_path}" + }, + { + "name": "Game: build game", + "working_dir": "${project_path}", + "shell": true, + "cmd": ["./build_game.sh debug"], "file_regex": "(.*\\.sh)(?:\\: line )([\\d]+)(?:\\:)([\\d]+)? (.*)", "syntax": "Packages/Python/Python.sublime-syntax", - "env": { - "BUILD_VERSION": "2.3" - }, "variants": [ { "name": "debug", - "cmd": ["./build.sh $file_base_name debug"] - }, - { - "name": "assert", - "cmd": ["./build.sh $file_base_name assert"] - }, - { - "name": "itest", - "cmd": ["./build.sh $file_base_name itest"] + "cmd": ["./build_game.sh debug"] }, { - "name": "itest_light", - "cmd": ["./build.sh $file_base_name itest_light"] + "name": "debug-ultrafast", + "cmd": ["./build_game.sh debug-ultrafast"] }, { - "name": "profiler", - "cmd": ["./build.sh $file_base_name profiler"] - }, - { - "name": "visual_log", - "cmd": ["./build.sh $file_base_name visual_log"] - }, - { - "name": "pico8_utest", - "cmd": ["./build.sh $file_base_name pico8_utest"] + "name": "cheat", + "cmd": ["./build_game.sh cheat"] }, { - "name": "pico8_utest (--no-minify)", - "cmd": ["./build.sh $file_base_name pico8_utest -m"] + "name": "ultrafast", + "cmd": ["./build_game.sh ultrafast"] }, { - "name": "log", - "cmd": ["./build.sh $file_base_name log"] + "name": "cheat-ultrafast", + "cmd": ["./build_game.sh cheat-ultrafast"] }, { - "name": "cheat", - "cmd": ["./build.sh $file_base_name cheat"] + "name": "sandbox", + "cmd": ["./build_game.sh sandbox"] }, { "name": "release", - "cmd": ["./build.sh $file_base_name release"] - }, + "cmd": ["./build_game.sh release"] + } ] }, { - "name": "p8tool: build and run or reload context", + "name": "Game: build itests", + "working_dir": "${project_path}", + "shell": true, + "cmd": ["./build_itest.sh"], + "file_regex": "(.*\\.sh)(?:\\: line )([\\d]+)(?:\\:)([\\d]+)? (.*)", + "syntax": "Packages/Python/Python.sublime-syntax" + }, + { + "name": "Game: build pico8 utests", "working_dir": "${project_path}", "shell": true, - "cmd": ["./build.sh $file_base_name debug && (./reload.sh || ./run.sh $file_base_name debug)"], + "cmd": ["./build_pico8_utests.sh"], + "file_regex": "(.*\\.sh)(?:\\: line )([\\d]+)(?:\\:)([\\d]+)? (.*)", + "syntax": "Packages/Python/Python.sublime-syntax" + }, + { + "name": "Game: build and reload or run", + "working_dir": "${project_path}", + "shell": true, + "cmd": ["./build_game.sh debug && (./pico-boots/scripts/reload.sh || ./run_game.sh debug)"], "file_regex": "(.*\\.sh)(?:\\: line )([\\d]+)(?:\\:)([\\d]+)? (.*)", "syntax": "Packages/Python/Python.sublime-syntax", - "env": { - "BUILD_VERSION": "2.3" - }, "variants": [ - { - "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)"] - }, - { - "name": "itest_light", - "cmd": ["./build.sh $file_base_name itest_light && (./reload.sh || ./run.sh $file_base_name itest_light)"] + "cmd": ["./build_game.sh debug && (./pico-boots/scripts/reload.sh || ./run_game.sh debug)"] }, { - "name": "profiler", - "cmd": ["./build.sh $file_base_name profiler && (./reload.sh || ./run.sh $file_base_name profiler)"] + "name": "debug-ultrafast", + "cmd": ["./build_game.sh debug-ultrafast && (./pico-boots/scripts/reload.sh || ./run_game.sh debug-ultrafast)"] }, { - "name": "visual_log", - "cmd": ["./build.sh $file_base_name visual_log && (./reload.sh || ./run.sh $file_base_name visual_log)"] - }, - { - "name": "pico8_utest", - "cmd": ["./build.sh $file_base_name pico8_utest && (./reload.sh || ./run.sh $file_base_name pico8_utest '-x; exec bash')"] + "name": "cheat", + "cmd": ["./build_game.sh cheat && (./pico-boots/scripts/reload.sh || ./run_game.sh cheat)"] }, { - "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": "ultrafast", + "cmd": ["./build_game.sh ultrafast && (./pico-boots/scripts/reload.sh || ./run_game.sh ultrafast)"] }, { - "name": "log", - "cmd": ["./build.sh $file_base_name log && (./reload.sh || ./run.sh $file_base_name log)"] + "name": "cheat-ultrafast", + "cmd": ["./build_game.sh cheat-ultrafast && (./pico-boots/scripts/reload.sh || ./run_game.sh cheat-ultrafast)"] }, { - "name": "cheat", - "cmd": ["./build.sh $file_base_name cheat && (./reload.sh || ./run.sh $file_base_name cheat)"] + "name": "sandbox", + "cmd": ["./build_game.sh sandbox && (./pico-boots/scripts/reload.sh || ./run_game.sh sandbox)"] }, { "name": "release", - "cmd": ["./build.sh $file_base_name release && (./reload.sh || ./run.sh $file_base_name release)"] - }, + "cmd": ["./build_game.sh release && (./pico-boots/scripts/reload.sh || ./run_game.sh release)"] + } ] }, { - "name": "p8tool: run context", + "name": "Game: build and reload or run itests", + "working_dir": "${project_path}", + "shell": true, + "cmd": ["./build_itest.sh && (./pico-boots/scripts/reload.sh || ./run_itest.sh)"], + "file_regex": "(.*\\.sh)(?:\\: line )([\\d]+)(?:\\:)([\\d]+)? (.*)", + "syntax": "Packages/Python/Python.sublime-syntax" + }, + { + "name": "Game: build and reload or run pico8 utests", "working_dir": "${project_path}", "shell": true, - "cmd": ["./run.sh $file_base_name debug"], - "env": { - "BUILD_VERSION": "2.3" - }, + "cmd": ["./build_pico8_utests.sh && (./pico-boots/scripts/reload.sh || ./run_pico8_utests.sh)"], + "file_regex": "(.*\\.sh)(?:\\: line )([\\d]+)(?:\\:)([\\d]+)? (.*)", + "syntax": "Packages/Python/Python.sublime-syntax" + }, + { + "name": "Game: start", + "working_dir": "${project_path}", + "shell": true, + "cmd": ["./run_game.sh debug"], + "syntax": "Packages/Python/Python.sublime-syntax", "variants": [ { "name": "debug", - "cmd": ["./run.sh $file_base_name debug"] + "cmd": ["./run_game.sh debug"] }, { - "name": "assert", - "cmd": ["./run.sh $file_base_name assert"] + "name": "debug-ultrafast", + "cmd": ["./run_game.sh debug-ultrafast"] }, { - "name": "itest", - "cmd": ["./run.sh $file_base_name itest"] - }, - { - "name": "itest_light", - "cmd": ["./run.sh $file_base_name itest_light"] + "name": "cheat", + "cmd": ["./run_game.sh cheat"] }, { - "name": "profiler", - "cmd": ["./run.sh $file_base_name profiler"] + "name": "ultrafast", + "cmd": ["./run_game.sh ultrafast"] }, { - "name": "visual_log", - "cmd": ["./run.sh $file_base_name visual_log"] + "name": "cheat-ultrafast", + "cmd": ["./run_game.sh cheat-ultrafast"] }, { - "name": "pico8_utest", - "cmd": ["./run.sh $file_base_name pico8_utest"] + "name": "sandbox", + "cmd": ["./run_game.sh sandbox"] }, { - "name": "log", - "cmd": ["./run.sh $file_base_name log"] + "name": "release", + "cmd": ["./run_game.sh release"] }, { - "name": "cheat", - "cmd": ["./run.sh $file_base_name cheat"] + "name": "itests", + "cmd": ["./run_itest.sh"], }, { - "name": "release", - "cmd": ["./run.sh $file_base_name release"] - }, + "name": "pico8 utests", + "cmd": ["./run_pico8_utests.sh"], + } ] }, { - "name": "p8tool: install game", + "name": "Game: install cartridge (Linux)", "working_dir": "${project_path}", "shell": true, - "cmd": ["./install.sh release"], - "env": { - "BUILD_VERSION": "2.3" - }, + "cmd": ["./install_cartridge_linux.sh debug"], + "syntax": "Packages/Python/Python.sublime-syntax", "variants": [ { "name": "debug", - "cmd": ["./install.sh debug"], - }, - { - "name": "assert", - "cmd": ["./install.sh assert"], - }, - { - "name": "itest", - "cmd": ["./install.sh itest"], + "cmd": ["./install_cartridge_linux.sh debug"] }, { - "name": "itest_light", - "cmd": ["./install.sh itest_light"], + "name": "debug-ultrafast", + "cmd": ["./install_cartridge_linux.sh debug-ultrafast"] }, { - "name": "profiler", - "cmd": ["./install.sh profiler"], - }, - { - "name": "visual_log", - "cmd": ["./install.sh visual_log"], + "name": "cheat", + "cmd": ["./install_cartridge_linux.sh cheat"] }, { - "name": "pico8_utest", - "cmd": ["./install.sh pico8_utest"], + "name": "ultrafast", + "cmd": ["./install_cartridge_linux.sh ultrafast"] }, { - "name": "log", - "cmd": ["./install.sh log"], + "name": "cheat-ultrafast", + "cmd": ["./install_cartridge_linux.sh cheat-ultrafast"] }, { - "name": "cheat", - "cmd": ["./install.sh cheat"], + "name": "sandbox", + "cmd": ["./install_cartridge_linux.sh sandbox"] }, { "name": "release", - "cmd": ["./install.sh release"], + "cmd": ["./install_cartridge_linux.sh release"] }, { "name": "debug (png)", - "cmd": ["./install.sh debug png"], - }, - { - "name": "assert (png)", - "cmd": ["./install.sh assert png"], + "cmd": ["./install_cartridge_linux.sh debug png"] }, { - "name": "itest (png)", - "cmd": ["./install.sh itest png"], + "name": "debug (png)-ultrafast", + "cmd": ["./install_cartridge_linux.sh debug-ultrafast png"] }, { - "name": "itest_light (png)", - "cmd": ["./install.sh itest_light png"], - }, - { - "name": "profiler (png)", - "cmd": ["./install.sh profiler png"], - }, - { - "name": "visual_log (png)", - "cmd": ["./install.sh visual_log png"], + "name": "cheat (png)", + "cmd": ["./install_cartridge_linux.sh cheat png"] }, { - "name": "pico8_utest (png)", - "cmd": ["./install.sh pico8_utest png"], + "name": "ultrafast (png)", + "cmd": ["./install_cartridge_linux.sh ultrafast png"] }, { - "name": "log (png)", - "cmd": ["./install.sh log png"], + "name": "cheat (png)-ultrafast", + "cmd": ["./install_cartridge_linux.sh cheat-ultrafast png"] }, { - "name": "cheat (png)", - "cmd": ["./install.sh cheat png"], + "name": "sandbox (png)", + "cmd": ["./install_cartridge_linux.sh sandbox png"] }, { "name": "release (png)", - "cmd": ["./install.sh release png"], + "cmd": ["./install_cartridge_linux.sh release png"] } ] }, { - "name": "p8tool: edit data", - "shell": true, - "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}", - "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}", - "shell": true, - "cmd": ["python3 -m prebuild.test_preprocess"], - "file_regex": "\"(.*\\.py)\"(?:, line )([\\d]+)(?:\\:)?([\\d]+)?, (.*)", - "syntax": "Packages/Python/Python.sublime-syntax" - }, - { - "name": "python: test replace_strings", + "name": "Game: export cartridge release", "working_dir": "${project_path}", "shell": true, - "cmd": ["python3 -m prebuild.test_replace_strings"], - "file_regex": "\"(.*\\.py)\"(?:, line )([\\d]+)(?:\\:)?([\\d]+)?, (.*)", + "cmd": ["./export_cartridge_release.sh"], "syntax": "Packages/Python/Python.sublime-syntax" }, { - "name": "python: test add_metadata", + "name": "Game: install and export cartridge release (Linux)", "working_dir": "${project_path}", "shell": true, - "cmd": ["python3 -m postbuild.test_add_metadata"], - "file_regex": "\"(.*\\.py)\"(?:, line )([\\d]+)(?:\\:)?([\\d]+)?, (.*)", + "cmd": ["./install_cartridge_linux.sh release && ./export_cartridge_release.sh"], "syntax": "Packages/Python/Python.sublime-syntax" }, { - "name": "python: test minify", + "name": "Game: test", "working_dir": "${project_path}", "shell": true, - "cmd": ["python3 -m postbuild.test_minify"], - "file_regex": "\"(.*\\.py)\"(?:, line )([\\d]+)(?:\\:)?([\\d]+)?, (.*)", - "syntax": "Packages/Python/Python.sublime-syntax" - }, - { - "name": "lua: parse", - "selector": "source.lua,source.p8", - "working_dir": "${project_path}", - "shell": true, - "cmd": ["luac -p $file"], - "file_regex": "((?:src|tests)\\/[-\\w\\/]+\\.lua)(?:\\:| @ )([\\d]+)(?:\\:)?([\\d]+)?\\n?(.*)" - }, - { - "name": "busted: test", - "working_dir": "${project_path}", - "shell": true, - // use current file name to decide what to test - // works from both src module and the corresponding test - "cmd": ["./test.sh $file_base_name"], - // file regex works fine except for last \n which apparently doesn't work in build output - "file_regex": "((?:src|tests)\\/[-\\w\\/]+\\.lua)(?:\\:| @ )([\\d]+)(?:\\:)?([\\d]+)?\\n?(.*)", - "env": { - "SUBLIME_TEXT": "YES" - }, + "cmd": ["./test.sh -f $file_base_name"], + // known limitation: last \n won't be detected in build output, so cannot display multi-line errors + "file_regex": "((?:pico-boots/src|src)\\/[-\\w\\/]+\\.lua)(?:\\:| @ )([\\d]+)(?:\\:)?([\\d]+)?\\n?(.*)", "syntax": "Packages/Python/Python.sublime-syntax", "variants": [ { - "name": "current - solo", - "cmd": ["./test.sh $file_base_name solo"], - }, - { - "name": "current - all", - "cmd": ["./test.sh $file_base_name all"], - }, - { - "name": "all", - "cmd": ["./test.sh all"], - }, - { - "name": "all - solo", - "cmd": ["./test.sh all solo"], - }, - { - "name": "all - all", - "cmd": ["./test.sh all all"], + "name": "(solo)", + "cmd": ["./test.sh -f $file_base_name -m solo"] }, { - "name": "assertions", - "cmd": ["./test.sh assertions"], + "name": "(all)", + "cmd": ["./test.sh -f $file_base_name -m all"] }, { - "name": "class", - "cmd": ["./test.sh class"], + "name": "*", + "cmd": ["./test.sh"] }, { - "name": "codetuner", - "cmd": ["./test.sh codetuner"], + "name": "* (solo)", + "cmd": ["./test.sh -m solo"] }, { - "name": "color", - "cmd": ["./test.sh color"], + "name": "* (all)", + "cmd": ["./test.sh -m all"] }, { - "name": "coroutine", - "cmd": ["./test.sh coroutine"], + "name": "application", + "cmd": ["./test.sh application"], }, { - "name": "credits", - "cmd": ["./test.sh credits"], + "name": "ingame", + "cmd": ["./test.sh ingame"], }, { - "name": "debug", - "cmd": ["./test.sh debug"], - }, - { - "name": "flow", - "cmd": ["./test.sh flow"], + "name": "itest", + "cmd": ["./test.sh itest"], }, { - "name": "helper", - "cmd": ["./test.sh helper"], + "name": "menu", + "cmd": ["./test.sh menu"], }, { - "name": "input", - "cmd": ["./test.sh input"], + "name": "platformer", + "cmd": ["./test.sh platformer"], }, { - "name": "integrationtest", - "cmd": ["./test.sh integrationtest"], + "name": "test_data", + "cmd": ["./test.sh test_data"], }, { - "name": "math", - "cmd": ["./test.sh math"], + "name": "headless_itests", + "cmd": ["./test.sh -f headless_itests"], }, { - "name": "playercharacter", - "cmd": ["./test.sh playercharacter"], + "name": "headless_itests (render)", + "cmd": ["./test.sh -f headless_itests"], + "env": { + "ENABLE_RENDER": "1" + } }, { - "name": "profiler", - "cmd": ["./test.sh profiler"], + "name": "headless_itests - solo", + "cmd": ["./test.sh -f headless_itests -m solo"], }, { - "name": "sprite", - "cmd": ["./test.sh sprite"], - }, + "name": "headless_itests - solo (render)", + "cmd": ["./test.sh -f headless_itests -m solo"], + "env": { + "ENABLE_RENDER": "1" + } + } + ] + }, + { + "name": "Engine: test", + "working_dir": "${project_path}/pico-boots", + "shell": true, + "cmd": ["./test.sh -f $file_base_name"], + // known limitation: last \n won't be detected in build output, so cannot display multi-line errors + "file_regex": "((?:src)\\/[-\\w\\/]+\\.lua)(?:\\:| @ )([\\d]+)(?:\\:)?([\\d]+)?\\n?(.*)", + "syntax": "Packages/Python/Python.sublime-syntax", + "variants": + [ { - "name": "stage", - "cmd": ["./test.sh stage"], + "name": "(solo)", + "cmd": ["./test.sh -f $file_base_name -m solo"] }, { - "name": "titlemenu", - "cmd": ["./test.sh titlemenu"], + "name": "(all)", + "cmd": ["./test.sh -f $file_base_name -m all"] }, { - "name": "ui", - "cmd": ["./test.sh ui"], + "name": "*", + "cmd": ["./test.sh"] }, { - "name": "headless_itests", - "cmd": ["./test.sh headless_itests"], + "name": "* (solo)", + "cmd": ["./test.sh -m solo"] }, { - "name": "headless_itests (render)", - "cmd": ["./test.sh headless_itests --render"], - }, - { - "name": "headless_itests - solo", - "cmd": ["./test.sh headless_itests solo"], - }, + "name": "* (all)", + "cmd": ["./test.sh -m all"] + } + ] + }, + { + "name": "All: test", + "working_dir": "${project_path}", + "shell": true, + // note that luacov report is generated in the working directory, so the second test report will overwrite the previous one + "cmd": ["./test.sh && echo '' && pico-boots/test.sh"], + // known limitation: last \n won't be detected in build output, so cannot display multi-line errors + "file_regex": "((?:pico-boots/src|src)\\/[-\\w\\/]+\\.lua)(?:\\:| @ )([\\d]+)(?:\\:)?([\\d]+)?\\n?(.*)", + "syntax": "Packages/Python/Python.sublime-syntax", + "variants": + [ { - "name": "headless_itests - solo (render)", - "cmd": ["./test.sh headless_itests solo --render"], - }, - ], + // if you aim for 100% coverage including render methods + "name": "(render)", + "cmd": ["./test.sh && echo '' && pico-boots/test.sh"], + "env": { + "ENABLE_RENDER": "1" + } + } + ] + }, + { + "name": "Engine: Postbuild: test add_metadata", + "working_dir": "${project_path}/pico-boots", + "shell": true, + "cmd": ["python3 -m scripts.test_add_metadata"], + "file_regex": "\"(.*\\.py)\"(?:, line )([\\d]+)(?:\\:)?([\\d]+)?(.*)", + "syntax": "Packages/Python/Python.sublime-syntax" + }, + { + "name": "Engine: Postbuild: test add_require", + "working_dir": "${project_path}/pico-boots", + "shell": true, + "cmd": ["python3 -m scripts.test_add_require"], + "file_regex": "\"(.*\\.py)\"(?:, line )([\\d]+)(?:\\:)?([\\d]+)?(.*)", + "syntax": "Packages/Python/Python.sublime-syntax" + }, + { + "name": "Engine: Postbuild: test analyze", + "working_dir": "${project_path}/pico-boots", + "shell": true, + "cmd": ["python3 -m scripts.test_analyze"], + "file_regex": "\"(.*\\.py)\"(?:, line )([\\d]+)(?:\\:)?([\\d]+)?(.*)", + "syntax": "Packages/Python/Python.sublime-syntax" + }, + { + "name": "Engine: Postbuild: test minify", + "working_dir": "${project_path}/pico-boots", + "shell": true, + "cmd": ["python3 -m scripts.test_minify"], + "file_regex": "\"(.*\\.py)\"(?:, line )([\\d]+)(?:\\:)?([\\d]+)?(.*)", + "syntax": "Packages/Python/Python.sublime-syntax" + }, + { + "name": "Engine: Postbuild: test preprocess", + "working_dir": "${project_path}/pico-boots", + "shell": true, + "cmd": ["python3 -m scripts.test_preprocess"], + "file_regex": "\"(.*\\.py)\"(?:, line )([\\d]+)(?:\\:)?([\\d]+)?(.*)", + "syntax": "Packages/Python/Python.sublime-syntax" + }, + { + "name": "Python (legacy): test replace_strings", + "working_dir": "${project_path}", + "shell": true, + "cmd": ["python3 -m prebuild.test_replace_strings"], + "file_regex": "\"(.*\\.py)\"(?:, line )([\\d]+)(?:\\:)?([\\d]+)?, (.*)", + "syntax": "Packages/Python/Python.sublime-syntax" + }, + { + "name": "lua: parse", + "selector": "source.lua,source.p8", + "working_dir": "${project_path}", + "shell": true, + "cmd": ["luac -p $file"], + "file_regex": "((?:src|tests)\\/[-\\w\\/]+\\.lua)(?:\\:| @ )([\\d]+)(?:\\:)?([\\d]+)?\\n?(.*)" }, ], "folders": @@ -459,8 +476,7 @@ ".git", "backup", "build", - "intermediate", - "__pycache__" + "intermediate" ], "path": "." } @@ -469,7 +485,11 @@ { "\\.lua$": [ - "PICO-8/PICO-8" + "sublime-PICO-8/PICO-8" + ], + "\\.luacov": + [ + "Lua/Lua" ] } } diff --git a/src/game/application/picosonic_app.lua b/src/application/picosonic_app.lua similarity index 55% rename from src/game/application/picosonic_app.lua rename to src/application/picosonic_app.lua index a9f16a54..f0537846 100644 --- a/src/game/application/picosonic_app.lua +++ b/src/application/picosonic_app.lua @@ -1,81 +1,89 @@ +-- custom game application +-- used by main and itest_main + +require("engine/application/constants") local gameapp = require("engine/application/gameapp") +require("engine/core/class") +local input = require("engine/input/input") -local flow = require("engine/application/flow") -local gamestate_proxy = require("game/application/gamestate_proxy") -local visual = require("game/resources/visual") +--#if tuner +local codetuner = require("engine/debug/codetuner") +--#endif --#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.3" ---#endif - ---#if visual_logger -local vlogger = require("engine/debug/visual_logger") ---#endif - ---#if tuner -local codetuner = require("engine/debug/codetuner") --#endif --#if profiler local profiler = require("engine/debug/profiler") --#endif +--#if visual_logger +local vlogger = require("engine/debug/visual_logger") +--#endif + --#if mouse local ui = require("engine/ui/ui") --#endif +local titlemenu = require("menu/titlemenu") +local credits = require("menu/credits") +local stage_state = require("ingame/stage_state") +local visual = require("resources/visual") + 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 +function picosonic_app:_init() + gameapp._init(self, fps60) +end + +function picosonic_app:instantiate_gamestates() -- override + return {titlemenu(), credits(), stage_state()} end -function picosonic_app.on_start() -- override --#if mouse +function picosonic_app:on_post_start() -- override + -- enable mouse devkit + input:toggle_mouse(true) ui:set_cursor_sprite_data(visual.sprite_data_t.cursor) ---#endif end +--#endif ---#if itest -function picosonic_app.on_reset() -- override +function picosonic_app:on_reset() -- override --#if mouse ui:set_cursor_sprite_data(nil) --#endif end + +function picosonic_app:on_update() -- override +--#if profiler + profiler.window:update() --#endif -function picosonic_app.on_update() -- override --#if visual_logger vlogger.window:update() --#endif ---#if profiler - profiler.window:update() ---#endif - --#if tuner codetuner:update_window() --#endif end -function picosonic_app.on_render() ---#if visual_logger - vlogger.window:render() ---#endif - +function picosonic_app:on_render() -- override --#if profiler profiler.window:render() --#endif +--#if visual_logger + vlogger.window:render() +--#endif + --#if tuner codetuner:render_window() --#endif --#if mouse + -- always draw cursor on top ui:render_mouse() --#endif end diff --git a/src/game/tests/utestpicosonic_app.lua b/src/application/picosonic_app_utest.lua similarity index 79% rename from src/game/tests/utestpicosonic_app.lua rename to src/application/picosonic_app_utest.lua index 8ba88801..5d179895 100644 --- a/src/game/tests/utestpicosonic_app.lua +++ b/src/application/picosonic_app_utest.lua @@ -1,16 +1,16 @@ require("engine/test/bustedhelper") -local picosonic_app = require("game/application/picosonic_app") +local picosonic_app = require("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 input = require("engine/input/input") 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") +local titlemenu = require("menu/titlemenu") +local credits = require("menu/credits") +local stage_state = require("ingame/stage_state") +local visual = require("resources/visual") describe('picosonic_app', function () @@ -20,38 +20,40 @@ describe('picosonic_app', function () app = picosonic_app() end) - describe('register_gamestates', function () + describe('instantiate_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]) + it('should return all gamestates', function () + assert.are_same({titlemenu(), credits(), stage_state()}, picosonic_app:instantiate_gamestates()) end) end) - describe('on_start', function () + describe('on_post_start', function () setup(function () + stub(input, "toggle_mouse") stub(ui, "set_cursor_sprite_data") end) teardown(function () + input.toggle_mouse:revert() ui.set_cursor_sprite_data:revert() end) after_each(function () + input.toggle_mouse:clear() ui.set_cursor_sprite_data:clear() end) + it('should toggle mouse cursor', function () + app:on_post_start() + local s = assert.spy(input.toggle_mouse) + s.was_called(1) + s.was_called_with(match.ref(input), true) + end) + it('should set the ui cursor sprite data', function () - app.on_start() + app:on_post_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)) @@ -59,7 +61,7 @@ describe('picosonic_app', function () end) - describe('on_reset (#utest only)', function () + describe('on_reset', function () setup(function () stub(ui, "set_cursor_sprite_data") diff --git a/src/game/data/collision_data.lua b/src/data/collision_data.lua similarity index 97% rename from src/game/data/collision_data.lua rename to src/data/collision_data.lua index 84d0eaf9..9d6f55f8 100644 --- a/src/game/data/collision_data.lua +++ b/src/data/collision_data.lua @@ -1,5 +1,5 @@ require("engine/core/math") -local tile = require("game/platformer/tile") +local tile = require("platformer/tile") local tile_data = tile.tile_data sprite_flags = { diff --git a/src/game/data/playercharacter_data.lua b/src/data/playercharacter_data.lua similarity index 54% rename from src/game/data/playercharacter_data.lua rename to src/data/playercharacter_data.lua index 341d2071..3389e09d 100644 --- a/src/game/data/playercharacter_data.lua +++ b/src/data/playercharacter_data.lua @@ -1,3 +1,4 @@ +require("engine/render/color") local sprite_data = require("engine/render/sprite_data") local animated_sprite_data = require("engine/render/animated_sprite_data") @@ -41,12 +42,20 @@ local playercharacter_data = { -- see comment in player_char:_next_ground_step on last block ground_sensor_extent_x = 2.5, - -- height between the character center and the ground sensors, i.e. the height of the character sprite center (0 when the center is at the bottom pixel level) + -- height between the standing character center and the ground sensors, i.e. the height of the character sprite center (0 when the center is at the bottom pixel level) center_height_standing = 8, - -- height between the ground sensors and the top of the character's collider (used to detect ceiling) + -- height between the ground sensors and the top of the standing character's collider (used to detect ceiling) + -- should be 2 * center_height_standing, but left as separate data for customization (e.g. you can add 1 as in the SPG) full_height_standing = 16, + -- same as center_height_standing but when character is crouching, rolling or jumping + center_height_compact = 4, + + -- same as full_height_standing but when character is crouching, rolling or jumping + -- should be 2 * center_height_compact, but left as separate data for customization (e.g. you can add 1 as in the SPG) + full_height_compact = 8, + -- 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 @@ -72,29 +81,29 @@ local playercharacter_data = { -- stand right sonic_sprite_data_table = { - 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)), + ["idle"] = sprite_data(sprite_id_location(0, 8), tile_vector(2, 2), vector(11, 8), colors.pink), + ["run1"] = sprite_data(sprite_id_location(2, 8), tile_vector(2, 2), vector(11, 8), colors.pink), + ["run2"] = sprite_data(sprite_id_location(4, 8), tile_vector(2, 2), vector(11, 8), colors.pink), + ["run3"] = sprite_data(sprite_id_location(6, 8), tile_vector(2, 2), vector(11, 8), colors.pink), + ["run4"] = sprite_data(sprite_id_location(8, 8), tile_vector(2, 2), vector(11, 8), colors.pink), + ["run5"] = sprite_data(sprite_id_location(10, 8), tile_vector(2, 2), vector(11, 8), colors.pink), + ["run6"] = sprite_data(sprite_id_location(12, 8), tile_vector(2, 2), vector(11, 8), colors.pink), + ["run7"] = sprite_data(sprite_id_location(14, 8), tile_vector(2, 2), vector(11, 8), colors.pink), + ["run8"] = sprite_data(sprite_id_location(0, 10), tile_vector(2, 2), vector(11, 8), colors.pink), + ["run9"] = sprite_data(sprite_id_location(2, 10), tile_vector(2, 2), vector(11, 8), colors.pink), + ["run10"] = sprite_data(sprite_id_location(4, 10), tile_vector(2, 2), vector(11, 8), colors.pink), + ["run11"] = sprite_data(sprite_id_location(6, 10), tile_vector(2, 2), vector(11, 8), colors.pink), + ["spin"] = sprite_data(sprite_id_location(0, 12), tile_vector(2, 2), vector(5, 5), colors.pink), }, } -- 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"] = animated_sprite_data.create(playercharacter_data.sonic_sprite_data_table, {"idle"}, 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), + ["spin"] = animated_sprite_data.create(playercharacter_data.sonic_sprite_data_table, {"spin"}, 10, true), } return playercharacter_data diff --git a/src/game/data/stage_data.lua b/src/data/stage_data.lua similarity index 94% rename from src/game/data/stage_data.lua rename to src/data/stage_data.lua index c0e09915..979f8010 100644 --- a/src/game/data/stage_data.lua +++ b/src/data/stage_data.lua @@ -1,4 +1,4 @@ -local audio = require("game/resources/audio") +local audio = require("resources/audio") return { diff --git a/src/game/data/tile_data.lua b/src/data/tile_data.lua similarity index 87% rename from src/game/data/tile_data.lua rename to src/data/tile_data.lua index 086fbb52..3e7806ac 100644 --- a/src/game/data/tile_data.lua +++ b/src/data/tile_data.lua @@ -1,8 +1,8 @@ -- 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") +--#if busted +local tile_test_data = require("test_data/tile_test_data") --#endif tile_symbol_to_ids = { @@ -17,13 +17,13 @@ tile_symbol_to_ids = { -- the map since we would need to store a backup of the original map -- and we don't care, since each itest will build its own mock map function setup_map_data() ---#ifn pico8 +--#if busted tile_test_data.setup() --#endif end function teardown_map_data() ---#ifn pico8 +--#if busted tile_test_data.teardown() --#endif end diff --git a/src/engine/application/constants.lua b/src/engine/application/constants.lua deleted file mode 100644 index 6848742d..00000000 --- a/src/engine/application/constants.lua +++ /dev/null @@ -1,7 +0,0 @@ -screen_width = 128 -screen_height = 128 - -tile_size = 8 - -fps = 60 -delta_time = 1 / fps diff --git a/src/engine/application/flow.lua b/src/engine/application/flow.lua deleted file mode 100644 index 7d6e891e..00000000 --- a/src/engine/application/flow.lua +++ /dev/null @@ -1,83 +0,0 @@ -require("engine/core/class") ---#if log -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 --- next_state gamestates next gamestate, nil if no transition expected -local flow = singleton(function (self) - -- parameters - self.gamestates = {} - - -- state vars - self.curr_state = nil - self.next_state = nil -end) - -function flow:update() - self:_check_next_state() - if self.curr_state then - self.curr_state:update() - end -end - -function flow:render() - if self.curr_state then - self.curr_state:render() - end -end - --- add a gamestate --- currently, we are not asserting if gamestate has already been added, --- as there are some places in utests that add the same gamestate twice, --- but it would definitely be cleaner -function flow:add_gamestate(gamestate) - assert(gamestate ~= nil, "flow:add_gamestate: passed gamestate is nil") - self.gamestates[gamestate.type] = gamestate -end - --- query a new gamestate -function flow:query_gamestate_type(gamestate_type) - assert(gamestate_type ~= nil, "flow:query_gamestate_type: passed gamestate_type is nil") - assert(self.curr_state == nil or self.curr_state.type ~= gamestate_type, "flow:query_gamestate_type: cannot query the current gamestate type '"..gamestate_type.."' itself") - self.next_state = self.gamestates[gamestate_type] - assert(self.next_state ~= nil, "flow:query_gamestate_type: gamestate type '"..gamestate_type.."' has not been added to the flow gamestates") -end - --- check if a new gamestate was queried, and enter it if so -function flow:_check_next_state(gamestate_type) - if self.next_state then - self:_change_state(self.next_state) - end -end - --- enter a new gamestate -function flow:_change_state(new_gamestate) - assert(new_gamestate ~= nil, "flow:_change_state: cannot change to nil gamestate") - if self.curr_state then - self.curr_state:on_exit() - end - self.curr_state = new_gamestate - new_gamestate:on_enter() - self.next_state = nil -- clear any gamestate query -end - ---#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") - self:_change_state(self.gamestates[gamestate_type]) -end ---#endif - --- export -return flow diff --git a/src/engine/application/gameapp.lua b/src/engine/application/gameapp.lua deleted file mode 100644 index e8169fb7..00000000 --- a/src/engine/application/gameapp.lua +++ /dev/null @@ -1,98 +0,0 @@ -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/core/class.lua b/src/engine/core/class.lua deleted file mode 100644 index 5878ee7e..00000000 --- a/src/engine/core/class.lua +++ /dev/null @@ -1,184 +0,0 @@ -require("engine/core/helper") - --- generic new metamethod (requires _init method) -local function new(cls, ...) - local self = setmetatable({}, cls) -- cls as instance metatable - self:_init(...) - return self -end - --- generic concat metamethod (requires _tostring method on tables) -local function concat(lhs, rhs) ---[[#pico8 ---#ifn log - -- caution: concat cannot be used as log symbol is not defined for this config - return tostr(lhs)..tostr(rhs) ---#endif ---#pico8]] ---#if log - return stringify(lhs)..stringify(rhs) ---#endif -end - --- metatable and memberwise equality comparison with usual equality operator --- (shallow or deep depending on override) --- return true iff tables have the same metatable and their members are equal -local function struct_eq(lhs, rhs) - -- we *must* compare by raw content to avoid infinite recursion on __eq, so we pass true as 3rd arg - -- we also re-enable defined equality at deeper levels to make sure the struct type matches in embedded attributes - -- so we pass true as 4th argument (this also prevents infinite recursion when forgetting match.ref on a struct in a busted spy test) - return getmetatable(lhs) == getmetatable(rhs) and are_same(lhs, rhs, true, true) -end - --- 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(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(self) do - if type(value) == 'table' then ---#if assert - assert(type(value.copy) == 'function', "value "..stringify(value).. - " is a table member of a struct but it doesn't have expected copy method, so it's not a struct itself") ---#endif - -- deep copy the struct member itself. never use circular references - -- between structs or you'll get an infinite recursion - copied[key] = value:copy() - else - copied[key] = value - end - end - - 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() --- note that most .__eq() definitions are only duck-typing lhs and rhs, --- so we can compare two instances of different classes (maybe related by inheritance) --- with the same members. slicing will occur when comparing a base instance --- and a derived instance with more members. add a class type member to simulate rtti --- and make sure only objects of the same class are considered equal (but we often don't need this) --- we recommend using a struct for simple structures, as they implement __eq automatically -function new_class() - local class = {} - class.__index = class -- 1st class as instance metatable - class.__concat = concat - - setmetatable(class, { - __call = new - }) - - return class -end - --- create and return a derived class from a base class --- you must override _init and call base_class._init(self, ...) appropriately -function derived_class(base_class) - local derived = {} - derived.__index = derived - derived.__concat = concat - - setmetatable(derived, { - __index = base_class, - __call = new - }) - - return derived -end - --- create a new struct, which is like a class with member-wise equality -function new_struct() - local struct = {} - struct.__index = struct -- 1st struct as instance metatable - struct.__concat = concat - struct.__eq = struct_eq - struct.copy = copy - struct.copy_assign = copy_assign - - setmetatable(struct, { - __call = new - }) - - return struct -end - --- create and return a derived struct from a base struct, redefining metamethods for this level -function derived_struct(base_struct) - local derived = {} - derived.__index = derived - derived.__concat = concat - derived.__eq = struct_eq - - setmetatable(derived, { - __index = base_struct, - __call = new - }) - - return derived -end - --- create a new singleton from an init method, which can also be used as reset method in unit tests --- the singleton is at the same time a class and its own instance -function singleton(init) - local s = {} - setmetatable(s, { - __concat = concat - }) - s.init = init - s:init() - return s -end - --- create a singleton from a base singleton and an optional derived_init method, so it can extend --- the functionality of a singleton while providing new static fields on the spot --- derived_init should *not* call base_singleton.init, as it is already done in the construct-time init -function derived_singleton(base_singleton, derived_init) - local ds = {} - -- do not set __index to base_singleton in metatable, so ds never touches the members - -- of the base singleton (if the base singleton is concrete or has other derived singletons, - -- this would cause them to all share and modify the same members) - setmetatable(ds, { - -- __index allows the derived_singleton to access base_singleton methods - -- never define an attribute on a singleton outside init (e.g. using s.attr = value) - -- as the "super" init in ds:init would not be able to shadow that attr with a personal attr - -- for the derived_singleton, which would access the base_singleton's attr via __index, - -- effectively sharing the attr with all the other singletons in that hierarchy! - __index = base_singleton, - __concat = concat - }) - function ds:init() - base_singleton.init(self) - if derived_init then - derived_init(self) - end - end - ds:init() - return ds -end diff --git a/src/engine/core/coroutine.lua b/src/engine/core/coroutine.lua deleted file mode 100644 index 32ce5a4d..00000000 --- a/src/engine/core/coroutine.lua +++ /dev/null @@ -1,16 +0,0 @@ -require("engine/core/class") - -coroutine_curry = new_class() - --- coroutine thread coroutine created with cocreate --- ... any arguments to pass to coresume -function coroutine_curry:_init(coroutine, ...) - self.coroutine = coroutine - self.args = {...} -- almost a lua table.pack, just without n = select("#", ...) -end - ---#if log -function coroutine_curry:_tostring() - return "[coroutine_curry] ("..costatus(self.coroutine)..") ("..joinstr_table(", ", self.args)..")" -end ---#endif diff --git a/src/engine/core/datastruct.lua b/src/engine/core/datastruct.lua deleted file mode 100644 index a099be35..00000000 --- a/src/engine/core/datastruct.lua +++ /dev/null @@ -1,101 +0,0 @@ --- circular buffer implementation. useful to represent fixed size queue --- 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 --- max_length int max length of the buffer --- state vars --- _buffer [any] data content, with length at most max_length --- _start_index into index of the oldest inserted entry, and where the circular buffer semantically starts -function circular_buffer:_init(max_length) - assert(max_length >= 1, "circular_buffer:_init: max_length must be a positive integer") - self.max_length = max_length - self._buffer = {} - self._start_index = 1 -end - ---#if log -function circular_buffer:_tostring() - return "circular_buffer("..self.max_length..", {"..joinstr_table(", ", self._buffer).."})" -end ---#endif - --- two circular buffers are considered equal when they have the same max length, --- the same buffer size and their reordered content is the same --- this means two circular buffers may be equal if their contents are circularly --- the same, but the start index is different --- if you want to compare a buffer content to a sequence of arbitrary size, --- you'll need to examine self._buffer itself -function circular_buffer.__eq(lhs, rhs) - if not (getmetatable(lhs) == getmetatable(rhs) and - lhs.max_length == rhs.max_length and - #lhs._buffer == #rhs._buffer) then - return false - end - for i = 1, #lhs._buffer do - -- rely on get to rotate correctly - if lhs:get(i) ~= rhs:get(i) then - return false - end - end - return true -end - -function circular_buffer:__len() - return #(self._buffer) -end - -function circular_buffer:__ipairs() - -- return iterator function, table, and starting point - return self._stateless_iter, self, 0 -end - -function circular_buffer:_stateless_iter(i) - i = i + 1 - local v = self:get(i) - if v then return i, v end -end - -function circular_buffer._rotate_indice(i, n) - return ((i - 1) % n) + 1 -end - --- positive values index from oldest to newest, in normal sense (starting with 1) --- negative values index from newest to oldest, in reverse sense (starting with -1) -function circular_buffer:get(i) - local history_length = #(self._buffer) - if i == 0 or math.abs(i) > history_length then - return nil - elseif i > 0 then - local i_rotated = self._rotate_indice(self._start_index - 1 + i, history_length) - return self._buffer[i_rotated] - else -- i < 0 - -- i is increasing in the negative sense, so it's really +i - local i_rotated = self._rotate_indice(self._start_index + i, history_length) - return self._buffer[i_rotated] - end -end - -function circular_buffer:is_filled() - return #self._buffer == self.max_length -end - --- push a new element to the buffer and return true iff an old element was replaced -function circular_buffer:push(value) - if self:is_filled() then - local value_to_be_removed = self._buffer[self._start_index] - self._buffer[self._start_index] = value - self._start_index = self._start_index == self.max_length and 1 or self._start_index + 1 - return true - else - self._buffer[#(self._buffer) + 1] = value - return false - end -end - -function circular_buffer:clear(value) - clear_table(self._buffer) - self._start_index = 1 -end diff --git a/src/engine/core/helper.lua b/src/engine/core/helper.lua deleted file mode 100644 index a14d2527..00000000 --- a/src/engine/core/helper.lua +++ /dev/null @@ -1,274 +0,0 @@ -require("engine/application/constants") - - --- create an enum from a sequence of variant names -function enum(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 = {} - for value in all(t) do - add(transformed_t, func(value)) - end - return transformed_t -end - --- 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 -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) - for k, v in pairs(t) do - return false - end - return true -end - --- return true if t1 and t2 have the same recursive content: --- - if t1 and t2 are tables, if they have the same keys and values, --- if compare_raw_content is false, table values with __eq method are compared by ==, --- but tables without __eq are still compared by content --- if compare_raw_content is true, tables are compared by pure content, as in busted assert.are_same --- however, keys are still compared with == --- (simply because it's more complicated to check all keys for deep equality, and rarely useful) --- - else, if they have the same values (if different types, it will return false) --- 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, check meta __eq - - 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 - - -- we must compare keys and values - - -- first iteration: check that all keys of t1 are in t2, with the same value - for k1, v1 in pairs(t1) do - local v2 = t2[k1] - if v2 == nil then - -- t2 misses key k1 that t1 has - return false - end - if not are_same(v1, v2, compare_raw_content and not no_deep_raw_content) then - return false - end - end - - -- second iteration: check that all keys of t2 are in t1. don't check values, it has already been done - for k2, _ in pairs(t2) do - if t1[k2] == nil then - -- t1 misses key k2 that t2 has - return false - end - end - return true -end - --- clear a table -function clear_table(t) - for k in pairs(t) do - t[k] = nil - end -end - --- unpack from munpack at https://gist.github.com/josefnpat/bfe4aaa5bbb44f572cd0 -function unpack(t, from, to) - from = from or 1 - to = to or #t - if from > to then return end - 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 - --- 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 - if sub(val, 1, 1) == '-' then - local abs_num = tonum(sub(val, 2)) - assert(abs_num, "could not parse absolute part of number: '-"..sub(val, 2).."'") - return - abs_num - else - local num = tonum(val) - assert(num, "could not parse number: '"..val.."'") - return num - end -end - ---#if log - -function stringify(value) - if type(value) == 'table' and value._tostring then - return value:_tostring() - else - return tostr(value) - end -end - --- concatenate a sequence of strings or stringables with a separator --- embedded nil values won't be ignored, but nils at the end will be -function joinstr_table(separator, args) - local n = #args - - local joined_string = "" - - -- iterate by index instead of for all, so we don't skip nil values - -- and #n (which counts nil values) match the used index - for index = 1, n do - joined_string = joined_string..stringify(args[index]) - if index < n then - joined_string = joined_string..separator - end - end - - return joined_string -end - --- variadic version -function joinstr(separator, ...) - return joinstr_table(separator, {...}) -end - ---#endif - --- https://pastebin.com/NS8rxMwH --- converted to clean lua, adapted coding style --- changed behavior: --- - avoid adding next line if first word of line is too long --- - don't add trailing space at end of line --- - don't add eol at the end of the last line --- - count the extra separator before next word in the line length prediction test --- i kept the fact that we don't collapse spaces so 2x, 3x spaces are preserved - --- word wrap (string, char width) -function wwrap(s,w) - local retstr = "" - local lines = strspl(s, "\n") - local nb_lines = count(lines) - - for i = 1, nb_lines do - local linelen = 0 - local words = strspl(lines[i], " ") - local nb_words = count(words) - - for k = 1, nb_words do - local wrd = words[k] - local should_wrap = false - - if k > 1 then - -- predict length after adding 1 separator + next word - if linelen + 1 + #wrd > w then - -- wrap - retstr = retstr.."\n" - linelen = 0 - should_wrap = true - else - -- don't wrap, so add space after previous word if not the first one - retstr = retstr.." " - linelen = linelen + 1 - end - end - - retstr = retstr..wrd - linelen = linelen + #wrd - - if k < nb_words and not should_wrap then - end - end - - -- wrap following \n already there - if i < nb_lines then - retstr = retstr.."\n" - end - end - - return retstr -end - --- port of lua string.split(string, separator) --- separator must be only one character --- added parameter collapse: --- if true, collapse consecutive separators into a big one --- if false or nil, handle each separator separately, --- adding an empty string between each consecutive pair --- ex1: strspl("|a||b", "|") => {"", "a", "", "b"} --- ex2: strspl("|a||b", "|", true) => {"a", "b"} -function strspl(s,sep,collapse) - local ret = {} - local buffer = "" - - for i = 1, #s do - if sub(s, i, i) == sep then - if #buffer > 0 or not collapse then - add(ret, buffer) - buffer = "" - end - else - buffer = buffer..sub(s,i,i) - end - end - if buffer ~= "" then - add(ret, buffer) - end - return ret -end - --- wait for [time]s. only works if you update your coroutines each frame. -function yield_delay(delay) - local nb_frames = fps * delay - -- we want to continue the coroutine as soon as the last frame - -- has been reached, so we don't want to yield the last time, hence -1 - -- in addition, if nb_frames is fractional we want to wait for the last frame - -- to be fully completed, hence ceil - for frame = 1, ceil(nb_frames) - 1 do - yield() - end -end diff --git a/src/engine/core/math.lua b/src/engine/core/math.lua deleted file mode 100644 index ae0152f8..00000000 --- a/src/engine/core/math.lua +++ /dev/null @@ -1,348 +0,0 @@ -require("engine/core/class") - --- numeric helpers - -function almost_eq(lhs, rhs, eps) - eps = eps or 0.01 - 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 - return lhs:almost_eq(rhs, eps) - else - assert(false, "almost_eq cannot compare "..lhs.." and "..rhs) - end -end - --- geometry/data grid helpers - --- tile_vector struct: a pair of integer coords (i, j) that represents a position --- on either a spritesheet or a tilemap of 8x8 squares (8 is the "tile size") --- for sprite locations and tilemap locations, use sprite_id_location and location resp. --- for sprite span (sprite size on the spritesheet), use tile_vector directly -tile_vector = new_struct() - --- i int horizontal coordinate in tile size --- j int vertical coordinate in tile size -function tile_vector:_init(i, j) - self.i = i - self.j = j -end - ---#if log -function tile_vector:_tostring() - return "tile_vector("..self.i..", "..self.j..")" -end ---#endif - --- return the topleft position corresponding to a tile location -function tile_vector:to_topleft_position() - return vector(8 * self.i, 8 * self.j) -end - --- sprite location is a special tile_vector with the semantics of a spritesheet location --- and associated conversion methods -sprite_id_location = derived_struct(tile_vector) - ---#if log -function sprite_id_location:_tostring() - return "sprite_id_location("..self.i..", "..self.j..")" -end ---#endif - --- return the sprite id corresponding to a sprite location on a spritesheet -function sprite_id_location:to_sprite_id() - return 16 * self.j + self.i -end - - --- location is a special tile_vector with the semantics of a tilemap location --- and associated conversion methods -location = derived_struct(tile_vector) - ---#if log -function location:_tostring() - return "location("..self.i..", "..self.j..")" -end ---#endif - --- return the center position corresponding to a tile location -function location:to_center_position() - return vector(8 * self.i + 4, 8 * self.j + 4) -end - - --- vector struct: a pair of pixel coordinates (x, y) that represents a 2d vector --- in the space (position, displacement, speed, acceleration...) -vector = new_struct() - --- x int horizontal coordinate in pixels --- y int vertical coordinate in pixels -function vector:_init(x, y) - self.x = x - self.y = y -end - ---#if log -function vector:_tostring() - return "vector("..self.x..", "..self.y..")" -end ---#endif - --- almost_eq can be used as static function of method, since self would simply replace lhs -function vector.almost_eq(lhs, rhs, eps) - assert(getmetatable(lhs) == vector and getmetatable(rhs) == vector, "vector.almost_eq: lhs and rhs are not both vectors (lhs: "..dump(lhs)..", rhs: "..dump(rhs)..")") - return almost_eq(lhs.x, rhs.x, eps) and almost_eq(lhs.y, rhs.y, eps) -end - -function vector.__add(lhs, rhs) - assert(getmetatable(lhs) == vector and getmetatable(rhs) == vector, "vector.__add: lhs and rhs are not both vectors (lhs: "..dump(lhs)..", rhs: "..dump(rhs)..")") - return vector(lhs.x + rhs.x, lhs.y + rhs.y) -end - --- in-place operation as native lua replacements for pico-8 += -function vector:add_inplace(other) - self.x = self.x + other.x - self.y = self.y + other.y -end - -function vector.__sub(lhs, rhs) - assert(getmetatable(lhs) == vector and getmetatable(rhs) == vector, "vector.__sub: lhs and rhs are not both vectors (lhs: "..dump(lhs)..", rhs: "..dump(rhs)..")") - return vector(lhs.x - rhs.x, lhs.y - rhs.y) -end - --- in-place operation as native lua replacements for pico-8 -= -function vector:sub_inplace(other) - self.x = self.x - other.x - self.y = self.y - other.y -end - -function vector.__unm(v) - return vector(-v.x, -v.y) -end - -function vector.__mul(lhs, rhs) - if type(lhs) == "number" then - return vector(lhs * rhs.x, lhs * rhs.y) - elseif type(rhs) == "number" then - return vector(rhs * lhs.x, rhs * lhs.y) - else ---#if assert - assert(false, "vector multiplication is only supported with a scalar, ".. - "tried to multiply "..lhs:_tostring().." and "..rhs:_tostring()) ---#endif - end -end - --- in-place operation as native lua replacements for pico-8 *= -function vector:mul_inplace(number) - local product = self * number - self.x = product.x - self.y = product.y -end - -function vector.__div(lhs, rhs) - if type(rhs) == "number" then - assert(rhs ~= 0, "cannot divide vector "..lhs:_tostring().." by zero") - return vector(lhs.x / rhs, lhs.y / rhs) - else ---#if assert - assert(false, "vector division is only supported with a scalar as rhs, ".. - "tried to multiply "..stringify(lhs).." and "..rhs) ---#endif - end -end - --- in-place operation as native lua replacements for pico-8 /= -function vector:div_inplace(number) - local product = self / number - self.x = product.x - self.y = product.y -end - -function vector.zero() - return vector(0, 0) -end - -function vector:is_zero() - return self.x == 0 and self.y == 0 -end - -function vector:sqr_magnitude() - return self.x ^ 2 + self.y ^ 2 -end - -function vector:magnitude() - return sqrt(self:sqr_magnitude()) -end - --- return a normalized vector is non-zero, else a zero vector -function vector:normalized() - local magnitude = self:magnitude() - if magnitude > 0 then - return self / magnitude - else - return vector.zero() - end -end - --- normalize vector in-place -function vector:normalize() - local magnitude = self:magnitude() - if magnitude > 0 then - self.x = self.x / magnitude - self.y = self.y / magnitude - end -end - --- return copy of vector with magnitude clamped by max_magnitude -function vector:with_clamped_magnitude(max_magnitude) - assert(max_magnitude >= 0) - local magnitude = self:magnitude() - if magnitude > max_magnitude then - return max_magnitude * self / magnitude - end - return self -end - --- clamp magnitude in-place -function vector:clamp_magnitude(max_magnitude) - assert(max_magnitude >= 0) - local magnitude = self:magnitude() - if magnitude > max_magnitude then - self.x = self.x * max_magnitude / magnitude - self.y = self.y * max_magnitude / magnitude - end -end - --- return copy of vector with magnitude clamped by max_magnitude in cardinal directions -function vector:with_clamped_magnitude_cardinal(max_magnitude_x, max_magnitude_y) - -- if 1 arg is passed, use the same max for x and y - max_magnitude_y = max_magnitude_y or max_magnitude_x - assert(max_magnitude_x >= 0 and max_magnitude_y >= 0) - return vector(mid(-max_magnitude_x, self.x, max_magnitude_x), mid(-max_magnitude_y, self.y, max_magnitude_y)) -end - --- clamp magnitude in cardinal directions in-place -function vector:clamp_magnitude_cardinal(max_magnitude_x, max_magnitude_y) - -- if 1 arg is passed, use the same max for x and y - max_magnitude_y = max_magnitude_y or max_magnitude_x - assert(max_magnitude_x >= 0 and max_magnitude_y >= 0) - self.x = mid(-max_magnitude_x, self.x, max_magnitude_x) - self.y = mid(-max_magnitude_y, self.y, max_magnitude_y) -end - --- mirror the vector horizontally in-place -function vector:mirror_x() - self.x = -self.x -end - --- mirror the vector vertically in-place -function vector:mirror_y() - self.y = -self.y -end - --- return copy of vector rotated by 90 degrees clockwise (for top-left origin) -function vector:rotated_90_cw() - return vector(-self.y, self.x) -end - --- rotate vector by 90 degrees clockwise in-place -function vector:rotate_90_cw_inplace() - local old_x = self.x - self.x = -self.y - self.y = old_x -end - --- return copy of vector rotated by 90 degrees counter-clockwise (for top-left origin) -function vector:rotated_90_ccw() - return vector(self.y, -self.x) -end - --- rotate by 90 degrees counter-clockwise in-place -function vector:rotate_90_ccw_inplace() - local old_x = self.x - self.x = self.y - self.y = -old_x -end - --- return the tile location containing this vector position (non-injective) -function vector:to_location() - return location(flr(self.x / tile_size), flr(self.y / tile_size)) -end - --- enums data - ---#ifn pico8 -directions = { - left = 0, - up = 1, - right = 2, - down = 3 -} ---#endif - -dir_vectors = { - [0] = vector(-1., 0.), - vector(0., -1.), - vector(1., 0.), - vector(0., 1.) -} - --- we are not stripping this enum as we need dynamic string-to-value --- conversion for itest dsl; we don't need it for normal build --- though, so when 'or' is supported in preprocessing, it will --- be better to surround this in --#if ~pico8 or itest -horizontal_dirs = { - left = 1, - right = 2 -} - -horizontal_dir_vectors = { - vector(-1., 0.), -- to left - vector(1., 0.) -- to right -} - -horizontal_dir_signs = { - -1, -- left sign - 1 -- right sign -} - --- return left if signed speed is negative, right if positive. ub unless signed speed is not 0 -function signed_speed_to_dir(signed_speed) - assert(signed_speed ~= 0) - return signed_speed < 0 and horizontal_dirs.left or horizontal_dirs.right -end - -function oppose_dir(direction) - return (direction + 2) % 4 -end - -function mirror_dir_x(direction) - if direction == directions.left then - return directions.right - elseif direction == directions.right then - return directions.left - else - return direction - end -end - -function mirror_dir_y(direction) - if direction == directions.up then - return directions.down - elseif direction == directions.down then - return directions.up - else - return direction - end -end - -function rotate_dir_90_cw(direction) - return (direction + 1) % 4 -end - -function rotate_dir_90_ccw(direction) - return (direction - 1) % 4 -end diff --git a/src/engine/data/tilemap.lua b/src/engine/data/tilemap.lua deleted file mode 100644 index 8ea292e5..00000000 --- a/src/engine/data/tilemap.lua +++ /dev/null @@ -1,33 +0,0 @@ -require("engine/core/class") - -local tilemap = new_struct() - --- content {{int}} 2-dimensional sequence of tile ids, by row, then column -function tilemap:_init(content) - self.content = content -end - --- load the content into the current map -function tilemap:load(content) - tilemap.clear_map() - for i = 1, #self.content do - local row = self.content[i] - for j = 1, #row do - mset(j - 1, i - 1, row[j]) - end - 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/debug/codetuner.lua b/src/engine/debug/codetuner.lua deleted file mode 100644 index d95a9cd9..00000000 --- a/src/engine/debug/codetuner.lua +++ /dev/null @@ -1,156 +0,0 @@ ---#if tuner - -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 - - -- if true, tuned values are used, else default values are used - self.active = false - - -- state vars - - -- table of tuned variables, identified by their names - self.tuned_vars = {} - - -- gui - self.gui = nil - self.main_panel = nil -end) - --- utilities from widget toolkit demo - --- return a new position on the right of a widget w at position (x, y), of width w, plus a margin dist -function codetuner.next_to(w, dist) - return w.x+w.w+(dist or 2), w.y -end - --- return a new position below a widget w at position (x, y), of height h, plus a margin dist -function codetuner.below(w, dist) - return w.x, w.y+w.h+(dist or 2) -end - --- 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_struct() - --- name string tuned variable identifier --- default_value any value used for tuned variable if codetuner is inactive -function codetuner.tuned_variable:_init(name, default_value) - self.name = name - self.default_value = default_value -end - --- return a string with format: tuned_variable "{name}" (default: {default_value}) -function codetuner.tuned_variable:_tostring(name, default_value) - return "tuned_variable \""..self.name.."\" (default: "..self.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) - return function (spinner) - self:set_tuned_var(tuned_var_name, spinner.value) - end -end - --- if codetuner is active, retrieve tuned var or create a new one with default value if needed --- if codetuner is inactive, return default value -function codetuner:get_or_create_tuned_var(name, default_value) - if self.active then - -- booleans may be used, so always compare to nil - if self.tuned_vars[name] == nil then - self:create_tuned_var(name, default_value) - end - return self.tuned_vars[name] - else - return default_value - end -end - -function codetuner:create_tuned_var(name, default_value) - self.tuned_vars[name] = default_value - - -- register to ui - local tuning_spinner = wtk.spinner.new(-100, 100, default_value, 1, self:get_spinner_callback(name)) - local next_pos_x, next_pos_y - if #self.main_panel.children > 0 then - next_pos_x, next_pos_y = codetuner.below(self.main_panel.children[#self.main_panel.children]) - else - next_pos_x, next_pos_y = 1, 1 - end - self.main_panel:add_child(tuning_spinner, next_pos_x, next_pos_y) -end - --- set tuned variable, even if codetuner is inactive --- fails with warning if name doesn't exist -function codetuner:set_tuned_var(name, value) - if self.tuned_vars[name] ~= nil then - self.tuned_vars[name] = value - else - warn("codetuner:set_tuned_var: no tuned var found with name: "..tostr(name), "codetuner") - end -end - --- short global alias for codetuner:get_or_create_tuned_var -function tuned(name, default_value) - return codetuner:get_or_create_tuned_var(name, default_value) -end - -function codetuner:show() - self.gui.visible = true -end - -function codetuner:hide() - self.gui.visible = false -end - -function codetuner:init_window() - self.gui = wtk.gui_root.new() - self.gui.visible = false - self.main_panel = wtk.panel.new(1, 1, colors.dark_gray, true) - self.gui:add_child(self.main_panel) -end - -function codetuner:update_window() - self.gui:update() -end - -function codetuner:render_window() - self.gui:draw() -end - --- always initialize window on start so we can add widgets for tuned variables --- at any time, even if the window is not shown -codetuner:init_window() - ---#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/engine/debug/debug_window.lua b/src/engine/debug/debug_window.lua deleted file mode 100644 index f42797f7..00000000 --- a/src/engine/debug/debug_window.lua +++ /dev/null @@ -1,35 +0,0 @@ -require("engine/core/class") -local wtk = require("engine/wtk/pico8wtk") - --- base class for debug windows --- usage: derive from debug_window and implement on_init --- create gui root, invisible --- gui gui_root root of the debug window gui -debug_window = singleton(function (self) - self.gui = wtk.gui_root.new() - self.gui.visible = false -end) - -function debug_window:show() - self.gui.visible = true -end - -function debug_window:hide() - self.gui.visible = false -end - -function debug_window:update() - self.gui:update() -end - -function debug_window:render() - camera() - self.gui:draw() -end - -function debug_window:add_label(text, color, x, y) - local label = wtk.label.new(text, color) - self.gui:add_child(label, x, y) -end - -return debug_window diff --git a/src/engine/debug/logging.lua b/src/engine/debug/logging.lua deleted file mode 100644 index 3534ef94..00000000 --- a/src/engine/debug/logging.lua +++ /dev/null @@ -1,287 +0,0 @@ ---#if log - -require("engine/core/class") -require("engine/core/helper") - -local logging = { - level = { - info = 1, -- show all messages - warning = 2, -- show warnings and errors - error = 3, -- show errors only - none = 4, -- show nothing - } -} - --- log message struct -local log_msg = new_struct() -logging.log_msg = log_msg - --- level logging.level importance level of the message --- text string textual content --- category string category in which the message belongs to (see logger.active_categories) -function log_msg:_init(level, category, text) - self.level = level - self.category = category - self.text = text -end - ---#if log -function log_msg:_tostring() - return "log_msg("..joinstr(", ", self.level, dump(self.category), dump(self.text))..")" -end ---#endif - -function logging.compound_message(lm) - if lm.level == logging.level.warning then - prefix = "warning: " - elseif lm.level == logging.level.error then - prefix = "error: " - else - prefix = "" - end - return "["..lm.category.."] "..prefix..lm.text -end - --- log stream abstract singleton --- active boolean is the stream active? is false, all output is muted --- log function(self, lm: log_msg) external callback on log message received --- on_log function(self, lm: log_msg) internal callback on log message received, only called if active -local log_stream = singleton(function (self) - self.active = true -end) -logging.log_stream = log_stream - -function log_stream:log(lm) - if self.active then - self:on_log(lm) - end -end - --- abstract --- function log_stream:on_log() --- end - - --- console log -console_log_stream = derived_singleton(log_stream) -logging.console_log_stream = console_log_stream - -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: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") -end - - -local logger = singleton(function (self) - self.active_categories = { - default = true, - flow = true, - player = true, - ui = true, - codetuner = true, - itest = true, - -- trace is considered a category, not a level, so we can toggle it independently from the rest - trace = false - } - self.current_level = logging.level.info - self.dump_max_recursion_level = 5 - - -- streams to log to - self._streams = {} -end) - --- export -logging.logger = logger - --- set all categories active flag to false to mute logging -function logger:deactivate_all_categories() - for category, _ in pairs(self.active_categories) do - self.active_categories[category] = false - end -end - --- register a stream toward which logging will be sent (console, file...) -function logger:register_stream(stream) - assert(stream, "logger:register_stream: passed stream is nil") - assert(type(stream.on_log) == "function" or type(stream.on_log) == "table" and getmetatable(stream.on_log).__call, "logger:register_stream: passed stream is invalid: on_log member is nil or not a callable") - 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 - local lm = log_msg(level, category, stringify(content)) - for stream in all(self._streams) do - stream:log(lm) - end - end -end - --- print an info content to the console in a category string -function log(content, category) - logger:_generic_log(logging.level.info, category, content) -end - --- print a warning content to the console in a category string -function warn(content, category) - logger:_generic_log(logging.level.warning, category, content) -end - --- print an error content to the console in a category string -function err(content, category) - logger:_generic_log(logging.level.error, category, content) -end - ---[[ -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) - if as_key == nil then - as_key = false - end - - level = level or logger.dump_max_recursion_level - - if use_tostring == nil then - use_tostring = false - end - - if sorted_keys == nil then - sorted_keys = false - end - - local repr - - if type(dumped_value) == "table" then - if use_tostring and dumped_value._tostring then - repr = dumped_value:_tostring() - else - if level > 0 then - local entries = {} - 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).."}" - else - -- we already surround with [], so even if as_key, don't add extra [] - return "[table]" - end - end - else - -- for most types - repr = tostr(dumped_value) - end - - -- non-string keys must be surrounded with [] (only once), string values with "" - if as_key and type(dumped_value) ~= "string" and sub(repr, 1, 1) ~= "[" then - repr = "["..repr.."]" - elseif not as_key and type(dumped_value) == "string" then - repr = "\""..repr.."\"" - end - - return repr -end - --- dump using _tostring method when possible -function nice_dump(value, sorted_keys) - return dump(value, false, nil, true, sorted_keys) -end - -return logging - ---#endif diff --git a/src/engine/debug/profiler.lua b/src/engine/debug/profiler.lua deleted file mode 100644 index 4fc843f7..00000000 --- a/src/engine/debug/profiler.lua +++ /dev/null @@ -1,58 +0,0 @@ ---#if profiler - -require("engine/core/class") -require("engine/render/color") -local debug_window = require("engine/debug/debug_window") - -local profiler = {} - -local stats_info = { - {"memory", 0}, - {"total cpu", 1}, - {"system cpu", 2}, - {"fps", 7}, - {"target fps", 8}, - {"actual fps", 9} -} - --- in order to align all stat values, we will draw them after the longest --- stat name (+ a small margin) -local max_stat_name_length = 0 -for stat_info in all(stats_info) do - local stat_name = stat_info[1] - max_stat_name_length = max(max_stat_name_length, #stat_name) -end -local stat_value_char_offset = max_stat_name_length + 1 - --- return a callback function to use for stat labels --- exposed via profiler for testing only -function profiler.get_stat_function(stat_index) - return function() - local stat_info = stats_info[stat_index] - local stat_name = stat_info[1] - -- pad stat name with spaces until it reaches a fixed length for stat value alignment - local space_padding_size = stat_value_char_offset - #stat_name - local space_padding = "" - for i = 1, space_padding_size do - space_padding = space_padding.." " - end - -- example: "total cpu 0.032" - return stat_name..space_padding..stat(stat_info[2]) - end -end - -profiler.stat_functions = {} -for i = 1, #stats_info do - profiler.stat_functions[i] = profiler.get_stat_function(i) -end - -profiler.window = derived_singleton(debug_window, function (self) - -- add stat labels to draw with their text callbacks - for i = 1, #stats_info do - self:add_label(profiler.stat_functions[i], colors.light_gray, 1, 1 + 6*(i-1)) -- aligned vertically - end -end) - -return profiler - ---#endif diff --git a/src/engine/debug/visual_logger.lua b/src/engine/debug/visual_logger.lua deleted file mode 100644 index 3691546d..00000000 --- a/src/engine/debug/visual_logger.lua +++ /dev/null @@ -1,57 +0,0 @@ ---#if visual_logger - -require("engine/core/class") -require("engine/core/datastruct") -require("engine/render/color") -local debug_window = require("engine/debug/debug_window") -local logging = require("engine/debug/logging") -local wtk = require("engine/wtk/pico8wtk") - -local vlogger = { - buffer_size = 5 -} - -vlogger.window = derived_singleton(debug_window, function (self) - -- fixed size queue of logger messages - self._msg_queue = circular_buffer(vlogger.buffer_size) - -- vertical layout of log messages - self.v_layout = wtk.vertical_layout.new(10, colors.dark_blue) - self.gui:add_child(self.v_layout, 0, 98) -end) - --- push a log_msg lm to the visual log --- caveat: the queue has a fixed size of messages rather than lines --- so when the queue is full, full multiline messages will pop out although --- in a normal console log, we would expect the lines to go out of view 1 by 1 -function vlogger.window:push_msg(lm) - local has_replaced = self._msg_queue:push(logging.log_msg(lm.level, lm.category, lm.text)) - - self:_on_msg_pushed(lm) - if has_replaced then - self:_on_msg_popped() - end -end - --- add a new label to the vertical layout -function vlogger.window:_on_msg_pushed(lm) - local wrapped_text = wwrap(lm.text, 32) - local log_label = wtk.label.new(wrapped_text, colors.white) - self.v_layout:add_child(log_label) -end - --- remove the oldest label of the vertical layout -function vlogger.window:_on_msg_popped() - assert(#self.v_layout.children >= 1, "vlogger.window:_on_msg_popped: no children in window.v_layout") - self.v_layout:remove_child(self.v_layout.children[1]) -end - -local vlog_stream = derived_singleton(logging.log_stream) -vlogger.vlog_stream = vlog_stream - -function vlog_stream:on_log(lm) - vlogger.window:push_msg(lm) -end - -return vlogger - ---#endif diff --git a/src/engine/input/input.lua b/src/engine/input/input.lua deleted file mode 100644 index d15fb920..00000000 --- a/src/engine/input/input.lua +++ /dev/null @@ -1,199 +0,0 @@ -require("engine/core/math") - -button_ids = { - left = 0, - right = 1, - up = 2, - down = 3, - o = 4, - x = 5 -} - ---#ifn pico8 -btn_states = { - released = 0, - just_pressed = 1, - pressed = 2, - just_released = 3 -} - -input_modes = { - native = 0, -- use pico8 input (or pico8api for utests) - simulated = 1 -- use hijacking simulated input -} ---#endif - -local input = { - mode = input_modes.native, -- current input mode - mouse_active = false, -- is the mouse active? - simulated_buttons_down = {} -- mimic pico8 btn() data for simulated mode only -} - --- fill simulated_buttons_down with false values. compressed form equivalent to: --- simulated_buttons_down = { --- [0] = { --- [button_ids.left] = false, --- [button_ids.right] = false, --- [button_ids.up] = false, --- [button_ids.down] = false, --- [button_ids.o] = false, --- [button_ids.x] = false --- }, --- [1] = { --- [button_ids.left] = false, --- [button_ids.right] = false, --- [button_ids.up] = false, --- [button_ids.down] = false, --- [button_ids.o] = false, --- [button_ids.x] = false --- } --- } -for i = 0, 1 do - local t = {} - for i = 0, 5 do - t[i] = false - end - input.simulated_buttons_down[i] = t -end - -local mouse_devkit_address = 0x5f2d -local cursor_x_stat = 32 -local cursor_y_stat = 33 - - --- generate the initial player_btn_states table for a player -function generate_initial_btn_states() - -- compressed form equivalent to: - -- return { - -- [button_ids.left] = btn_states.released, - -- [button_ids.right] = btn_states.released, - -- [button_ids.up] = btn_states.released, - -- [button_ids.down] = btn_states.released, - -- [button_ids.o] = btn_states.released, - -- [button_ids.x] = btn_states.released - -- } - local t = {} - for i = 0, 5 do - t[i] = btn_states.released - end - return t -end - --- player_btn_states tables, indexed by played ID -input.players_btn_states = { - [0] = generate_initial_btn_states(), - [1] = generate_initial_btn_states() -} - ---#if mouse - --- activate mouse devkit -function input:toggle_mouse(active) - if active == nil then - -- no argument => reverse value - active = not self.mouse_active - end - value = active and 1 or 0 - self.mouse_active = active - poke(mouse_devkit_address, value) -end - --- return the current cursor position -function input.get_cursor_position() - return vector(stat(cursor_x_stat), stat(cursor_y_stat)) -end - ---#endif - --- return a button state for player id (0 by default) -function input:get_button_state(button_id, player_id) - assert(type(button_id) == "number" and button_id >= 0 and button_id < 6, "input:get_button_state: button_id ("..tostr(button_id)..") is not between 0 and 5") - player_id = player_id or 0 - return self.players_btn_states[player_id][button_id] -end - --- return true if button is released or just released for player id (0 by default) -function input:is_up(button_id, player_id) - local button_state = self:get_button_state(button_id, player_id) - return button_state == btn_states.released or button_state == btn_states.just_released -end - --- return true if button is pressed or just pressed for player id (0 by default) -function input:is_down(button_id, player_id) - return not self:is_up(button_id, player_id) -end - --- return true if button is just released for player id (0 by default) -function input:is_just_released(button_id, player_id) - local button_state = self:get_button_state(button_id, player_id) - return button_state == btn_states.just_released -end - --- return true if button is just pressed for player id (0 by default) -function input:is_just_pressed(button_id, player_id) - local button_state = self:get_button_state(button_id, player_id) - return button_state == btn_states.just_pressed -end - --- update button states for each player based on previous and current button states -function input:process_players_inputs() - for player_id = 0, 1 do - self:_process_player_inputs(player_id) - end -end - --- update button states for a specific player based on previous and current button states -function input:_process_player_inputs(player_id) - local player_btn_states = self.players_btn_states[player_id] - for button_id, _ in pairs(player_btn_states) do - if self.mode == input_modes.native then - -- note that btnp should always return true when just pressed, but the reverse is not true because pico8 - -- has a repeat input feature, that we are not reproducing ---#if assert - assert(player_btn_states[button_id] ~= btn_states.released and player_btn_states[button_id] ~= btn_states.just_released or - not btn(button_id, player_id) or btnp(button_id, player_id), "input:_update_button_state: button "..button_id.." was released and is now pressed, but btnp("..button_id..") returns false") ---#endif - end - player_btn_states[button_id] = self:_compute_next_button_state(player_btn_states[button_id], self:_btn_proxy(button_id, player_id)) - end -end - --- return true if the button is considered down by the current low-level i/o: native or simulated -function input:_btn_proxy(button_id, player_id) - if self.mode == input_modes.native then - return btn(button_id, player_id) - else -- self.mode == input_modes.simulated - player_id = player_id or 0 - return self.simulated_buttons_down[player_id][button_id] - end -end - --- return the next button state of a button based on its previous dynamic state (stored) and current static state (pico8 input) -function input:_compute_next_button_state(previous_button_state, is_down) - if previous_button_state == btn_states.released then - if is_down then - return btn_states.just_pressed - end - elseif previous_button_state == btn_states.just_pressed then - if is_down then - return btn_states.pressed - else - return btn_states.just_released - end - elseif previous_button_state == btn_states.pressed then - if not is_down then - return btn_states.just_released - end - else -- previous_button_state == btn_states.just_released - if is_down then - return btn_states.just_pressed - else - return btn_states.released - end - end - - -- no change detected - return previous_button_state -end - -return input diff --git a/src/engine/physics/collision.lua b/src/engine/physics/collision.lua deleted file mode 100644 index 3a4d68f2..00000000 --- a/src/engine/physics/collision.lua +++ /dev/null @@ -1,148 +0,0 @@ -require("engine/application/constants") -require("engine/core/math") - -local collision = {} - --- axis-aligned bounding box struct -local aabb = new_struct() -collision.aabb = aabb - --- center vector center of the box --- extents vector half-size of the box. components must be positive or zero -function aabb:_init(center, extents) - self.center = center - self.extents = extents -end - ---#if log -function aabb:_tostring() - return "aabb("..self.center..", "..self.extents..")" -end ---#endif - --- mirror the aabb horizontally in-place -function aabb:mirror_x() - self.center:mirror_x() -end - --- mirror the aabb vertically in-place -function aabb:mirror_y() - self.center:mirror_y() -end - --- return copy of aabb rotated by 90 degrees clockwise --- extents coordinates are swapped to match the new orientation -function aabb:rotated_90_cw() - local swapped_extents = vector(self.extents.y, self.extents.x) - return aabb(self.center:rotated_90_cw(), swapped_extents) -end - --- rotate by 90 degrees clockwise in-place --- extents coordinates are swapped to match the new orientation -function aabb:rotate_90_cw_inplace() - local rotated_aabb = self:rotated_90_cw() - self.center = rotated_aabb.center - self.extents = rotated_aabb.extents -end - --- return copy of aabb rotated by 90 degrees counter-clockwise --- extents coordinates are swapped to match the new orientation -function aabb:rotated_90_ccw() - local swapped_extents = vector(self.extents.y, self.extents.x) - return aabb(self.center:rotated_90_ccw(), swapped_extents) -end - --- rotate by 90 degrees counter-clockwise in-place --- extents coordinates are swapped to match the new orientation -function aabb:rotate_90_ccw_inplace() - local rotated_aabb = self:rotated_90_ccw() - self.center = rotated_aabb.center - self.extents = rotated_aabb.extents -end - --- helper function for collides, touches and intersects, that return a couple (signed_distance, escape_vector) --- where signed_distance is the signed distance between this aabb and another one (negative when boxes are intersecting), --- for a given prioritized escape direction, the escape direction won't be used by calling methods if signed_distance > 0, --- but it would still have the meaning of the direction in which bb1 should go to go even further away from bb2 -function aabb:_compute_signed_distance_and_escape_direction(other, prioritized_escape_direction) - local self_left = self.center.x - self.extents.x - local self_right = self.center.x + self.extents.x - local self_top = self.center.y - self.extents.y - local self_bottom = self.center.y + self.extents.y - local other_left = other.center.x - other.extents.x - local other_right = other.center.x + other.extents.x - local other_top = other.center.y - other.extents.y - local other_bottom = other.center.y + other.extents.y - - -- in convex polygon theory, we estimate the distance between polygons with a "phi-function" which is evaluated as: - -- (maximum when swapping polygons A and B of - -- (maximum when iterating over A's edges of - -- (minimum when iterating over B's vertices of - -- signed distance of B's vertex from A's edge where A's edge positive side is oriented toward the outside of A))) - -- for aabb, it's much simpler since all sides are aligned and we know what edge coordinate differences are lower than others (e.g. other_left - self_right < other_left - self_left) - -- besides, we don't have to swap the box' roles since the distances edge-vertex are really just edge-edge distances, which are symmetrical - -- so we use a much simplified operation. however, unlike phi-function we only compute the escape vector while iterating - - -- table of lowest signed distances between edge of box 1 and edge of box 2, indexed by potential escape direction (if that distance is negative) - local min_signed_edge_to_edge_distances = { - [0] = other_left - self_right, -- 0: left - other_top - self_bottom, -- 1: up - self_left - other_right, -- 2: right - self_top - other_bottom -- 3: down - } - - -- find max of the signed distances, while defining the associated escape vector - local max_signed_distance = - math.huge - local best_escape_direction = nil - for escape_direction, signed_distance in pairs(min_signed_edge_to_edge_distances) do - -- check prioritized_escape_direction in case of equality (in which case only the 2nd assignment in the block is useful) - if signed_distance > max_signed_distance or signed_distance == max_signed_distance and prioritized_escape_direction == escape_direction then - max_signed_distance = signed_distance - -- only set escape_vector if the boxes projected on this axis are intersecting (they still may not intersect if they are separate in the other axis) - -- note that if we replace abs(signed_distance) with - signed_distance, we get a generic formula for a motion vector - -- that will ensure both boxes are just touching (escape when signed_distance < 0, come to contact if signed_distance > 0) - best_escape_direction = escape_direction - end - end - - return max_signed_distance, best_escape_direction -end - --- return escape_vector if aabb and other's interiors are intersecting --- where escape_vector is the minimal motion in magnitude that this aabb can do --- to escape collision (leaving it in a touching state with other). --- if multiple escape vectors with the same magnitude exist, an arbitrary one is chosen --- else return nil --- if optional prioritized_escape_direction is set, in case of draw between the smallest escape vectors, the prioritized_escape_direction is chosen --- note that classic sonic uses an ultimate direction where sonic would be pushed in the opposite direction of his last motion --- to escape a collider, even if it meant moving of a much longer distance that in other directions (this is a known exploit for speedruns) --- but we prefer picking the smallest escape vector whatever --- if some aabb extents has a 0 component, it is treated with a very thin or small box, not a no-collision -function aabb:compute_escape_vector(other, prioritized_escape_direction) - signed_distance, escape_direction = self:_compute_signed_distance_and_escape_direction(other, prioritized_escape_direction) - if signed_distance < 0 then - return abs(signed_distance) * dir_vectors[escape_direction] - else - return nil - end -end - -function aabb:collides(other) - signed_distance, _ = self:_compute_signed_distance_and_escape_direction(other, nil) - return signed_distance < 0 -end - --- return true iff aabb and other's boundaries are intersection but their interiors are not --- if some aabb extents has a 0 component, it is treated with a very thin or small box, not a no-touch -function aabb:touches(other) - signed_distance, _ = self:_compute_signed_distance_and_escape_direction(other, nil) - return signed_distance == 0 -end - --- return true iff aabb and other's boundaries or interiors are intersecting -function aabb:intersects(other) - signed_distance, _ = self:_compute_signed_distance_and_escape_direction(other, nil) - return signed_distance <= 0 -end - -return collision diff --git a/src/engine/render/animated_sprite.lua b/src/engine/render/animated_sprite.lua deleted file mode 100644 index 5128f281..00000000 --- a/src/engine/render/animated_sprite.lua +++ /dev/null @@ -1,99 +0,0 @@ --- 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_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 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 -end - ---#if log -function animated_sprite:_tostring() - 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 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, 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_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 - 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 --- 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 - -- 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 = 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 = self.local_frame - anim_spr_data.step_frames - else - -- stop playing - self.playing = false - break - end - end - end - end -end - --- render the current sprite data with passed arguments -function animated_sprite: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/src/engine/render/animated_sprite_data.lua b/src/engine/render/animated_sprite_data.lua deleted file mode 100644 index 1311278f..00000000 --- a/src/engine/render/animated_sprite_data.lua +++ /dev/null @@ -1,34 +0,0 @@ --- 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) - assert(#sprites > 0) - assert(step_frames > 0) - self.sprites = sprites - self.step_frames = step_frames - if looping == nil then - looping = false - end - self.looping = looping -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)..")" -end ---#endif - -return animated_sprite_data diff --git a/src/engine/render/color.lua b/src/engine/render/color.lua deleted file mode 100644 index 625903a9..00000000 --- a/src/engine/render/color.lua +++ /dev/null @@ -1,57 +0,0 @@ --- default pico-8 colors - ---#ifn pico8 -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 -} ---#endif - ---#if log -color_strings = { - [0] = "black", - "dark_blue", - "dark_purple", - "dark_green", - "brown", - "dark_gray", - "light_gray", - "white", - "red", - "orange", - "yellow", - "green", - "blue", - "indigo", - "pink", - "peach" -} - -function color_tostring(colour) - return color_strings[colour] or "unknown color" -end ---#endif - --- set colour as the only transparent color -function set_unique_transparency(colour) - -- reset any previous transparency change - palt() - -- default color (black) is not transparent anymore - palt(0, false) - -- new transparency - palt(colour, true) -end diff --git a/src/engine/render/sprite_data.lua b/src/engine/render/sprite_data.lua deleted file mode 100644 index f0436e08..00000000 --- a/src/engine/render/sprite_data.lua +++ /dev/null @@ -1,52 +0,0 @@ -require("engine/application/constants") -require("engine/core/math") -require("engine/render/color") - --- sprite struct -sprite_data = new_struct() - --- id_loc sprite_id_location sprite location on the spritesheet --- span tile_vector tile_vector(1, 1) sprite span on the spritesheet --- pivot vector (0, 0) reference center to draw (top-left is (0 ,0)) -function sprite_data:_init(id_loc, span, pivot) - self.id_loc = id_loc - self.span = span or tile_vector(1, 1) - self.pivot = pivot or vector.zero() -end - ---#if log -function sprite_data:_tostring() - return "sprite_data("..joinstr(", ", self.id_loc, self.span, self.pivot)..")" -end ---#endif - --- draw this sprite at position, optionally flipped --- position vector --- flip_x bool --- flip_y bool -function sprite_data:render(position, flip_x, flip_y) - set_unique_transparency(colors.pink) - - 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, - flip_x, flip_y) -end - -return sprite_data diff --git a/src/engine/test/assertions.lua b/src/engine/test/assertions.lua deleted file mode 100644 index 5dc25ba6..00000000 --- a/src/engine/test/assertions.lua +++ /dev/null @@ -1,47 +0,0 @@ ---#if log - -require("engine/core/math") - -function contains(t, searched_value) - for key, value in pairs(t) do - if value == searched_value then - return true - end - end - return false -end - --- custom assertions to extend luaassert in utests and provide assertion with messages in itests - -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) - 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) - 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 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) - end -end - ---#endif diff --git a/src/engine/test/bustedhelper.lua b/src/engine/test/bustedhelper.lua deleted file mode 100644 index fde86f1e..00000000 --- a/src/engine/test/bustedhelper.lua +++ /dev/null @@ -1,55 +0,0 @@ --- required module for all tests -require("engine/test/pico8api") -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" - --- 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) - -- level 0 is the C getinfo function - -- level 1 is this line, which is useless - -- level 2 is the line calling get_file_line, which often interests us - -- if an intermediate function calls get_file_line, we add extra levels to reach the first function of interest (non-helper) - extra_level = extra_level or 0 - local debug_info = debug.getinfo(2 + extra_level) - return debug_info.source..":"..debug_info.currentline -end - -function print_at_line(message) - print(get_file_line(1)..": "..message) -end - --- utest history prefix symbol explanations --- in order to track the efficiency of my utests, I add symbols in the it('...') to remember what effect they had on development --- for every occurrence, I add an extra symbol --- I also use the symbols for pico8 utests and Python tests --- from worst to best case: --- - the test was hard to write, but didn't help me to spot bug during implementation --- ~ the test didn't take long to write but it adds a lot of time to the test run and didn't help find bugs --- ? the test passed, but later I realized it was wrong, and passed because both the code and expected were wrong but matching (no big loss of time though) --- ?? same, but big loss of time, as the bug later appeared in a failed itest with unexpected behavior --- . the test failed because the test itself was wrong, so I fixed it --- .. the test used to pass but failed at some point because of behavior subtleties requiring a more precise test --- 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 --- ! 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/src/engine/test/integrationtest.lua b/src/engine/test/integrationtest.lua deleted file mode 100644 index 5d9e7e54..00000000 --- a/src/engine/test/integrationtest.lua +++ /dev/null @@ -1,447 +0,0 @@ -require("engine/application/constants") -require("engine/core/class") -require("engine/render/color") -require("engine/test/assertions") ---#if log -local logging = require("engine/debug/logging") ---#endif -local input = require("engine/input/input") - -local mod = {} - -test_states = { - none = 'none', -- no test started - running = 'running', -- the test is still running - success = 'success', -- the test has just succeeded - failure = 'failure', -- the test has just failed - timeout = 'timeout' -- the test has timed out -} - --- integration test manager: registers all itests --- itests {string: itest} registered itests, indexed by name -itest_manager = singleton(function (self) - self.itests = {} -end) -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, --- as in the describe-it pattern --- name string itest name --- states {gamestates} sequence of non-dummy gamestates used for the itest --- definition function definition callback - --- definition example: --- function () --- setup(function () --- -- setup test --- end) --- add_action(time_trigger(1.0), function () --- -- change character intention --- end) --- add_action(time_trigger(0.5), function () --- -- more actions --- end) --- final_assert(function () --- return -- true if everything is as expected --- end) --- end) -function itest_manager:register_itest(name, states, definition) - local itest = mod.integration_test(name, states) - self:register(itest) - - -- context - -- last time trigger - local last_time_trigger = nil - - -- we are defining global functions capturing local variables, which is bad - -- but it's acceptable to have them accessible inside the definition callback - -- (as getfenv/setfenv cannot be implemented in pico8 due to missing debug.getupvalue) - -- actually they would be callable even after calling register_itest as they "leak" - -- later, we'll build a full dsl parser that will not require such functions - - - -- don't name setup, busted would hide this name - function setup_callback(callback) - itest.setup = callback - end - - function add_action(trigger, callback, name) - itest:add_action(trigger, callback, name) - end - - function wait(time, use_frame_unit) - if last_time_trigger then - -- we were already waiting, so finish last wait with empty action - itest:add_action(last_time_trigger, nil) - end - last_time_trigger = mod.time_trigger(time, use_frame_unit) - end - - function act(callback) - if last_time_trigger then - itest:add_action(last_time_trigger, callback) - 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(mod.immediate_trigger, callback) - end - end - - function final_assert(callback) - itest.final_assertion = callback - end - - definition() - - -- if we finished with a wait (with or without final assertion), - -- we need to close the itest with a wait-action - if last_time_trigger then - itest:add_action(last_time_trigger, nil) - end -end - --- register a created itest instance --- you can add actions and final assertion later -function itest_manager:register(itest) - add(self.itests, itest) -end - --- proxy method for itest runner helper method -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.") - itest_runner:init_game_and_start(itest) -end - --- integration test runner singleton --- 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) - self.initialized = false - self.current_test = nil - self.current_frame = 0 - self._last_trigger_frame = 0 - self._next_action_index = 1 - self.current_state = test_states.none - self.current_message = nil - self.app = nil -end) - --- helper method to use in rendered itest _init -function itest_runner:init_game_and_start(test) - 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 - self:stop() - self.app:reset() - end - - 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 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") - 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") - if self.current_state == test_states.failure then - log("failed: "..self.current_message, "itest") - end - end - end -end - --- helper method to use in rendered itest _draw -function itest_runner:draw_game_and_test() - self.app:draw() - self:draw() -end - --- start a test: integration_test -function itest_runner:start(test) - -- lazy initialization - if not self.initialized then - self:_initialize() - end - - -- log after _initialize which sets up the logger - log("starting itest: "..test.name, "trace") - - self.current_test = test - self.current_state = test_states.running - - if test.setup then - test.setup() - end - - -- edge case: 0 actions in the action sequence. check end - -- immediately to avoid out of bounds index in _check_next_action - if not self:_check_end() then - self:_check_next_action() - end -end - -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 - -- just because we repeated update a bit too much in utests) - return - end - - -- advance time - self.current_frame = self.current_frame + 1 - - -- check for timeout (if not 0) - if self.current_test:check_timeout(self.current_frame) then - self.current_state = test_states.timeout - else - self:_check_next_action() - end -end - -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)) - else - api.print("no itest running", 8, 8, colors.white) - end -end - -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 - return colors.white - elseif test_state == test_states.success then - return colors.green - elseif test_state == test_states.failure then - return colors.red - else -- test_state == test_states.timeout then - return colors.dark_purple - end -end - -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() - logging.logger.active_categories["itest"] = true - logging.logger.active_categories["trace"] = false ---#endif - - self.initialized = true -end - -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 - 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) - if should_trigger_next_action then - -- apply next action and update time/index, unless nil (useful to just wait before itest end and final assertion) - if next_action.callback then - next_action.callback() - end - self._last_trigger_frame = self.current_frame - self._next_action_index = self._next_action_index + 1 - if self:_check_end() then - break - end - end - until not should_trigger_next_action -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 - end - if self._next_action_index > #self.current_test.action_sequence then - self:_end_with_final_assertion() - return true - end - return false -end - -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 - self.current_state = test_states.success - else - self.current_state = test_states.failure - self.current_message = message - end -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 itest_runner:stop() - if self.current_test.teardown then - self.current_test.teardown() - end - - self.current_test = nil - self.current_frame = 0 - self._last_trigger_frame = 0 - self._next_action_index = 1 - self.current_state = test_states.none -end - --- time trigger struct -local time_trigger = new_struct() -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) --- use_frame_unit bool if true, count the time in frames instead of seconds --- members --- frames int number of frames to wait before running callback after last trigger (defined from float time in s) -function time_trigger:_init(time, use_frame_unit) - if use_frame_unit then - self.frames = time - else - self.frames = flr(time * fps) - end -end - ---#if log -function time_trigger:_tostring() - return "time_trigger("..self.frames..")" -end ---#endif - --- return true if the trigger condition is verified in this context --- else return false --- elapsed_frames int number of frames elapsed since the last trigger -function time_trigger:_check(elapsed_frames) - return elapsed_frames >= self.frames -end - --- helper triggers -mod.immediate_trigger = time_trigger(0, true) - - --- scripted action struct (but we use class because comparing functions only work by reference) -scripted_action = new_class() - --- parameters --- trigger trigger trigger that will run the callback --- callback function callback called on trigger --- name string | nil optional name for debugging -function scripted_action:_init(trigger, callback, name) - self.trigger = trigger - self.callback = callback - self.name = name or "unnamed" -end - ---#if log -function scripted_action:_tostring() - return "[scripted_action ".."'"..self.name.."' ".."@ "..self.trigger.."]" -end ---#endif - - --- integration test class -local integration_test = new_class() -mod.integration_test = integration_test - --- parameters --- name string test name --- setup function setup callback - called on test start (pure function) --- teardown function teardown callback - called on test finish (pure function) --- action_sequence [scripted_action] sequence of scripted actions - run during test --- final_assertion function () => (bool, string) assertion function that returns (assertion passed, error message if failed) - called on test end --- timeout_frames int number of frames before timeout (0 for no timeout, if you know the time triggers will do the job) --- active_gamestates [gamestate.types] (non-pico8 only) sequence of gamestate modules to require for that itest. --- must be the same as in itest script first line --- and true gamestate modules should be required accordingly if directly referenced -function integration_test:_init(name, active_gamestates) - self.name = name - self.setup = nil - self.teardown = nil - self.action_sequence = {} - self.final_assertion = nil - self.timeout_frames = 0 ---#ifn pico8 - assert(active_gamestates, "integration_test._init: non-pico8 build requires active_gamestates to define them at runtime") - self.active_gamestates = active_gamestates ---#endif -end - ---#if log -function integration_test:_tostring() - return "[integration_test '"..self.name.."']" -end ---#endif - --- add an action to the action sequence. nil callback is acceptable, it acts like an empty function. -function integration_test:add_action(trigger, callback, name) - assert(trigger ~= nil, "integration_test:add_action: passed trigger is nil") - add(self.action_sequence, scripted_action(trigger, callback, name)) -end - --- set the timeout with a time parameter in s -function integration_test:set_timeout(time) - self.timeout_frames = flr(time * fps) -end - --- return true if the test has timed out at given frame -function integration_test:check_timeout(frame) - return self.timeout_frames > 0 and frame >= self.timeout_frames -end - --- return true if final assertion passes, (false, error message) else -function integration_test:_check_final_assertion() - if self.final_assertion then - return self.final_assertion() - else - return true - end -end - -return mod diff --git a/src/engine/test/pico8api.lua b/src/engine/test/pico8api.lua deleted file mode 100644 index a2d25d8e..00000000 --- a/src/engine/test/pico8api.lua +++ /dev/null @@ -1,680 +0,0 @@ --- 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("engine/test/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 --- pico8 table taken from: --- https://github.com/gamax92/picolove/blob/master/main.lua --- original repository by Jez Kabanov --- https://github.com/picolove/picolove --- both under zlib license --- see readme.md for more information - --- Lua 5.3 supports binary ops but still useful for advanced ops -local bit = require("bit32") - -pico8={ - fps=60, - memory_usage=0, - total_cpu=0, - system_cpu=0, - frames=0, - spriteflags={}, - current_music=nil, - cartdata={}, - clipboard="", - keypressed={ - [0]={}, - [1]={}, - counter=0 - }, - mousepos={ -- simulate mouse position - x=0, - y=0 - }, - mousebtnpressed={ -- simulate mouse buttons - false, - false, - false - }, - mwheel=0, - camera_x=0, - camera_y=0, - pal_transparent={}, - map={}, - poked_addresses={} -- not a complete simulation of memory, just of poked addresses set to value -} - -for i=0, 15 do - -- set all but black to opaque, black to transparent (similar to palt() implementation) - pico8.pal_transparent[i] = i == 0 and true or false -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 - self.map[y] = {} - for x = 0, 127 do - self.map[y][x] = 0 - end - end -end - --- busted-only helper to clear the spriteflags, as memset(0x3000, 0, 0x100) wouldn't work --- and we don't really do that in pico8 anyway -function pico8:clear_spriteflags() - for n = 0, 255 do - self.spriteflags[n] = 0 - end -end - -pico8:clear_spriteflags() -pico8:clear_map() - -function camera(x, y) - pico8.camera_x=flr(x) - pico8.camera_y=flr(y) -end - -function clip(x, y, w, h) - -- almost like pico8: if an arg is not missing but explicitly - -- passed as nil, it will become 0 (but that's impossible to check in Lua) - if x and y and w and h then - pico8.clip={flr(x), flr(y), flr(w), flr(h)} - else - pico8.clip=nil - end -end - -function cls(c) - pico8.clip=nil -end - -function pset(x, y, c) - if c then - color(c) - end -end - -function pget(x, y) - return 0 -end - -function color(c) - c=flr(c or 0)%16 - pico8.color=c -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 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 - --- 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 - return val - elseif kind == "number" then - if hex then - -- 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) - else - 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 .. "]" - end -end - -function spr(n, x, y, w, h, flip_x, flip_y) -end - -function sspr(sx, sy, sw, sh, dx, dy, dw, dh, flip_x, flip_y) -end - -function rect(x0, y0, x1, y1, col) - if col then - color(col) - end -end - -function rectfill(x0, y0, x1, y1, col) - if col then - color(col) - end -end - -function circ(ox, oy, r, col) - if col then - color(col) - end -end - -function circfill(cx, cy, r, col) - if col then - color(col) - end -end - -function line(x0, y0, x1, y1, col) - if col then - color(col) - end -end - -function pal(c0, c1, p) - -- the 2nd nil means undefined here, but we can't check in Lua - if c0 == nil and c1 == nil then - palt() - end -end - -function palt(c, t) - -- the 2nd nil means undefined - if c==nil or t==nil then - for i=0, 15 do - -- reset all but black to opaque, black to transparent - pico8.pal_transparent[i] = i == 0 and true or false - end - else - c=flr(c)%16 - pico8.pal_transparent[c] = t - end -end - -function fillp(p) -end - -function map(cel_x, cel_y, sx, sy, cel_w, cel_h, bitmask) -end - -function mget(x, y) - x=flr(x or 0) - y=flr(y or 0) - if x>=0 and x<128 and y>=0 and y<64 then - return pico8.map[y][x] -- will be nil if not set in test before - end - return 0 -end - -function mset(x, y, v) - x=flr(x or 0) - y=flr(y or 0) - v=flr(v or 0)%256 - if x>=0 and x<128 and y>=0 and y<64 then - pico8.map[y][x]=v - end -end - -function fget(n, f) - if n==nil then return nil end - if f~=nil then - -- return just that bit as a boolean - if not pico8.spriteflags[flr(n)] then - return false - end - return pico8.spriteflags[flr(n)] & (1 << flr(f)) ~= 0 - end - return pico8.spriteflags[flr(n)] or 0 -end - -function fset(n, f, v) - -- fset n [f] v - -- f is the flag index 0..7 - -- v is boolean - if v==nil then - v, f = f, nil - end - if f then - -- set specific bit to v (true or false) - if v then - pico8.spriteflags[n]=pico8.spriteflags[n] | (1 << f) - else - pico8.spriteflags[n]=pico8.spriteflags[n] & ~(1 << f) - end - else - -- set bitfield to v (number) - pico8.spriteflags[n]=v - end -end - -function sget(x, y) - return 0 -end - -function sset(x, y, c) -end - -function music(n, fadems, channel_mask) - n = n or -1 - if n < -1 then - n = 0 - end - if n >= 0 then - -- simulate music currently played - -- this will be correct for a looping music (without knowing the used channels and current play time) - -- however for a non-looping music we won't detect when the music is supposed to end in integration tests - fadems = fadems or 0 - channel_mask = channel_mask or 0 - pico8.current_music={music=n, fadems=fadems, channel_mask=channel_mask} - else - pico8.current_music = nil - end -end - -function sfx(n, channel, offset) - -- most sfx are non-looping so it's not so useful to have a current sfx, and it's tedious - -- to keep a list of played sfx history, so we just do nothing and will spy on sfx if needed -end - -function peek(addr) - return pico8.poked_addresses[addr] -end - -function poke(addr, val) - pico8.poked_addresses[addr] = val -end - -function peek4(addr) - local val = 0 - val = val + peek(addr+0)/0x10000 - val = val + peek(addr+1)/0x100 - val = val + peek(addr+2) - val = val + peek(addr+3)*0x100 - return val -end - -function poke4(addr, val) - val=val*0x10000 - poke(addr+0, (val & 0x000000FF) >> 0) - poke(addr+1, (val & 0x0000FF00) >> 8) - poke(addr+2, (val & 0x00FF0000) >> 16) - poke(addr+3, (val & 0xFF000000) >> 24) -end - -function memcpy(dest_addr, source_addr, len) - if len<1 or dest_addr==source_addr then - return - end - - -- Screen Hack (removed) - - local offset=dest_addr-source_addr - if source_addr>dest_addr then - for i=dest_addr, dest_addr+len-1 do - poke(i, peek(i-offset)) - end - else - for i=dest_addr+len-1, dest_addr, -1 do - poke(i, peek(i-offset)) - end - end - - -- __scrimg and __scrblit (removed) -end - -function memset(dest_addr, val, len) - if len<1 then - return - end - for i=dest_addr, dest_addr+len-1 do - poke(i, val) - end -end - -function reload(dest_addr, source_addr, len) -end - -function cstore(dest_addr, source_addr, len) -end - -function rnd(x) - return math.random()*(x or 1) -end - -function srand(seed) - math.randomseed(flr(seed*0x10000)) -end - -function flr(value) - if value ~= nil then - return math.floor(value) - else - return 0 - end -end - -function ceil(value) - if value ~= nil then - return math.ceil(value) - else - return 0 - end -end - -function sgn(x) - return x<0 and-1 or 1 -end - -abs=math.abs - --- pico8 min only supports 2 arguments. use math.min if you want the min of 3+ arguments in busted tests -function min(a, b) - if a==nil or b==nil then - return 0 - end - if ab then return a end - 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> y)/0x10000 -end - -function rotl(x, y) - return bit.lrotate(x*0x10000, y)/0x10000 -end - -function rotr(x, y) - return bit.rrotate(x*0x10000, y)/0x10000 -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 - -function btn(i, p) - if i~=nil or p~=nil then - p=p or 0 - if p<0 or p>1 then - return false - end - return not not pico8.keypressed[p][i] - else - local bits=0 - for i=0, 5 do - bits=bits+(pico8.keypressed[0][i] and 2^i or 0) - bits=bits+(pico8.keypressed[1][i] and 2^(i+8) or 0) - end - return bits - end -end - -function btnp(i, p) - if i~=nil or p~=nil then - p=p or 0 - if p<0 or p>1 then - return false - end - local init=(pico8.fps/2-1) - local v=pico8.keypressed.counter - if pico8.keypressed[p][i] and (v==init or v==1) then - return true - end - return false - else - local init=(pico8.fps/2-1) - local v=pico8.keypressed.counter - if not (v==init or v==1) then - return 0 - end - local bits=0 - for i=0, 5 do - bits=bits+(pico8.keypressed[0][i] and 2^i or 0) - bits=bits+(pico8.keypressed[1][i] and 2^(i+8) or 0) - end - return bits - end -end - -function cartdata(id) -end - -function dget(index) - index=flr(index) - if index<0 or index>63 then - -- out of range - return nil - end - return pico8.cartdata[index] -end - -function dset(index, value) - index=flr(index) - if index<0 or index>63 then - -- out of range - return - end - pico8.cartdata[index]=value -end - -local tfield={[0]="year", "month", "day", "hour", "min", "sec"} -function stat(x) - if x == 0 then - return pico8.memory_usage - elseif x == 1 then - return pico8.total_cpu - elseif x == 2 then - return pico8.system_cpu - elseif x == 4 then - return pico8.clipboard - elseif x == 7 then - return pico8.fps - elseif x == 8 then - return pico8.fps - elseif x == 9 then - return pico8.fps - elseif x >= 16 and x <= 23 then - return 0 -- audio channels not supported - elseif x == 30 then - return 0 -- devkit keyboard not supported - elseif x == 31 then - return "" -- devkit keyboard not supported - elseif x == 32 then - return pico8.mousepos.x - elseif x == 33 then - return pico8.mousepos.y - elseif x == 34 then - local btns=0 - for i=0, 2 do - if pico8.mousebtnpressed[i+1] then - btns=btns | (1 << i) - end - end - return btns - elseif x == 36 then - return pico8.mwheel - elseif (x >= 80 and x <= 85) or (x >= 90 and x <= 95) then - local tinfo - if x < 90 then - tinfo = os.date("!*t") - else - tinfo = os.date("*t") - end - return tinfo[tfield[x%10]] - elseif x == 100 then - return nil -- TODO: breadcrumb not supported - end - return 0 -end - -function holdframe() -end - -sub=string.sub -cocreate=coroutine.create -coresume=coroutine.resume -yield=coroutine.yield -costatus=coroutine.status -trace=debug.traceback - --- The functions below are normally attached to the program code, but are here for simplicity -function all(a) - if a==nil or #a==0 then - return function() end - end - local i, li=1 - return function() - if (a[i]==li) then i=i+1 end - while(a[i]==nil and i<=#a) do i=i+1 end - li=a[i] - return a[i] - end -end - -function foreach(a, f) - for v in all(a) do - f(v) - end -end - -function count(a) - local count=0 - for i=1, #a do - if a[i]~=nil then count=count+1 end - end - return count -end - -function add(a, v) - if a==nil then return end - a[#a+1]=v -end - -function del(a, dv) - if a==nil then return end - for i=1, #a do - if a[i]==dv then - table.remove(a, i) - return - end - end -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) --- 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 - 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 = {} - --- only print is defined under api to avoid overriding native print --- (used by busted -o TAP) --- note that runtime code will need to define api.print -function api.print(str, x, y, col) - if col then - color(col) - end -end diff --git a/src/engine/test/unittest.lua b/src/engine/test/unittest.lua deleted file mode 100644 index c738b7b2..00000000 --- a/src/engine/test/unittest.lua +++ /dev/null @@ -1,47 +0,0 @@ -require("engine/core/class") -require("engine/test/unittest_helper") - -local unittest = {} - --- unit test framework mimicking some busted features --- for direct use in pico8 headless --- busted features supported: "it" as "check" (immediate assert, no collection of test results) - - --- unit test manager: registers all utests and runs them --- utests [utest] registered utests -utest_manager = singleton(function (self) - self.utests = {} -end) -unittest.utest_manager = utest_manager - -function utest_manager:register(utest) - add(self.utests, utest) -end - -function utest_manager:run_all_tests() - for utest in all(self.utests) do - utest.callback() - end -end - --- unit test class for pico8 -local unit_test = new_class() -unittest.unit_test = unit_test - --- parameters --- name string test name --- callback function test callback, containing assertions -function unit_test:_init(name, callback) - self.name = name - self.callback = callback -end - --- busted-like shortcut functions - -function check(name, callback) - local utest = unit_test(name, callback) - utest_manager:register(utest) -end - -return unittest diff --git a/src/engine/test/unittest_helper.lua b/src/engine/test/unittest_helper.lua deleted file mode 100644 index 01de040f..00000000 --- a/src/engine/test/unittest_helper.lua +++ /dev/null @@ -1,16 +0,0 @@ -require("engine/core/helper") - --- helper for unitests executed in pico8, that miss busted assertions - -function are_same_with_message(t, passed, compare_raw_content) - 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(...))) - return true, "Expected objects to not be the same (compare_raw_content: "..tostr(compare_raw_content)..").\nPassed in:\n"..nice_dump(passed).."\nDid not expect:\n"..nice_dump(t) - else - return false, "Expected objects to be the same (compare_raw_content: "..tostr(compare_raw_content)..").\nPassed in:\n"..nice_dump(passed).."\nExpected:\n"..nice_dump(t) - end -end diff --git a/src/engine/tests/utestanimated_sprite.lua b/src/engine/tests/utestanimated_sprite.lua deleted file mode 100644 index c3ebe733..00000000 --- a/src/engine/tests/utestanimated_sprite.lua +++ /dev/null @@ -1,322 +0,0 @@ -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") - -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, 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, - 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_table) - 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) - - describe('_tostring', 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([3 sprites], 10, true), no_loop = animated_sprite_data([2 sprites], 10, false)}, true, 1.5, 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 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) - - 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_frame) - 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_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.play_speed_frame = 1 - 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 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 - - anim_spr:update() - - assert.are_same({1, 9}, - {anim_spr.current_step, anim_spr.local_frame}) - end) - - 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 - - anim_spr:update() - - assert.are_same({2, 0}, - {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 = 3 - 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 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.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, 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) - - end) - - describe('render', function () - - setup(function () - sprite_data_render = stub(sprite_data, "render") - end) - - teardown(function () - sprite_data_render:revert() - end) - - after_each(function () - sprite_data_render:clear() - end) - - 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) - assert.spy(sprite_data_render).was_called_with(match.ref(spr_data2), vector(41, 80), false, true) - end) - - end) - -end) diff --git a/src/engine/tests/utestanimated_sprite_data.lua b/src/engine/tests/utestanimated_sprite_data.lua deleted file mode 100644 index 478b2556..00000000 --- a/src/engine/tests/utestanimated_sprite_data.lua +++ /dev/null @@ -1,40 +0,0 @@ -require("engine/test/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)) - local spr_data_table = {step1 = spr_data1, step2 = spr_data2} - - 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('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 () - 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/src/engine/tests/utestassertions.lua b/src/engine/tests/utestassertions.lua deleted file mode 100644 index 6bf33853..00000000 --- a/src/engine/tests/utestassertions.lua +++ /dev/null @@ -1,55 +0,0 @@ -require("engine/test/bustedhelper") -require("engine/core/math") -require("engine/test/assertions") - -describe('assertions', function () - - describe('contains', function () - it('should return true when the searched value is contained in the table', function () - assert.is_true(contains({1, 2, 3}, 2)) - assert.is_true(contains({"string", vector(2, 4)}, vector(2, 4))) - end) - it('should return false when the searched value is not contained in the table', function () - assert.is_false(contains({1, 2, 3}, 0)) - assert.is_false(contains({"string", vector(2, 5)}, vector(2, 4))) - end) - end) - - 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}" - 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)}".."" - 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}" - 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)}".."" - 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, "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 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) - -end) diff --git a/src/engine/tests/utestbustedhelper.lua b/src/engine/tests/utestbustedhelper.lua deleted file mode 100644 index 012870f1..00000000 --- a/src/engine/tests/utestbustedhelper.lua +++ /dev/null @@ -1,52 +0,0 @@ -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("@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("@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("@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 - outside() -- line 24 - end) - end) - - describe('print_at_line', function () - - local native_print_stub - - setup(function () - native_print_stub = stub(_G, "print") -- native print - end) - - teardown(function () - native_print_stub:revert() - end) - - after_each(function () - native_print_stub:clear() - end) - - 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("@src/engine/tests/utestbustedhelper.lua:45: text") - end) - - end) - -end) diff --git a/src/engine/tests/utestclass.lua b/src/engine/tests/utestclass.lua deleted file mode 100644 index 18735e39..00000000 --- a/src/engine/tests/utestclass.lua +++ /dev/null @@ -1,474 +0,0 @@ -require("engine/test/bustedhelper") -local class = require("engine/core/class") - -local dummy_class = new_class() - -function dummy_class:_init(value) - self.value = 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: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) - assert.are_equal(3, dummy.value) - end) - - it('should support custom method: _tostring', function () - assert.are_equal("dummy:12", dummy_class(12):_tostring()) - end) - - it('should support instance concatenation with a string', function () - assert.are_equal("dummy:11str", dummy_class(11).."str") - end) - it('should support instance concatenation with a boolean', function () - assert.are_equal("dummy:11true", dummy_class(11)..true) - end) - it('should support instance concatenation with a number', function () - assert.are_equal("dummy:1124", dummy_class(11)..24) - end) - it('should support instance concatenation with a number on the left', function () - assert.are_equal("27dummy:11", "27"..dummy_class(11)) - end) - it('should support instance concatenation with another instance', function () - assert.are_equal("dummy:11dummy:46", dummy_class(11)..dummy_class(46)) - end) - it('should support instance concatenation with a chain of objects', function () - assert.are_equal("dummy:11, and dummy:46", dummy_class(11)..", and "..dummy_class(46)) - end) - - it('should support metamethod: __eq for equality', function () - assert.are_equal(dummy_class(-5), dummy_class(-5)) - end) - - it('should support metamethod: __eq for inequality', function () - assert.are_not_equal(dummy_class(-5), dummy_class(-3)) - end) - - it('should support custom method: get_incremented_value', function () - assert.are_equal(-4, dummy_class(-5):get_incremented_value()) - end) - - describe('dummy_derived class', function () - - local dummy_derived_class = derived_class(dummy_class) - - function dummy_derived_class:_init(value, value2) - -- always call ._init on base class, never :_init which would set static members - dummy_class._init(self, value) - self.value2 = value2 - end - - function dummy_derived_class:_tostring() - return "dummy_derived:"..tostr(self.value)..","..tostr(self.value2) - end - - function dummy_derived_class.__eq(lhs, rhs) - return lhs.value == rhs.value and lhs.value2 == rhs.value2 - end - - it('should create a new dummy_derived_class with a value attribute', function () - local dummy_derived = dummy_derived_class(3, 7) - assert.are_same({3, 7}, {dummy_derived.value, dummy_derived.value2}) - end) - - it('should support custom method: _tostring', function () - assert.are_equal("dummy_derived:12,45", dummy_derived_class(12, 45):_tostring()) - end) - - it('should support instance concatenation with a string', function () - assert.are_equal("dummy_derived:11,45str", dummy_derived_class(11, 45).."str") - end) - it('should support instance concatenation with a boolean', function () - assert.are_equal("dummy_derived:11,45true", dummy_derived_class(11, 45)..true) - end) - it('should support instance concatenation with a number', function () - assert.are_equal("dummy_derived:11,4524", dummy_derived_class(11, 45)..24) - end) - it('should support instance concatenation with a number on the left', function () - assert.are_equal("27dummy_derived:11,45", "27"..dummy_derived_class(11, 45)) - end) - it('should support instance concatenation with another instance of dummy_derived', function () - assert.are_equal("dummy_derived:11,45dummy_derived:46,23", dummy_derived_class(11, 45)..dummy_derived_class(46, 23)) - end) - it('should support instance concatenation with an instance of dummy', function () - assert.are_equal("dummy_derived:11,45dummy:46", dummy_derived_class(11, 45)..dummy_class(46)) - end) - it('should support instance concatenation within a chain of objects', function () - assert.are_equal("dummy_derived:11,45, and dummy:46", dummy_derived_class(11, 45)..", and "..dummy_class(46)) - end) - - it('should support metamethod: __eq for equality', function () - assert.are_equal(dummy_derived_class(-5, 45), dummy_derived_class(-5, 45)) - end) - - it('should support metamethod: __eq for inequality', function () - assert.are_not_equal(dummy_derived_class(-5, 45), dummy_derived_class(-5, 43)) - end) - - it('should allow access to base class custom method: get_incremented_value', function () - assert.are_equal(-4, dummy_derived_class(-5, 45):get_incremented_value()) - end) - - end) - -end) - -describe('new_struct', function () - - local dummy_struct = new_struct() - - function dummy_struct:_init(value1, value2) - self.value1 = value1 - self.value2 = value2 - end - - function dummy_struct:_tostring() - return "dummy: "..joinstr(", ", self.value1, self.value2) - end - - function dummy_struct:get_sum() - 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 - - it('should create a new struct with _init()', function () - local dummy = dummy_struct(3, 7) - assert.are_same({3, 7}, {dummy.value1, dummy.value2}) - end) - - it('should create a new struct with access to methods via __index', function () - local dummy = dummy_struct(3, 7) - assert.are_equal(10, dummy:get_sum()) - end) - - describe('struct_eq', function () - - it('should return true for two structs equal by reference', function () - local dummy1 = dummy_struct(3, 7) - assert.is_true(dummy1 == dummy1) - end) - - it('should return true for two structs with same content', function () - local dummy1 = dummy_struct(3, 7) - local dummy2 = dummy_struct(3, 7) - assert.is_true(dummy1 == dummy2) - end) - - it('should return false for two structs with different contents', function () - local dummy1 = dummy_struct(3, 7) - local dummy2 = dummy_struct(3, -10) - assert.is_true(dummy1 ~= dummy2) - end) - - it('should return false for one struct and an unrelated table with same content', function () - local dummy1 = dummy_struct(3, 7) - local not_the_same_struct = { value1 = 3, value2 = 7 } - assert.is_true(dummy1 ~= not_the_same_struct) - end) - - end) - - describe('copy', 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) -- are_equal also works, provided __eq is working - assert.is_false(rawequal(dummy, copied_dummy)) - 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 complex = complex_struct(3, 7) - local copied_complex = complex:copy() - - assert.are_same(complex, 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) - - end) - - describe('dummy_derived struct', function () - - local dummy_derived_struct = derived_struct(dummy_struct) - - function dummy_derived_struct:_init(value1, value2, value3) - -- always call ._init on base struct, never :_init which would set static members - dummy_struct._init(self, value1, value2) - self.value3 = value3 - end - - function dummy_derived_struct:_tostring() - return "dummy_derived_struct: "..joinstr(", ", self.value1, self.value2, self.value3) - end - - function dummy_derived_struct:get_sum() - return dummy_struct.get_sum(self) + self.value3 - end - - it('should create a new struct with _init()', function () - local dummy_derived = dummy_derived_struct(3, 7, 9) - assert.are_same({3, 7, 9}, {dummy_derived.value1, dummy_derived.value2, dummy_derived.value3}) - end) - - it('should create a new struct with access to methods via __index (override calling base)', function () - local dummy_derived = dummy_derived_struct(3, 7, 9) - 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 () - local dummy_derived1 = dummy_derived_struct(3, 7, 9) - assert.is_true(dummy_derived1 == dummy_derived1) - end) - - it('should return true for two structs with same content', function () - local dummy_derived1 = dummy_derived_struct(3, 7, 9) - local dummy_derived2 = dummy_derived_struct(3, 7, 9) - assert.is_true(dummy_derived1 == dummy_derived2) - end) - - it('should return false for two structs with different contents (on derived members only)', function () - local dummy_derived1 = dummy_derived_struct(3, 7, 9) - local dummy_derived2 = dummy_derived_struct(3, 7, -99) - assert.is_true(dummy_derived1 ~= dummy_derived2) - end) - - it('should return false for one struct and an unrelated table with same content', function () - local dummy_derived1 = dummy_derived_struct(3, 7, 9) - local not_the_same_struct = { value1 = 3, value2 = 7, value = 9 } - assert.is_true(dummy_derived1 ~= not_the_same_struct) - end) - - end) - - end) - -end) - -describe('singleton', function () - - local my_singleton = singleton(function (self) - self.type = "custom" - end) - - function my_singleton:_tostring() - return "[my_singleton "..self.type.."]" - end - - it('should define a singleton with unique members', function () - assert.are_equal("custom", my_singleton.type) - end) - - describe('changing member', function () - - setup(function () - my_singleton.type = "changed" - end) - - teardown(function () - my_singleton.type = "custom" - end) - - it('init should reinit the state vars', function () - my_singleton:init() - assert.are_equal("custom", my_singleton.type) - end) - - end) - - it('should support custom method: _tostring', function () - assert.are_equal("[my_singleton custom]", my_singleton:_tostring()) - end) - - it('should support string concatenation with _tostring', function () - assert.are_equal("this is [my_singleton custom]", "this is "..my_singleton) - end) - -end) - -describe('derived_singleton', function () - - local my_singleton = singleton(function (self) - self.types = { "custom" } -- the table allows us to check if __index in derived_singleton reaches it by ref to change it - end) - - function my_singleton:get_first_type() - return self.types[1] - end - - function my_singleton:_tostring() - return "[my_singleton "..self.types[1].."]" - end - - local my_derived_singleton = derived_singleton(my_singleton, function (self) - self.subtype = "special" - end) - - function my_derived_singleton:_tostring() - return "[my_derived_singleton "..my_singleton._tostring(self)..", "..self.subtype.."]" - end - - local my_derived_singleton_no_init = derived_singleton(my_derived_singleton) - - function my_derived_singleton_no_init:new_method() - return 5 - end - - it('should define a derived_singleton with base members', function () - assert.are_equal("custom", my_derived_singleton.types[1]) - end) - - it('should define a derived_singleton with derived members using derived_init', function () - assert.are_equal("special", my_derived_singleton.subtype) - end) - - it('should define a derived_singleton with derived members with same init if none is provided', function () - assert.are_equal("special", my_derived_singleton_no_init.subtype) - end) - - it('should define a derived_singleton with new methods', function () - assert.are_equal(5, my_derived_singleton_no_init.new_method()) - end) - - describe('changing base member copy', function () - - before_each(function () - my_derived_singleton.types[1] = "changed" - end) - - after_each(function () - my_derived_singleton.types[1] = "custom" - end) - - it('should create a copy of base members on the derived singleton so they are never changed on the base singleton', function () - assert.are_equal("custom", my_singleton.types[1]) - end) - - describe('changing base member copy', function () - - before_each(function () - my_derived_singleton.subtype = "subchanged" - end) - - after_each(function () - my_derived_singleton.subtype = "special" - end) - - it('init should reinit the state vars', function () - assert.are_equal("changed", my_derived_singleton.types[1]) - assert.are_equal("subchanged", my_derived_singleton.subtype) - my_derived_singleton:init() - assert.are_equal("custom", my_derived_singleton.types[1]) - assert.are_equal("special", my_derived_singleton.subtype) - end) - - end) - - end) - - it('should access base method: get_first_type', function () - assert.are_equal("custom", my_derived_singleton:get_first_type()) - end) - - it('should support custom method: _tostring', function () - 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 [my_singleton custom], special]", "this is "..my_derived_singleton) - end) - -end) diff --git a/src/engine/tests/utestcodetuner.lua b/src/engine/tests/utestcodetuner.lua deleted file mode 100644 index fd40587f..00000000 --- a/src/engine/tests/utestcodetuner.lua +++ /dev/null @@ -1,235 +0,0 @@ -require("engine/test/bustedhelper") -local codetuner = require("engine/debug/codetuner") - -describe('codetuner', function () - - describe('position utils', function () - - -- simulate widget child with position and size via duck-typing - local mock_widget = {x = 5, y = 6, w = 8, h = 9} - - describe('next_to', function () - - it('should return a new position on the right of a widget with a default margin of 2', function () - assert.are_same({15, 6}, {codetuner.next_to(mock_widget)}) - end) - - it('should return a new position on the right of a widget with the passed margin', function () - assert.are_same({17, 6}, {codetuner.next_to(mock_widget, 4)}) - end) - - end) - - describe('below', function () - - it('should return a new position below a widget with a default margin of 2', function () - assert.are_same({5, 17}, {codetuner.below(mock_widget)}) - end) - - it('should return a new position below a widget with the passed margin', function () - assert.are_same({5, 19}, {codetuner.below(mock_widget, 4)}) - end) - - end) - - end) - - describe('tuned_variable', function () - - describe('_init', function () - it('should create a new tile vector with the right coordinates', function () - local tuned_var = codetuner.tuned_variable("offset", 6) - assert.are_same({"offset", 6}, {tuned_var.name, tuned_var.default_value}) - end) - end) - - describe('_tostring', function () - it('should return a string representation with the name and default value', function () - local tuned_var = codetuner.tuned_variable("offset", 6) - assert.are_equal("tuned_variable \"offset\" (default: 6)", tuned_var:_tostring()) - end) - end) - - end) - - describe('(codetuner active)', function () - local warn_stub - - setup(function () - codetuner.active = true -- needed to create tuned vars - warn_stub = stub(_G, "warn") - end) - - teardown(function () - codetuner.active = false - warn_stub:revert() - end) - - after_each(function () - clear_table(codetuner.tuned_vars) - clear_table(codetuner.main_panel.children) - warn_stub:clear() - end) - - describe('get_spinner_callback', function () - - it('should return a function that sets an existing tuned var', function () - tuned("tuned_var", 17) - local f = codetuner:get_spinner_callback("tuned_var") - -- simulate spinner via duck-typing - local fake_spinnner = {value = 11} - f(fake_spinnner) - assert.are_equal(11, codetuner.tuned_vars["tuned_var"]) - end) - - end) - - describe('get_or_create_tuned_var', function () - - it('when name doesn\'t exist it should create tuned var with default value and return it', function () - local result = tuned("unknown", 14) - assert.are_same({14, 14}, {codetuner.tuned_vars["unknown"], result}) - end) - - it('when name exists it should return the current tuned value', function () - tuned("tuned_var", 20) - -- we normally avoid conflicting default values, - -- but this example is to show we use the actual current value - local tuned_var_before_set = tuned("tuned_var", -20) - codetuner:set_tuned_var("tuned_var", 170) - local tuned_var_after_set = tuned("tuned_var", -25) - assert.are_same({20, 170}, {tuned_var_before_set, tuned_var_after_set}) - end) - - it('should add corresponding children to the panel', function () - tuned("tuned_var1", 1) - tuned("tuned_var2", 2) - assert.is_not_nil(codetuner.main_panel) - assert.are_equal(2, #codetuner.main_panel.children) - end) - - end) - - describe('set_tuned_var', function () - - it('should set tuned value if it exists', function () - tuned("tuned_var", 24) - codetuner:set_tuned_var("tuned_var", 26) - assert.are_equal(26, codetuner.tuned_vars["tuned_var"]) - end) - - it('should do nothing if the passed tuned var doesn\'t exist', function () - codetuner:set_tuned_var("unknown", 28) - assert.is_nil(codetuner.tuned_vars["unknown"]) - assert.spy(warn_stub).was_called(1) - assert.spy(warn_stub).was_called_with(match.matches('codetuner:set_tuned_var: no tuned var found with name: .*'), "codetuner") - end) - - end) - - end) - - describe('(codetuner inactive)', function () - - after_each(function () - clear_table(codetuner.tuned_vars) - clear_table(codetuner.main_panel.children) - end) - - describe('get_or_create_tuned_var', function () - - it('should not create a new tuned var, not return any existing tuned var and return default value', function () - -- avoid conflicting default values, but this example is to show we use the passed one - codetuner.active = false - local inactive_tuned_var_before_set = tuned("tuned_var", 12) - local inactive_tuned_var_after_set = tuned("tuned_var", 18) - -- if a new default is provided, it is used whatever - assert.is_nil(codetuner.tuned_vars["tuned var"]) - assert.are_same({inactive_tuned_var_before_set, inactive_tuned_var_after_set}, - {12, 18}) - end) - end) - - end) - - describe('(gui invisible)', function () - - before_each(function () - codetuner.gui.visible = false - end) - - after_each(function () - codetuner.gui.visible = false - end) - - describe('show', function () - - it('should make the gui visible', function () - codetuner:show() - assert.is_true(codetuner.gui.visible) - end) - - end) - - describe('hide', function () - - it('should make the gui invisible', function () - codetuner.gui.visible = true - codetuner:hide() - assert.is_false(codetuner.gui.visible) - end) - - end) - - end) - - describe('(on start) codetuner:init_window', function () - - it('should have constructed a gui root with a panel of tuned values', function () - assert.is_not_nil(codetuner.gui) - assert.is_false(codetuner.gui.visible) - assert.are_equal(1, #codetuner.gui.children) - assert.are_equal(codetuner.main_panel, codetuner.gui.children[1]) - end) - - end) - - describe('codetuner:update_window', function () - local update_stub - - setup(function () - update_stub = stub(codetuner.gui, "update") - end) - - teardown(function () - update_stub:revert() - end) - - it('should call gui:update', function () - codetuner:update_window() - assert.spy(update_stub).was_called() - assert.spy(update_stub).was_called_with(match.ref(codetuner.gui)) - end) - - end) - - describe('codetuner:render_window', function () - local draw_stub - - setup(function () - draw_stub = stub(codetuner.gui, "draw") - end) - - teardown(function () - draw_stub:revert() - end) - - it('should call gui:draw', function () - codetuner:render_window() - assert.spy(draw_stub).was_called() - assert.spy(draw_stub).was_called_with(match.ref(codetuner.gui)) - end) - - end) - -end) diff --git a/src/engine/tests/utestcollision.lua b/src/engine/tests/utestcollision.lua deleted file mode 100644 index e8df4063..00000000 --- a/src/engine/tests/utestcollision.lua +++ /dev/null @@ -1,679 +0,0 @@ -require("engine/test/bustedhelper") -require("engine/application/constants") -require("engine/core/math") -local collision = require("engine/physics/collision") -local aabb = collision.aabb - --- retrieve the filter arguments so we can optimize by only generating tests we will need -local cli = require('busted.modules.cli')() -local cli_args, cli_err = cli:parse(arg) --- we are testing "all" in module iff we are using empty filters -local is_testing_all = #cli_args["filter"] == 0 and #cli_args["filter-out"] == 0 - --- create variants of an original test with 2 aabb and --- an expected result for collides, touches and intersects, with prioritized escape direction --- by applying various transformations, and register them all -local function describe_all_test_variants(original_bb1, original_bb2, - original_escape_vector, original_touches_result, intersects_result, original_prioritized_escape_direction) - - -- generate 32 variants of this configuration by: - -- - swapping boxes - -- - applying horizontal and vertical symmetry (not both, this is already done via 180 rotation) - -- - rotation by 0, 90, 180, 270 degrees - -- every time, apply the corresponding transformations to the escape vector, if any - for role_swap in all({false, true}) do - for symmetry_x in all({false, true}) do - for symmetry_y in all({false, true}) do - for quadrant = 0, 3 do - - local should_describe_test = true - - -- except when testing all in module (typically in ci), ignore extra test variants for faster local unit tests - local is_extra_test = role_swap or symmetry_y or quadrant > 1 - if is_extra_test and not is_testing_all then - should_describe_test = false - end - - if should_describe_test then - - local bb1 = original_bb1:copy() - local bb2 = original_bb2:copy() - - -- copy results if not nil - local escape_vector = original_escape_vector and original_escape_vector:copy() - local prioritized_escape_direction = original_prioritized_escape_direction - - -- if boxes are swapped, collision works the same but escape vector is opposite - if role_swap then - local temp = bb1 - bb1 = bb2 - bb2 = temp - - if escape_vector then - escape_vector:mul_inplace(-1) - end - if prioritized_escape_direction then - prioritized_escape_direction = oppose_dir(prioritized_escape_direction) - end - end - - -- if boxes are mirrored, collision works the same but escape vector is mirrored - - if symmetry_x then - bb1:mirror_x() - bb2:mirror_x() - - if escape_vector then - escape_vector:mirror_x() - end - if prioritized_escape_direction then - prioritized_escape_direction = mirror_dir_x(prioritized_escape_direction) - end - end - - if symmetry_y then - bb1:mirror_y() - bb2:mirror_y() - - if escape_vector then - escape_vector:mirror_y() - end - if prioritized_escape_direction then - prioritized_escape_direction = mirror_dir_y(prioritized_escape_direction) - end - end - - -- if boxes are rotates, collision works the same but escape vector is rotated - for i = 1, quadrant do - bb1:rotate_90_cw_inplace() - bb2:rotate_90_cw_inplace() - - if escape_vector then - escape_vector:rotate_90_cw_inplace() - end - if prioritized_escape_direction then - prioritized_escape_direction = rotate_dir_90_cw(prioritized_escape_direction) - end - end - - local transformation_description = '(~ role_swap: '..tostr(role_swap)..', '.. - '+ symmetry_x: '..tostr(symmetry_x)..', '.. - '+ symmetry_y: '..tostr(symmetry_y)..', '.. - '+~~ rotation: '..tostr(90 * quadrant)..')' - - local test_description = transformation_description..' (compute_escape_vector, collides, touches, . intersects) should return ('..joinstr(', ', escape_vector, original_touches_result, - intersects_result)..')' - - -- we test all the public methods that than private helper _compute_signed_distance_and_escape_direction - -- but we could also test _compute_signed_distance_and_escape_direction, then the public methods - -- with simple unit test that doesn't recheck the whole thing (e.g. with api call checks or by mocking the helper result) - it(test_description, function () - assert.are_same({escape_vector, escape_vector ~= nil, original_touches_result, intersects_result}, - {bb1:compute_escape_vector(bb2, prioritized_escape_direction), bb1:collides(bb2), bb1:touches(bb2), bb1:intersects(bb2)}) - end) - - end - - end - end - end - end - -end - -describe('collision', function () - - describe('aabb', function () - - describe('_init', function () - - it('should create an AABB with center and extents', function () - local bb = aabb(vector(-3., 4.), vector(2., 6.)) - assert.are_same({vector(-3., 4.), vector(2., 6.)}, {bb.center, bb.extents}) - end) - - end) - - describe('_tostring', function () - - it('should return "aabb({self.center}, {self.extents})"', function () - local bb = aabb(vector(-3., 4.), vector(2., 6.)) - assert.are_equal("aabb(vector(-3.0, 4.0), vector(2.0, 6.0))", bb:_tostring()) - end) - - end) - - describe('rotated_90_cw', function () - it('aabb((-4, 6), (2, 3)) => aabb((-6, -4), (3, 2))"', function () - assert.are_equal(aabb(vector(-6., -4.), vector(3., 2.)), aabb(vector(-4, 6), vector(2, 3)):rotated_90_cw()) - end) - end) - - describe('rotate_90_cw_inplace', function () - it('aabb((-4, 6), (2, 3)) => aabb((-6, -4), (3, 2))"', function () - local bb = aabb(vector(-4, 6), vector(2, 3)) - bb:rotate_90_cw_inplace() - assert.are_equal(aabb(vector(-6., -4.), vector(3., 2.)), bb) - end) - end) - - describe('rotated_90_ccw', function () - it('aabb((-4, 6), (2, 3)) => aabb((6, 4), (3, 2))"', function () - assert.are_equal(aabb(vector(6., 4.), vector(3., 2.)), aabb(vector(-4, 6), vector(2, 3)):rotated_90_ccw()) - end) - end) - - describe('rotate_90_ccw_inplace', function () - it('aabb((-4, 6), (2, 3)) => aabb((6, 4), (3, 2))"', function () - local bb = aabb(vector(-4, 6), vector(2, 3)) - bb:rotate_90_ccw_inplace() - assert.are_equal(aabb(vector(6., 4.), vector(3., 2.)), bb) - end) - end) - - describe('mirror_x', function () - it('aabb((-4, 6), (2, 3)) => aabb((4, 6), (2, 3))"', function () - local bb = aabb(vector(-4, 6), vector(2, 3)) - bb:mirror_x() - assert.are_equal(aabb(vector(4., 6.), vector(2., 3.)), bb) - end) - end) - - describe('mirror_y', function () - it('aabb((-4, 6), (2, 3)) => aabb((-4, -6), (2, 3))"', function () - local bb = aabb(vector(-4, 6), vector(2, 3)) - bb:mirror_y() - assert.are_equal(aabb(vector(-4., -6.), vector(2., 3.)), bb) - end) - end) - - describe('collision methods', function () - - describe('+ Case 1: Diagonal opposite', function () - - describe_all_test_variants( - aabb(vector(-2., -2.), vector(1., 1.)), - aabb(vector(2., 2.), vector(1., 1.)), - nil, - false, - false - ) - end) - - describe('Case 2: 1-axis overlap', function () - - describe_all_test_variants( - aabb(vector(-2., 1.), vector(1., 1.)), - aabb(vector(2., 2.), vector(1., 1.)), - nil, - false, - false - ) - end) - - describe('Case 3: Full 1-axis overlap', function () - - describe_all_test_variants( - aabb(vector(-2., 2.), vector(1., 1.)), - aabb(vector(2., 2.), vector(1., 1.)), - nil, - false, - false - ) - end) - - describe('Case 4: 1-axis inside', function () - - describe_all_test_variants( - aabb(vector(-2., 2.), vector(1., 0.5)), - aabb(vector(2., 2.), vector(1., 1.)), - nil, - false, - false - ) - end) - - describe('Case 5: 1-axis cover', function () - - describe_all_test_variants( - aabb(vector(-2., 2.), vector(1., 2.)), - aabb(vector(2., 2.), vector(1., 1.)), - nil, - false, - false - ) - end) - - describe('Case 6: Touch corner', function () - - describe_all_test_variants( - aabb(vector(-1., -1.), vector(1., 1.)), - aabb(vector(1., 1.), vector(1., 1.)), - nil, - true, - true - ) - end) - - describe('Case 7: Touch partial edge', function () - - describe_all_test_variants( - aabb(vector(-1., 0.), vector(1., 1.)), - aabb(vector(1., 1.), vector(1., 1.)), - nil, - true, - true - ) - end) - - describe('Case 8: Touch full edge', function () - - describe_all_test_variants( - aabb(vector(-1., 1.), vector(1., 1.)), - aabb(vector(1., 1.), vector(1., 1.)), - nil, - true, - true - ) - end) - - describe('. Case 9: Touch inside', function () - - describe_all_test_variants( - aabb(vector(-1., 1.), vector(1., 0.5)), - aabb(vector(1., 1.), vector(1., 1.)), - nil, - true, - true - ) - end) - - describe('Case 10: Touch cover', function () - - describe_all_test_variants( - aabb(vector(-1., 0.), vector(1., 2.)), - aabb(vector(1., 0.), vector(1., 1.)), - nil, - true, - true - ) - end) - - describe('+ Case 11a: Overlap corner, priority left', function () - - describe_all_test_variants( - aabb(vector(0., 0.), vector(1., 1.)), - aabb(vector(1., 1.), vector(1., 1.)), - vector(-1., 0.), - false, - true, - directions.left - ) - end) - - describe('+ Case 11b: Overlap corner, priority up', function () - - describe_all_test_variants( - aabb(vector(0., 0.), vector(1., 1.)), - aabb(vector(1., 1.), vector(1., 1.)), - vector(0., -1.), - false, - true, - directions.up - ) - end) - - describe('+ Case 12: Overlap corner clear', function () - - describe_all_test_variants( - aabb(vector(0., 0.), vector(1., 2.)), - aabb(vector(1., 2.), vector(1., 2.)), - vector(-1., 0.), - false, - true - ) - end) - - describe('Case 13: Overlap full side', function () - - describe_all_test_variants( - aabb(vector(0., 1.), vector(1., 1.)), - aabb(vector(1., 1.), vector(1., 1.)), - vector(-1., 0.), - false, - true - ) - end) - - describe('Case 14: Overlap inside side', function () - - describe_all_test_variants( - aabb(vector(0., 1.), vector(1., 0.5)), - aabb(vector(1., 1.), vector(1., 1.)), - vector(-1., 0.), - false, - true - ) - end) - - describe('Case 15: Overlap cover side', function () - - describe_all_test_variants( - aabb(vector(0., 2.), vector(1., 2.)), - aabb(vector(1., 2.), vector(1., 1.)), - vector(-1., 0.), - false, - true - ) - end) - - describe('. Case 16a: Cover from corner, priority left', function () - describe_all_test_variants( - aabb(vector(0., 0.), vector(2., 2.)), - aabb(vector(1., 1.), vector(1., 1.)), - vector(-2., 0.), - false, - true, - directions.left - ) - end) - - describe('. Case 16b: Cover from corner, priority up', function () - describe_all_test_variants( - aabb(vector(0., 0.), vector(2., 2.)), - aabb(vector(1., 1.), vector(1., 1.)), - vector(0., -2.), - false, - true, - directions.up - ) - end) - - describe('. Case 17a: Cover from side, priority up', function () - describe_all_test_variants( - aabb(vector(1., 1.), vector(2., 1.)), - aabb(vector(1., 1.), vector(1., 1.)), - vector(0., -2.), - false, - true, - directions.up - ) - end) - - describe('. Case 17b: Cover from side, priority down', function () - describe_all_test_variants( - aabb(vector(1., 1.), vector(2., 1.)), - aabb(vector(1., 1.), vector(1., 1.)), - vector(0., 2.), - false, - true, - directions.down - ) - end) - - describe('. Case 18a: Cover from both sides, priority up', function () - - describe_all_test_variants( - aabb(vector(1., 1.), vector(2., 1.)), - aabb(vector(1., 1.), vector(1., 1.)), - vector(0., -2.), - false, - true, - directions.up - ) - end) - - describe('. Case 18b: Cover from both sides, priority down', function () - - describe_all_test_variants( - aabb(vector(1., 1.), vector(2., 1.)), - aabb(vector(1., 1.), vector(1., 1.)), - vector(0., 2.), - false, - true, - directions.down - ) - end) - - describe('Case 19: Cover from 3 sides escape top', function () - - describe_all_test_variants( - aabb(vector(1., 0.), vector(3., 2.)), - aabb(vector(1., 1.), vector(1., 1.)), - vector(0., -2.), - false, - true - ) - end) - - describe('. Case 20: Cover over, priority left', function () - - describe_all_test_variants( - aabb(vector(1., 1.), vector(2., 2.)), - aabb(vector(1., 1.), vector(1., 1.)), - vector(-3., 0.), - false, - true, - directions.left - ) - end) - - describe('. Case 20: Cover over, priority right', function () - - describe_all_test_variants( - aabb(vector(1., 1.), vector(2., 2.)), - aabb(vector(1., 1.), vector(1., 1.)), - vector(3., 0.), - false, - true, - directions.right - ) - end) - - describe('. Case 20: Cover over, priority up', function () - - describe_all_test_variants( - aabb(vector(1., 1.), vector(2., 2.)), - aabb(vector(1., 1.), vector(1., 1.)), - vector(0., -3.), - false, - true, - directions.up - ) - end) - - describe('. Case 20: Cover over, priority down', function () - - describe_all_test_variants( - aabb(vector(1., 1.), vector(2., 2.)), - aabb(vector(1., 1.), vector(1., 1.)), - vector(0., 3.), - false, - true, - directions.down - ) - end) - - describe('Case 21: Cover from corner clear', function () - - describe_all_test_variants( - aabb(vector(0., 1.), vector(2., 3.)), - aabb(vector(1., 2.), vector(1., 2.)), - vector(-2., 0.), - false, - true - ) - end) - - describe('Case 22: Cover from side clear', function () - - describe_all_test_variants( - aabb(vector(0., 2.), vector(2., 2.)), - aabb(vector(1., 2.), vector(1., 2.)), - vector(-2., 0.), - false, - true - ) - end) - - describe('Case 23: Cover from both sides clear', function () - - describe_all_test_variants( - aabb(vector(0., 2.), vector(3., 2.)), - aabb(vector(1., 2.), vector(1., 2.)), - vector(-3., 0.), - false, - true - ) - end) - - describe('Case 24: Cover from 3 sides escape lateral', function () - - describe_all_test_variants( - aabb(vector(0., 1.), vector(3., 3.)), - aabb(vector(1., 2.), vector(1., 2.)), - vector(-3., 0.), - false, - true - ) - end) - - describe('Case 25: Cover over clear', function () - - describe_all_test_variants( - aabb(vector(0., 1.), vector(3., 3.)), - aabb(vector(1., 1.), vector(1., 1.)), - vector(-3., 0.), - false, - true - ) - end) - - describe('. Case 26: Pierce over side', function () - - describe_all_test_variants( - aabb(vector(0., -0.5), vector(2., 0.5)), - aabb(vector(0., 0.), vector(1., 1.)), - vector(0., -1.), - false, - true - ) - end) - - describe('Case 27: Pierce over side thin', function () - - describe_all_test_variants( - aabb(vector(-1, 2.), vector(3., 2.)), - aabb(vector(0., 0.), vector(1., 4.)), - vector(-3., 0.), - false, - true - ) - end) - - describe('Case 28: Pierce through', function () - - describe_all_test_variants( - aabb(vector(0., 0.), vector(2., 0.5)), - aabb(vector(0., 1.), vector(1., 2.)), - vector(0., -1.5), - false, - true - ) - end) - - describe('Case 29: Pierce through thin', function () - - describe_all_test_variants( - aabb(vector(-1., 0.), vector(3., 0.5)), - aabb(vector(0., 0.), vector(1., 4.)), - vector(-3., 0.), - false, - true - ) - end) - - describe('Case 30: Pierce stop', function () - - describe_all_test_variants( - aabb(vector(-1., 0.), vector(2., 0.5)), - aabb(vector(0., 0.), vector(1., 4.)), - vector(-2., 0.), - false, - true - ) - end) - - describe('. Case 31: Perfect overlap, priority up', function () - - describe_all_test_variants( - aabb(vector(0., 0.), vector(2., 1.)), - aabb(vector(0., 0.), vector(2., 1.)), - vector(0., -2.), - false, - true, - directions.up - ) - end) - - describe('. Case 31: Perfect overlap, priority down', function () - - describe_all_test_variants( - aabb(vector(0., 0.), vector(2., 1.)), - aabb(vector(0., 0.), vector(2., 1.)), - vector(0., 2.), - false, - true, - directions.down - ) - end) - - describe('Case 32: Point outside', function () - - describe_all_test_variants( - aabb(vector(-2., -2.), vector(0., 0.)), - aabb(vector(1., 1.), vector(1., 1.)), - nil, - false, - false - ) - end) - - describe('Case 33: Point at corner', function () - - describe_all_test_variants( - aabb(vector(0., 0.), vector(0., 0.)), - aabb(vector(1., 1.), vector(1., 1.)), - nil, - true, - true - ) - end) - - describe('Case 34: Point on edge', function () - - describe_all_test_variants( - aabb(vector(0., 1.), vector(0., 0.)), - aabb(vector(1., 1.), vector(1., 1.)), - nil, - true, - true - ) - end) - - describe('Case 35: Point inside', function () - - describe_all_test_variants( - aabb(vector(1., 2.), vector(0., 0.)), - aabb(vector(2., 2.), vector(2., 2.)), - vector(-1., 0.), - false, - true - ) - end) - - end) - - end) - -end) diff --git a/src/engine/tests/utestcolor.lua b/src/engine/tests/utestcolor.lua deleted file mode 100644 index b1b6054f..00000000 --- a/src/engine/tests/utestcolor.lua +++ /dev/null @@ -1,65 +0,0 @@ -require("engine/test/bustedhelper") -require("engine/render/color") - -describe('color_tostring', function () - - it('should return the name of a known color by index', function () - assert.are_equal("dark_purple", color_tostring(2)) - end) - - it('should return the name of a known color by enum', function () - assert.are_equal("pink", color_tostring(colors.pink)) - end) - - it('should return "unknown color" for nil', function () - assert.are_equal("unknown color", color_tostring(nil)) - end) - - it('should return "unknown color" for -1', function () - assert.are_equal("unknown color", color_tostring(-1)) - end) - - it('should return "unknown color" for 16', function () - assert.are_equal("unknown color", color_tostring(16)) - end) - - it('should return "unknown color" for a table', function () - assert.are_equal("unknown color", color_tostring({})) - end) - -end) - -describe('set_unique_transparency', function () - - it('should set the passed color as the unique transparent color', function () - set_unique_transparency(12) - assert.are_same({ - [0] = false, false, false, false, - false, false, false, false, - false, false, false, false, - true, false, false, false}, - pico8.pal_transparent - ) - end) - - it('should return the name of a known color by enum', function () - assert.are_equal("pink", color_tostring(colors.pink)) - end) - - it('should return "unknown color" for nil', function () - assert.are_equal("unknown color", color_tostring(nil)) - end) - - it('should return "unknown color" for -1', function () - assert.are_equal("unknown color", color_tostring(-1)) - end) - - it('should return "unknown color" for 16', function () - assert.are_equal("unknown color", color_tostring(16)) - end) - - it('should return "unknown color" for a table', function () - assert.are_equal("unknown color", color_tostring({})) - end) - -end) diff --git a/src/engine/tests/utestcoroutine.lua b/src/engine/tests/utestcoroutine.lua deleted file mode 100644 index 569e88e2..00000000 --- a/src/engine/tests/utestcoroutine.lua +++ /dev/null @@ -1,28 +0,0 @@ -require("engine/test/bustedhelper") -require("engine/core/coroutine") - -describe('coroutine_curry', function () - - local function test_fun_async_with_args(var1, var2) - end - - describe('_init', function () - it('should initialize a coroutine curry with some arguments', function () - local t = {} - local curry = coroutine_curry(cocreate(test_fun_async_with_args), 5, t) - assert.are_equal(2, #curry.args) - assert.are_equal(5, curry.args[1]) - assert.are_equal(t, curry.args[2]) - end) - end) - - describe('_tostring', function () - it('should return "[coroutine_curry] (status) (arg1 arg2 ...)"', function () - local my_couroutine_curry = coroutine_curry(cocreate(test_fun_async_with_args), 5, {}) - assert.are_equal("[coroutine_curry] (suspended) (5, [table])", my_couroutine_curry:_tostring()) - coresume(my_couroutine_curry.coroutine) - assert.are_equal("[coroutine_curry] (dead) (5, [table])", my_couroutine_curry:_tostring()) - end) - end) - -end) diff --git a/src/engine/tests/utestdatastruct.lua b/src/engine/tests/utestdatastruct.lua deleted file mode 100644 index b16344f6..00000000 --- a/src/engine/tests/utestdatastruct.lua +++ /dev/null @@ -1,510 +0,0 @@ -require("engine/test/bustedhelper") -require("engine/core/datastruct") - -describe('circular_buffer', function () - - describe('_init', function () - - it('should create an empty circular buffer with given max length', function () - local cb = circular_buffer(3) - -- interface - assert.is_not_nil(cb) - assert.are_equal(0, #cb) - -- implementation - assert.are_equal(3, cb.max_length) - assert.are_same({}, cb._buffer) - assert.are_equal(1, cb._start_index) - end) - - describe('_tostring', function () - - it('should return circular_buffer({self,max_length}, {} for an empty buffer)', function () - local cb = circular_buffer(3) - assert.are_equal("circular_buffer(3, {})", cb:_tostring()) - end) - - it('should return circular_buffer({self,max_length}, {content, ...})', function () - local cb = circular_buffer(3) - cb:push(1) - cb:push(2) - cb:push(10) - assert.are_equal("circular_buffer(3, {1, 2, 10})", cb:_tostring()) - end) - - end) - - end) - - describe('__eq', function () - - it('should return true when the buffer have exactly the same content for the same max length', function () - local cb1 = circular_buffer(3) - cb1:push(1) - cb1:push(2) - cb1:push(10) - local cb2 = circular_buffer(3) - cb2:push(1) - cb2:push(2) - cb2:push(10) - assert.is_true(cb1:__eq(cb2)) - end) - - it('should return true when the buffer have the same circular content for the same max length', function () - local cb1 = circular_buffer(3) - cb1:push(1) - cb1:push(2) - cb1:push(10) - local cb2 = circular_buffer(3) - cb2:push(999) - cb2:push(999) - cb2:push(1) - cb2:push(2) - cb2:push(10) - assert.is_true(cb1:__eq(cb2)) - end) - - it('should return false when the contents are different for the same buffer size', function () - local cb1 = circular_buffer(3) - cb1:push(1) - cb1:push(2) - cb1:push(10) - local cb2 = circular_buffer(3) - cb2:push(1) - cb2:push(2) - cb2:push(999) - assert.is_false(cb1:__eq(cb2)) - end) - - it('should return false when the buffer sizes are different', function () - local cb1 = circular_buffer(4) - cb1:push(1) - cb1:push(2) - cb1:push(10) - local cb2 = circular_buffer(4) - cb2:push(1) - cb2:push(2) - cb2:push(10) - cb2:push(999) - assert.is_false(cb1:__eq(cb2)) - end) - - it('should return false when the max lengths are different', function () - local cb1 = circular_buffer(3) - cb1:push(1) - cb1:push(2) - cb1:push(10) - local cb2 = circular_buffer(4) - cb2:push(1) - cb2:push(2) - cb2:push(10) - cb2:push(999) - assert.is_false(cb1:__eq(cb2)) - end) - - it('should return false when the other member is not a circular buffer', function () - local cb1 = circular_buffer(3) - cb1:push(1) - cb1:push(2) - cb1:push(10) - local cb2 = { - 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 - } - assert.is_false(cb1:__eq(cb2)) - end) - - end) - - describe('__len', function () - - it('#(^) => 0', function () - local cb = circular_buffer(2) - assert.are_equal(0, #cb) - end) - - it('#(^1, 2) => 2', function () - local cb = circular_buffer(2) - cb:push(1) - cb:push(2) - assert.are_equal(2, #cb) - end) - - it('#(3, ^2) => 2', function () - local cb = circular_buffer(2) - cb:push(1) - cb:push(2) - cb:push(3) - assert.are_equal(2, #cb) - end) - - end) - - describe('__ipairs', 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) - - it('(^1): 1 iteration', function () - local cb = circular_buffer(2) - cb:push(10) - local count = 0 - local result_ipairs = {} - for i, v in ipairs(cb) do - count = count + 1 - result_ipairs[count] = {i, v} - end - assert.are_same({{1, 10}}, result_ipairs) - end) - - it('(4, ^2, 3): iterate 3 times from 2 to 4, cycling', function () - local cb = circular_buffer(3) - cb:push(10) - cb:push(20) - cb:push(30) - cb:push(40) - local count = 0 - local result_ipairs = {} - for i, v in ipairs(cb) do - count = count + 1 - result_ipairs[count] = {i, v} - end - assert.are_same({{1, 20}, {2, 30}, {3, 40}}, result_ipairs) - end) - - end) - - describe('_stateless_iter', function () - - it('(^):_stateless_iter() => nil', function () - local cb = circular_buffer(2) - assert.is_nil(cb:_stateless_iter(0)) - end) - - it('(^1):_stateless_iter() => 1, nil', function () - local cb = circular_buffer(2) - cb:push(1) - assert.are_same({1, 1}, {cb:_stateless_iter(0)}) - assert.is_nil(cb:_stateless_iter(1)) - end) - - it('(4, ^2, 3):_stateless_iter() => 2, 3, 4, nil', function () - local cb = circular_buffer(3) - cb:push(1) - cb:push(2) - cb:push(3) - cb:push(4) - assert.are_same({1, 2}, {cb:_stateless_iter(0)}) - assert.are_same({2, 3}, {cb:_stateless_iter(1)}) - assert.are_same({3, 4}, {cb:_stateless_iter(2)}) - assert.is_nil(cb:_stateless_iter(3)) - end) - - end) - - describe('_rotate_indice', function () - - it('_rotate_indice(-2, 3) => 1', function () - assert.are_equal(1, circular_buffer._rotate_indice(-2, 3)) - end) - - it('_rotate_indice(-1, 3) => 2', function () - assert.are_equal(2, circular_buffer._rotate_indice(-1, 3)) - end) - - it('_rotate_indice(0, 3) => 3', function () - assert.are_equal(3, circular_buffer._rotate_indice(0, 3)) - end) - - it('_rotate_indice(1, 3) => 1', function () - assert.are_equal(1, circular_buffer._rotate_indice(1, 3)) - end) - - it('_rotate_indice(2, 3) => 2', function () - assert.are_equal(2, circular_buffer._rotate_indice(2, 3)) - end) - - it('_rotate_indice(3, 3) => 3', function () - assert.are_equal(3, circular_buffer._rotate_indice(3, 3)) - end) - - it('_rotate_indice(4, 3) => 1', function () - assert.are_equal(1, circular_buffer._rotate_indice(4, 3)) - end) - - end) - - describe('get (oldest indicated by ^)', function () - - it('(^1, 2, 3):get(1) => 1', function () - local cb = circular_buffer(3) - cb:push(1) - cb:push(2) - cb:push(3) - assert.are_equal(1, cb:get(1)) - end) - - it('(^1, 2, 3):get(3) => 3', function () - local cb = circular_buffer(3) - cb:push(1) - cb:push(2) - cb:push(3) - assert.are_equal(3, cb:get(3)) - end) - - it('(4, ^2, 3):get(1) => 2', function () - local cb = circular_buffer(3) - cb:push(1) - cb:push(2) - cb:push(3) - cb:push(4) - assert.are_equal(2, cb:get(1)) - end) - - it('(4, ^2, 3):get(3) => 4', function () - local cb = circular_buffer(3) - cb:push(1) - cb:push(2) - cb:push(3) - cb:push(4) - assert.are_equal(4, cb:get(3)) - end) - - it('(^1, 2):get(-1) => 2', function () - local cb = circular_buffer(3) - cb:push(1) - cb:push(2) - assert.are_equal(2, cb:get(-1)) - end) - - it('(^1, 2):get(-2) => 1', function () - local cb = circular_buffer(3) - cb:push(1) - cb:push(2) - assert.are_equal(1, cb:get(-2)) - end) - - it('(4, ^2, 3):get(-1) => 4', function () - local cb = circular_buffer(3) - cb:push(1) - cb:push(2) - cb:push(3) - cb:push(4) - assert.are_equal(4, cb:get(-1)) - end) - - it('(^1, 2, 3):get(0) => nil', function () - local cb = circular_buffer(3) - cb:push(1) - cb:push(2) - cb:push(3) - assert.is_nil(cb:get(0)) - end) - - it('(^1, 2, 3):get(4) => nil', function () - local cb = circular_buffer(3) - cb:push(1) - cb:push(2) - cb:push(3) - assert.is_nil(cb:get(4)) - end) - - it('(^1, 2, 3):get(-4) => nil', function () - local cb = circular_buffer(3) - cb:push(1) - cb:push(2) - cb:push(3) - assert.is_nil(cb:get(-4)) - end) - - end) - - describe('is_filled', function () - - it('should return true when the max length has been reached', function () - local cb = circular_buffer(3) - cb:push(1) - cb:push(2) - cb:push(10) - assert.is_true(cb:is_filled()) - end) - - it('should return true when the max length has been reached and some data overriden', function () - local cb = circular_buffer(3) - cb:push(1) - cb:push(2) - cb:push(10) - cb:push(999) - assert.is_true(cb:is_filled()) - end) - - it('should return false when the buffer is empty', function () - local cb = circular_buffer(3) - assert.is_false(cb:is_filled()) - end) - - it('should return false when the buffer has some elements but max length is not reached', function () - local cb = circular_buffer(3) - cb:push(1) - cb:push(2) - assert.is_false(cb:is_filled()) - end) - - end) - - describe('push', function () - - local cb = circular_buffer(3) - - after_each(function () - cb:clear() - end) - - describe('(when buffer is empty)', function () - - it('should add a new element at index 1', function () - local has_replaced = cb:push(1) - -- interface - assert.is_false(has_replaced) - assert.are_equal(1, #cb) - assert.are_equal(1, cb:get(1)) - -- implementation - assert.are_same({1}, cb._buffer) - assert.are_equal(1, cb._start_index) - end) - - end) - - describe('(when buffer has some entries but is not full)', function () - - setup(function () - cb:push(1) - cb:push(2) - end) - - it('should add a new element at the next index', function () - local has_replaced = cb:push(3) - -- interface - assert.is_false(has_replaced) - assert.are_equal(3, #cb) - assert.are_equal(1, cb:get(1)) - assert.are_equal(2, cb:get(2)) - assert.are_equal(3, cb:get(3)) - -- implementation - assert.are_same({1, 2, 3}, cb._buffer) - assert.are_equal(1, cb._start_index) - end) - - end) - - describe('(when buffer is full)', function () - - setup(function () - cb:push(1) - cb:push(2) - cb:push(3) - end) - - it('should replace the oldest element, moving oldest to the next element ', function () - local has_replaced = cb:push(4) - -- interface - assert.is_true(has_replaced) - assert.are_equal(3, #cb) - assert.are_equal(2, cb:get(1)) - assert.are_equal(3, cb:get(2)) - assert.are_equal(4, cb:get(3)) - -- implementation - assert.are_same({4, 2, 3}, cb._buffer) - assert.are_equal(2, cb._start_index) - end) - - end) - - describe('(when buffer is full again)', function () - - setup(function () - cb:push(1) - cb:push(2) - cb:push(3) - cb:push(4) - cb:push(5) - cb:push(6) - end) - - it('should replace the 1st, oldest element again, moving oldest to the next element ', function () - local has_replaced = cb:push(7) - -- interface - assert.is_true(has_replaced) - assert.are_equal(3, #cb) - assert.are_equal(5, cb:get(1)) - assert.are_equal(6, cb:get(2)) - assert.are_equal(7, cb:get(3)) - -- implementation - assert.are_same({7, 5, 6}, cb._buffer) - assert.are_equal(2, cb._start_index) - end) - - end) - - end) - - describe('clear', function () - - describe('(when buffer is empty)', function () - - it('should do nothing', function () - local cb = circular_buffer(3) - cb:clear() - -- interface - assert.are_equal(0, #cb) - -- implementation - assert.are_same({}, cb._buffer) - assert.are_equal(1, cb._start_index) - end) - - end) - - describe('(when buffer has some entries but is not full)', function () - - it('should clear the elements and oldest index', function () - local cb = circular_buffer(3) - cb:push(1) - cb:clear() - -- interface - assert.are_equal(0, #cb) - -- implementation - assert.are_same({}, cb._buffer) - assert.are_equal(1, cb._start_index) - end) - - end) - - describe('(when buffer is full)', function () - - it('should clear all elements and oldest index', function () - local cb = circular_buffer(3) - cb:push(1) - cb:push(2) - cb:push(3) - cb:clear() - -- interface - assert.are_equal(0, #cb) - -- implementation - assert.are_same({}, cb._buffer) - assert.are_equal(1, cb._start_index) - end) - - end) - - end) - -end) diff --git a/src/engine/tests/utestdebug_window.lua b/src/engine/tests/utestdebug_window.lua deleted file mode 100644 index 1f0b0e11..00000000 --- a/src/engine/tests/utestdebug_window.lua +++ /dev/null @@ -1,94 +0,0 @@ -require("engine/test/bustedhelper") -local debug_window = require("engine/debug/debug_window") -local wtk = require("engine/wtk/pico8wtk") - -describe('debug_window', function () - - after_each(function () - debug_window:init() - end) - - describe('init', function () - - it('should initialize the gui root, invisible', function () - assert.is_not_nil(debug_window.gui) - assert.is_false(debug_window.gui.visible) - end) - - end) - - describe('show', function () - - it('should make the gui root visible', function () - debug_window.gui.visible = false -- in case the default changes - debug_window:show() - assert.is_true(debug_window.gui.visible) - end) - - end) - - describe('hide', function () - - it('should make the gui root invisible', function () - debug_window.gui.visible = true - debug_window:hide() - assert.is_false(debug_window.gui.visible) - end) - - end) - - describe('update', function () - - it('should call gui.update', function () - local update_stub = stub(debug_window.gui, "update") - debug_window:update() - assert.spy(update_stub).was_called(1) - assert.spy(update_stub).was_called_with(match.ref(debug_window.gui)) - end) - - end) - - describe('render_window', function () - - local draw_stub - - setup(function () - draw_stub = stub(debug_window.gui, "draw") - end) - - teardown(function () - draw_stub:revert() - end) - - it('should reset camera and call gui.draw', function () - debug_window:render() - assert.are_same({0, 0}, {pico8.camera_x, pico8.camera_y}) - assert.spy(draw_stub).was_called(1) - assert.spy(draw_stub).was_called_with(match.ref(debug_window.gui)) - end) - - end) - - describe('add_label', function () - - local add_child_stub - - setup(function () - add_child_stub = stub(debug_window.gui, "add_child") - end) - - teardown(function () - add_child_stub:revert() - end) - - - it('should call gui.add_child, passing a label(text, color) at position (x, y)', function () - debug_window:add_label("hello", 5, 12, 45) - assert.spy(add_child_stub).was_called(1) - local label = wtk.label.new("hello", 5) -- will be matched by table content - assert.spy(add_child_stub).was_called_with(match.ref(debug_window.gui), label, 12, 45) - end) - - end) - -end) diff --git a/src/engine/tests/utestflow.lua b/src/engine/tests/utestflow.lua deleted file mode 100644 index 7ceb2a19..00000000 --- a/src/engine/tests/utestflow.lua +++ /dev/null @@ -1,368 +0,0 @@ -require("engine/test/bustedhelper") -local flow = require("engine/application/flow") -local helper = require("engine/core/helper") - -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}) - end) - - describe('update', function () - - it('should not crash when there is no current gamestate not next gamestate set', function () - assert.has_no_errors(function () flow:update() end) - end) - - end) - - describe('add_gamestate', function () - - after_each(function () - clear_table(flow.gamestates) - end) - - it('should add a gamestate', function () - 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 () - assert.has_error(function () - flow:add_gamestate(nil) - end, - "flow:add_gamestate: passed gamestate is nil") - end) - - end) - - describe('(mock gamestate 1 added)', function () - - setup(function () - flow:add_gamestate(mock_gamestate1) - end) - - teardown(function () - clear_table(flow.gamestates) - end) - - describe('query_gamestate_type', function () - - after_each(function () - flow.next_state = nil - end) - - it('should query a new gamestate with the correct type', function () - 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(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 () - assert.has_error(function () - flow:query_gamestate_type(nil) - end, - "flow:query_gamestate_type: passed gamestate_type is nil") - end) - - describe('(mock gamestate 1 entered)', function () - - before_each(function () - flow.curr_state = mock_gamestate1 - end) - - after_each(function () - flow.curr_state = nil - end) - - it('should assert if the same gamestate type as the current one is passed', function () - assert.has_error(function () - flow:query_gamestate_type(mock_gamestate1.type) - end, - "flow:query_gamestate_type: cannot query the current gamestate type 'mock1' itself") - end) - - end) - - end) - - describe('query_gamestate_type', function () - - before_each(function () - flow:query_gamestate_type(mock_gamestate1.type) - end) - - after_each(function () - flow.next_state = nil - end) - - describe('_check_next_state', function () - - before_each(function () - flow:_check_next_state() - end) - - after_each(function () - -- 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(mock_gamestate1.type, flow.curr_state.type) - end) - - it('should enter a new gamestate with the correct reference', function () - assert.are_equal(flow.gamestates[mock_gamestate1.type], flow.curr_state) - end) - - it('should clear the next gamestate query', function () - assert.is_nil(flow.next_state) - end) - - end) - - describe('update', function () - - before_each(function () - flow:update() - end) - - after_each(function () - -- 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(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[mock_gamestate1.type], flow.curr_state) - end) - - it('via _check_next_state hence clear the next gamestate query', function () - assert.is_nil(flow.next_state) - end) - - end) - - describe('_change_state', function () - - after_each(function () - if flow.curr_state then - -- no need to on_exit current state, we know our mock states do nothing - flow.curr_state = nil - end - end) - - it('should assert if a nil gamestate is passed', function () - assert.has_error(function () - flow:_change_state(nil) - end, - "flow:_change_state: cannot change to nil gamestate") - end) - - it('should directly enter a gamestate', function () - 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(mock_gamestate1) - assert.is_nil(flow.next_state) - end) - - end) - - describe('change_gamestate_by_type (utest only)', function () - - - setup(function () - spy.on(flow, "_change_state") - end) - - teardown(function () - flow._change_state:revert() - end) - - after_each(function () - if flow.curr_state then - -- no need to on_exit current state, we know our mock states do nothing - flow.curr_state = nil - end - flow._change_state:clear() - end) - - it('should assert if an invalid gamestate type is passed', function () - assert.has_error(function () - flow:change_gamestate_by_type('invalid') - end, - "flow:change_gamestate_by_type: gamestate type 'invalid' has not been added to the flow gamestates") - end) - - it('should directly enter a gamestate by type', function () - 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(mock_gamestate1)) - -- interface - assert.are_equal(flow.gamestates[mock_gamestate1.type], flow.curr_state) - assert.are_equal(mock_gamestate1.type, flow.curr_state.type) - end) - - end) - - end) - - describe('_change_state 1st time', function () - local mock_gamestate1_on_enter_stub - - setup(function () - mock_gamestate1_on_enter_stub = stub(mock_gamestate1, "on_enter") - end) - - teardown(function () - mock_gamestate1_on_enter_stub:revert() - end) - - before_each(function () - flow:_change_state(mock_gamestate1) - end) - - after_each(function () - flow.curr_state = nil - mock_gamestate1_on_enter_stub:clear() - end) - - it('should directly enter a gamestate', function () - assert.are_equal(flow.gamestates[mock_gamestate1.type], flow.curr_state) - end) - - it('should call the gamestate:on_enter', function () - 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('(mock gamestate 2 added)', function () - - setup(function () - flow:add_gamestate(mock_gamestate2) - end) - - teardown(function () - flow.gamestates[mock_gamestate2.type] = nil - end) - - describe('_change_state 2nd time', function () - local mock_gamestate1_on_exit_stub - local mock_gamestate2_on_enter_stub - - setup(function () - mock_gamestate1_on_exit_stub = stub(mock_gamestate1, "on_exit") - mock_gamestate2_on_enter_stub = stub(mock_gamestate2, "on_enter") - end) - - teardown(function () - mock_gamestate1_on_exit_stub:revert() - mock_gamestate2_on_enter_stub:revert() - end) - - before_each(function () - flow:_change_state(mock_gamestate2) - end) - - after_each(function () - 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[mock_gamestate2.type], flow.curr_state) - end) - - it('should call the old gamestate:on_exit', function () - 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(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) -- (mock_gamestate2 gamestate added) - - end) -- changed gamestate 1st time - - end) -- (mock_gamestate1 gamestate added) - - describe('render', function () - - it('should not crash when there is no current gamestate not next gamestate set', function () - assert.has_no_errors(function () flow:render() end) - end) - - describe('(when current gamestate is set)', function () - - local mock_gamestate1_render_stub - - setup(function () - mock_gamestate1_render_stub = stub(mock_gamestate1, "render") - end) - - teardown(function () - mock_gamestate1_render_stub:revert() - end) - - before_each(function () - flow:_change_state(mock_gamestate1) - end) - - after_each(function () - flow.curr_state:on_exit() - flow.curr_state = nil - mock_gamestate1_render_stub:clear() - end) - - it('should not delegate render to current gamestate', function () - flow:render() - assert.spy(mock_gamestate1_render_stub).was_called(1) - assert.spy(mock_gamestate1_render_stub).was_called_with(match.ref(mock_gamestate1)) - end) - - end) - - end) - -end) diff --git a/src/engine/tests/utestgameapp.lua b/src/engine/tests/utestgameapp.lua deleted file mode 100644 index 910a559b..00000000 --- a/src/engine/tests/utestgameapp.lua +++ /dev/null @@ -1,270 +0,0 @@ -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/utesthelper.lua b/src/engine/tests/utesthelper.lua deleted file mode 100644 index 00d141d6..00000000 --- a/src/engine/tests/utesthelper.lua +++ /dev/null @@ -1,446 +0,0 @@ -require("engine/test/bustedhelper") -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 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"}) - end) -end) - -describe('get_members', function () - it('should return module members from their names as multiple values', 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) - -describe('is_empty', function () - it('return true if the table is empty', function () - assert.is_true(is_empty({})) - end) - it('return false if the sequence is not empty', function () - assert.is_false(is_empty({2, "ah"})) - end) - it('return false if the table has only non-sequence entries', function () - assert.is_false(is_empty({a = "str"})) - end) - it('return false if the table has a mix of entries', function () - assert.is_false(is_empty({4, 5, d = "dummy"})) - end) -end) - -describe('are_same', function () - local single_t = {} - - local comparable_mt_sum = { - __eq = function (lhs, rhs) - -- 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} - 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({}, {})) - end) - it('return true if both tables are sequences with the same elements in order', function () - assert.is_true(are_same({false, "ah"}, {false, "ah"})) - end) - it('return true if both tables are sequences with the same elements by ref in order', function () - assert.is_true(are_same({2, single_t}, {2, single_t})) - end) - it('return true if both tables are former sequences with a hole with the same elements in order', function () - assert.is_true(are_same({2, nil, "ah"}, {2, nil, "ah"})) - end) - it('return true if both tables have the same keys and values', function () - assert.is_true(are_same({a = "str", b = "at"}, {b = "at", a = "str"})) - end) - it('return true if both tables have the same keys and values by reference', function () - assert.is_true(are_same({a = "str", b = single_t, c = nil}, {b = single_t, c = nil, a = "str"})) - end) - it('return true if both tables have the same keys and values', function () - assert.is_true(are_same({a = false, b = "at"}, {b = "at", a = false})) - end) - it('return true if both tables have the same keys and values by custom equality', function () - assert.is_true(are_same({a = "str", b = comparable_struct1}, {b = comparable_struct2, a = "str"})) - end) - it('return true if both tables have the same keys and values, even if their metatables differ', function () - local t1 = {} - setmetatable(t1, {}) - local t2 = {} - assert.is_true(are_same(t1, t2)) - end) - it('return false if both tables are sequences but an element is missing on the first', function () - assert.is_false(are_same({1, 2}, {1, 2, 3})) - end) - it('return false if both tables are sequences but an element is missing on the second', function () - assert.is_false(are_same({1, 2, 3}, {1, 2})) - end) - it('return false if both tables are sequences but an element differs', function () - assert.is_false(are_same({1, 2, 3}, {1, 2, 4})) - end) - it('return false if both tables are sequences with the same elements by value at deep level', function () - assert.is_true(are_same({1, 2, {}}, {1, 2, {}})) - end) - it('return false if first table has a key the other doesn\'t have', function () - assert.is_false(are_same({a = false, b = "at"}, {a = false})) - end) - it('return false if second table has a key the other doesn\'t have', function () - assert.is_false(are_same({b = "the"}, {c = 54, b = "the"})) - end) - it('return false if both tables have the same keys but a value differs', function () - assert.is_false(are_same({a = false, b = "at"}, {a = false, b = "the"})) - end) - it('return true if both tables have the same keys and values by value', function () - assert.is_true(are_same({a = "str", t = {}}, {a = "str", t = {}})) - end) - it('return false if both tables have the same values but a key differs by reference', function () - assert.is_false(are_same({[{20}] = 10}, {[{20}] = 10})) - end) - it('return true if both tables have the same key refs and value contents by defined equality', function () - assert.is_true(are_same({a = "str", t = {e = vector(5, 8)}}, {a = "str", t = {e = vector(5, 8)}}, true)) - end) - it('return false if we don\'t compare_raw_content and some values have the same content but differ by type', function () - assert.is_false(are_same({x = 5, y = 8}, vector(5, 8))) - end) - it('return false if we don\'t compare_raw_content and some values have the same content but differ by type (deep)', function () - assert.is_false(are_same({a = "str", t = {e = {x = 5, y = 8}}}, {a = "str", t = {e = vector(5, 8)}})) - end) - it('return true if we compare_raw_content and some values have the same content, even if they differ by type (deep)', function () - assert.is_true(are_same({x = 5, y = 8}, vector(5, 8), true)) - end) - it('return true if we compare_raw_content and some values have the same content, even if they differ by type (deep)', function () - assert.is_true(are_same({{x = 1, y = 2}, t = {e = {x = 5, y = 8}}}, {vector(1, 2), t = {e = vector(5, 8)}}, true)) - end) - it('return false if we compare_raw_content and some values have the same content, but they differ by type at a deep level', function () - assert.is_false(are_same({{x = 1, y = 2}, t = {e = {x = 5, y = 8}}}, {vector(1, 2), t = {e = vector(5, 8)}}, true, true)) - end) -end) - -describe('clear_table', function () - it('should clear a sequence', function () - local t = {1, 5, -5} - clear_table(t) - assert.are_equal(0, #t) - end) - it('should clear a table', function () - local t = {1, 5, a = "b", b = 50.1} - clear_table(t) - assert.is_true(is_empty(t)) - end) -end) - -describe('unpack', function () - it('should unpack a sequence fully by default', function () - local function foo(a, b, c) - assert.are_same({1, "foo", 20.2}, {a, b, c}) - end - foo(unpack({1, "foo", 20.2})) - end) - it('should unpack a sequence from start if from is not passed', function () - local function foo(a, b, c, d) - assert.are_same({1, "foo", 20.2}, {a, b, c}) - assert.are_not_equal(50, d) - end - foo(unpack({1, "foo", 20.2, 50}, nil, 3)) - end) - it('should unpack a sequence to the end if to is not passed', function () - local function foo(a, b, c) - assert.are_same({1, "foo", 20.2}, {a, b, c}) - end - foo(unpack({45, 1, "foo", 20.2}, 2)) - end) - it('should unpack a sequence from from to to', function () - local function foo(a, b, c, d) - assert.are_same({1, "foo", 20.2}, {a, b, c}) - assert.are_not_equal(50, d) - end - foo(unpack({45, 1, "foo", 20.2, 50}, 2, 4)) - 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('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('"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('"-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 () - string_tonum(nil) - end, - "bad argument #1 to 'sub' (string expected, got nil)") - end) - it('true => error', function () - assert.has_error(function () - string_tonum(true) - end, - "bad argument #1 to 'sub' (string expected, got boolean)") - end) - it('{} => error', function () - assert.has_error(function () - string_tonum({}) - end, - "bad argument #1 to 'sub' (string expected, got table)") - end) -end) - -describe('stringify', function () - it('nil => "[nil]"', function () - assert.are_equal("[nil]", stringify(nil)) - end) - it('"string" => "string"', function () - assert.are_equal("string", stringify("string")) - end) - it('true => "true"', function () - assert.are_equal("true", stringify(true)) - end) - it('false => "false"', function () - assert.are_equal("false", stringify(false)) - end) - it('56 => "56"', function () - assert.are_equal("56", stringify(56)) - end) - it('56.2 => "56.2"', function () - assert.are_equal("56.2", stringify(56.2)) - end) - it('vector(2 3) => "vector(2 3)" (_tostring implemented)', function () - assert.are_equal("vector(2, 3)", stringify(vector(2, 3))) - end) - it('{} => "[table]" (_tostring not implemented)', function () - assert.are_equal("[table]", stringify({})) - end) - it('function => "[function]"', function () - local f = function () - end - assert.are_equal("[function]", stringify(f)) - end) -end) - -describe('joinstr_table', function () - it('joinstr_table("_" {nil 5 "at" nil}) => "[nil]_5_at"', function () - assert.are_equal("[nil]_5_at", joinstr_table("_", {nil, 5, "at", nil})) - end) - it('joinstr_table("comma " nil 5 "at" {}) => "[nil]comma 5comma atcomma [table]"', function () - assert.are_equal("[nil], 5, at, [table]", joinstr_table(", ", {nil, 5, "at", {}})) - end) -end) - -describe('joinstr', function () - it('joinstr("" nil 5 "at" nil) => "[nil]5at"', function () - assert.are_equal("[nil]5at", joinstr("", nil, 5, "at", nil)) - end) - it('joinstr("comma " nil 5 "at" {}) => "[nil]comma 5comma atcomma [table]"', function () - assert.are_equal("[nil], 5, at, [table]", joinstr(", ", nil, 5, "at", {})) - end) -end) - -describe('wwrap', function () - it('+ wwrap("hello", 5) => "hello"', function () - assert.are_equal("hello", wwrap("hello", 5)) - end) - it('+ wwrap("hello world", 5) => "hello\nworld"', function () - assert.are_equal("hello\nworld", wwrap("hello world", 5)) - end) - it('+ wwrap("hello world", 10) => "hello\nworld"', function () - assert.are_equal("hello\nworld", wwrap("hello world", 10)) - end) - it('wwrap("hello world", 11) => "hello world"', function () - assert.are_equal("hello world", wwrap("hello world", 11)) - end) - it('+ wwrap("toolongfromthestart", 5) => "toolongfromthestart" (we can\'t warp at all, give up)', function () - assert.are_equal("toolongfromthestart", wwrap("toolongfromthestart", 5)) - end) - it('wwrap("toolongfromthestart this is okay", 5) => "toolongfromthestart\nthis\nis\nokay" (we can\'t warp at all, give up)', function () - assert.are_equal("toolongfromthestart\nthis\nis\nokay", wwrap("toolongfromthestart this is okay", 5)) - end) - it('wwrap("hello\nworld", 5) => "hello\nworld"', function () - assert.are_equal("hello\nworld", wwrap("hello\nworld", 5)) - end) - it('wwrap("hello world\nhow are you today?", 8) => "hello\nworld\nhow are\nyou\ntoday?"', function () - assert.are_equal("hello\nworld\nhow are\nyou\ntoday?", wwrap("hello world\nhow are you today?", 8)) - end) - it('wwrap("short\ntoolongfromthestart\nshort again", 8) => "short\ntoolongfromthestart\nshort\nagain"', function () - assert.are_equal("short\ntoolongfromthestart\nshort\nagain", wwrap("short\ntoolongfromthestart\nshort again", 8)) - end) -end) - -describe('strspl', function () - it('strspl("", " ") => {""}', function () - assert.are_same({}, strspl("", " ")) - end) - it('strspl("hello", " ") => {"hello"}', function () - assert.are_same({"hello"}, strspl("hello", " ")) - end) - it('strspl("hello world", " ") => {"hello", "world"}', function () - assert.are_same({"hello", "world"}, strspl("hello world", " ")) - end) - it('strspl("hello world", "l") => {"he", "", "o wor", "d"} (multiple separators leave empty strings)', function () - assert.are_same({"he", "", "o wor", "d"}, strspl("hello world", "l")) - end) - it('strspl("hello\nworld", "\n") => {"hello", "world"}', function () - assert.are_same({"hello", "world"}, strspl("hello\nworld", "\n")) - end) - it('strspl("hello world", "lo") => {"hello world"} (multicharacter not supported)', function () - assert.are_same({"hello world"}, strspl("hello world", "lo")) - end) - it('strspl("|a||b", "|", false) => {"", a", "", "b"}', function () - assert.are_same({"", "a", "", "b"}, strspl("|a||b", "|", false)) - end) - it('strspl("|a||b", "|", true) => {"a", "b"}', function () - assert.are_same({"a", "b"}, strspl("|a||b", "|", true)) - end) -end) - -describe('yield_delay (wrapped in set_var_after_delay_async)', function () - local test_var - local coroutine - - local function set_var_after_delay_async(delay) - yield_delay(delay) - test_var = 1 - end - - before_each(function () - test_var = 0 - coroutine = cocreate(set_var_after_delay_async) - end) - - it('should start suspended', function () - assert.are_equal("suspended", costatus(coroutine)) - assert.are_equal(0, test_var) - end) - - it('should not stop after 59/60 frames (for a delay of 1s)', function () - coresume(coroutine, 1.0) -- pass delay of 60 frames in 1st call - for t = 2, 1.0 * fps - 1 do - coresume(coroutine) -- further calls don't need arg, it's only used as yield() return value - end - assert.are_equal("suspended", costatus(coroutine)) - assert.are_equal(0, test_var) - end) - it('should stop after the 60th frame, and continue body execution', function () - coresume(coroutine, 1.0) - for t=2, 1.0 * fps do - coresume(coroutine) - end - assert.are_equal("dead", costatus(coroutine)) - assert.are_equal(1, test_var) - end) - - it('should not stop after 60/60.6 frames (for a delay of 1.01s)', function () - coresume(coroutine, 1.01) -- pass delay of 60.6 frames in 1st call - for t=2, 1.0 * fps do - coresume(coroutine) - end - assert.are_equal("suspended", costatus(coroutine)) - assert.are_equal(0, test_var) - end) - it('should stop after the 61th frame (ceil of 60.6), and continue body execution', function () - coresume(coroutine, 1.01) -- pass delay of 60.6 frames in 1st call - for t=2, 1.0 * fps + 1 do - coresume(coroutine) - end - assert.are_equal("dead", costatus(coroutine)) - assert.are_equal(1, test_var) - end) - -end) diff --git a/src/engine/tests/utestinput.lua b/src/engine/tests/utestinput.lua deleted file mode 100644 index cb72b024..00000000 --- a/src/engine/tests/utestinput.lua +++ /dev/null @@ -1,629 +0,0 @@ -require("engine/test/bustedhelper") -local input = require("engine/input/input") - -describe('input', function () - - describe('generate_initial_btn_states', function () - - it('should return a table of released button states for each input key', function () - assert.are_same({ - [button_ids.left] = btn_states.released, - [button_ids.right] = btn_states.released, - [button_ids.up] = btn_states.released, - [button_ids.down] = btn_states.released, - [button_ids.o] = btn_states.released, - [button_ids.x] = btn_states.released - }, - generate_initial_btn_states()) - end) - - end) - - describe('input.players_btn_states', function () - - it('should contain 2 tables of released button states, one per player', function () - assert.are_same({ - [0] = { - [button_ids.left] = btn_states.released, - [button_ids.right] = btn_states.released, - [button_ids.up] = btn_states.released, - [button_ids.down] = btn_states.released, - [button_ids.o] = btn_states.released, - [button_ids.x] = btn_states.released - }, - [1] = { - [button_ids.left] = btn_states.released, - [button_ids.right] = btn_states.released, - [button_ids.up] = btn_states.released, - [button_ids.down] = btn_states.released, - [button_ids.o] = btn_states.released, - [button_ids.x] = btn_states.released - } - }, - input.players_btn_states) - end) - - end) - - describe('toggle_mouse', function () - - describe('(mouse devkit inactive)', function () - - before_each(function () - input.mouse_active = false - poke(0x5f2d, 0) - end) - - after_each(function () - input.mouse_active = false - poke(0x5f2d, 0) - end) - - it('(true) => activate mouse devkit', function () - input:toggle_mouse(true) - assert.are_same({1, true}, {peek(0x5f2d), input.mouse_active}) - end) - - it('(false) => deactivate mouse devkit', function () - input:toggle_mouse(false) - assert.are_same({0, false}, {peek(0x5f2d), input.mouse_active}) - end) - - it('() => toggle to active', function () - input:toggle_mouse() - assert.are_same({1, true}, {peek(0x5f2d), input.mouse_active}) - end) - - end) - - describe('(mouse devkit active)', function () - - before_each(function () - input.mouse_active = true - poke(0x5f2d, 1) - end) - - after_each(function () - input.mouse_active = false - poke(0x5f2d, 0) - end) - - it('(true) => activate mouse devkit', function () - input:toggle_mouse(true) - assert.are_same({1, true}, {peek(0x5f2d), input.mouse_active}) - end) - - it('(false) => deactivate mouse devkit', function () - input:toggle_mouse(false) - assert.are_same({0, false}, {peek(0x5f2d), input.mouse_active}) - end) - - it('() => toggle to inactive', function () - input:toggle_mouse() - assert.are_same({0, false}, {peek(0x5f2d), input.mouse_active}) - end) - - end) - - end) - -end) - -describe('(mouse toggled)', function () - - setup(function () - pico8.mousepos = vector(24, 36) - end) - - teardown(function () - pico8.mousepos = vector.zero() - end) - - describe('get_cursor_position', function () - - it('should return the current cursor position (sign test)', function () - local cursor_position = input.get_cursor_position() - assert.are_equal(24, cursor_position.x) - assert.are_equal(36, cursor_position.y) - end) - - end) - - describe('(when both players have some input)', function () - - setup(function () - input.players_btn_states = { - [0] = { - [button_ids.left] = btn_states.released, - [button_ids.right] = btn_states.just_pressed, - [button_ids.up] = btn_states.released, - [button_ids.down] = btn_states.just_pressed, - [button_ids.o] = btn_states.pressed, - [button_ids.x] = btn_states.just_released - }, - [1] = { - [button_ids.left] = btn_states.just_pressed, - [button_ids.right] = btn_states.pressed, - [button_ids.up] = btn_states.just_released, - [button_ids.down] = btn_states.released, - [button_ids.o] = btn_states.pressed, - [button_ids.x] = btn_states.pressed - } - } - end) - - teardown(function () - input.players_btn_states = { - [0] = generate_initial_btn_states(), - [1] = generate_initial_btn_states() - } - end) - - describe('get_button_state', function () - - it('should return a button state for player 0 by default', function () - assert.are_equal(btn_states.released, input:get_button_state(button_ids.left)) - end) - - it('should return a button state for player 0', function () - assert.are_equal(btn_states.just_released, input:get_button_state(button_ids.x, 0)) - end) - - it('should return a button state for player 1', function () - assert.are_equal(btn_states.released, input:get_button_state(button_ids.down, 1)) - end) - - end) - - describe('is_up', function () - - it('should return true if button is released for player 0 by default', function () - assert.is_true(input:is_up(button_ids.left)) - end) - - it('should return true if button is just released for player 0 by default', function () - assert.is_true(input:is_up(button_ids.x)) - end) - - it('should return true if button is released for player 0', function () - assert.is_true(input:is_up(button_ids.left, 0)) - end) - - it('should return true if button is released for player 0', function () - assert.is_true(input:is_up(button_ids.x, 0)) - end) - - it('should return false if button is pressed for player 0', function () - assert.is_false(input:is_up(button_ids.o, 0)) - end) - - it('should return false if button is just pressed for player 0', function () - assert.is_false(input:is_up(button_ids.right, 0)) - end) - - it('should return true if button is released for player 1', function () - assert.is_true(input:is_up(button_ids.down, 1)) - end) - - it('should return true if button is released for player 1', function () - assert.is_true(input:is_up(button_ids.up, 1)) - end) - - it('should return false if button is pressed for player 1', function () - assert.is_false(input:is_up(button_ids.o, 1)) - end) - - it('should return false if button is just pressed for player 1', function () - assert.is_false(input:is_up(button_ids.right, 1)) - end) - - end) - - describe('is_down', function () - - it('should return the opposite of is_up', function () - assert.is_true(input:is_down(button_ids.left) == not input:is_up(button_ids.left)) - assert.is_true(input:is_down(button_ids.up, 0) == not input:is_up(button_ids.up, 0)) - assert.is_true(input:is_down(button_ids.x, 1) == not input:is_up(button_ids.x, 1)) - end) - - end) - - describe('is_just_released', function () - - it('should return true if the button was just released', function () - assert.is_true(input:is_just_released(button_ids.x, 0)) - end) - - it('should return false if the button was not just released', function () - assert.are_same({false, false, false}, - { - input:is_just_released(button_ids.up, 0), - input:is_just_released(button_ids.left, 1), - input:is_just_released(button_ids.right, 1) - }) - end) - - end) - - describe('is_just_pressed', function () - - it('should return true if the button was just released', function () - assert.is_true(input:is_just_pressed(button_ids.down, 0)) - end) - - it('should return false if the button was not just released', function () - assert.are_same({false, false, false}, - { - input:is_just_pressed(button_ids.up, 0), - input:is_just_pressed(button_ids.up, 1), - input:is_just_pressed(button_ids.right, 1) - }) - end) - - end) - - end) - - describe('process_players_inputs', function () - - local _process_player_inputs_stub - - setup(function () - _process_player_inputs_stub = stub(input, "_process_player_inputs") - end) - - teardown(function () - _process_player_inputs_stub:revert() - end) - - after_each(function () - _process_player_inputs_stub:clear() - end) - - it('should call _process_player_inputs on each player', function () - input:process_players_inputs() - assert.spy(_process_player_inputs_stub).was_called(2) - assert.spy(_process_player_inputs_stub).was_called_with(match.ref(input), 0) - assert.spy(_process_player_inputs_stub).was_called_with(match.ref(input), 1) - end) - - end) - - describe('_process_player_inputs', function () - - after_each(function () - -- reset all button states - clear_table(pico8.keypressed[0]) - clear_table(pico8.keypressed[1]) - - input.players_btn_states = { - [0] = generate_initial_btn_states(), - [1] = generate_initial_btn_states() - } - - pico8.keypressed.counter = 0 - end) - - describe('(when input mode is native)', function () - - describe('(when player 0 has button left & up: released, right & down: just pressed, o & x: pressed)', function () - - before_each(function () - input.players_btn_states[0] = { - [button_ids.left] = btn_states.released, - [button_ids.right] = btn_states.just_pressed, - [button_ids.up] = btn_states.released, - [button_ids.down] = btn_states.just_pressed, - [button_ids.o] = btn_states.pressed, - [button_ids.x] = btn_states.pressed - } - pico8.keypressed[0] = { - [0] = false, -- left - false, -- right - true, -- up - true, -- down - false, -- o - true -- x - } - -- counter should be 1 (or a multiple of the repeat period) if a button is supposed to be just pressed this frame - pico8.keypressed.counter = 1 - end) - - it('should update all button states for player 0 in parallel', function () - input:_process_player_inputs(0) - assert.are_same({ - [button_ids.left] = btn_states.released, - [button_ids.right] = btn_states.just_released, - [button_ids.up] = btn_states.just_pressed, - [button_ids.down] = btn_states.pressed, - [button_ids.o] = btn_states.just_released, - [button_ids.x] = btn_states.pressed - }, - input.players_btn_states[0]) - end) - - end) - - describe('(when player 1 has button left & up: released, right & down: just released, o & x: pressed)', function () - - before_each(function () - input.players_btn_states[1] = { - [button_ids.left] = btn_states.released, - [button_ids.right] = btn_states.just_released, - [button_ids.up] = btn_states.released, - [button_ids.down] = btn_states.just_released, - [button_ids.o] = btn_states.pressed, - [button_ids.x] = btn_states.pressed - } - pico8.keypressed[1] = { - [0] = false, -- left - false, -- right - true, -- up - true, -- down - false, -- o - true -- x - } - -- counter should be 1 (or a multiple of the repeat period) if a button is supposed to be just pressed this frame - pico8.keypressed.counter = 1 - end) - - it('should update all button states for player 1 in parallel', function () - input:_process_player_inputs(1) - assert.are_same({ - [button_ids.left] = btn_states.released, - [button_ids.right] = btn_states.released, - [button_ids.up] = btn_states.just_pressed, - [button_ids.down] = btn_states.just_pressed, - [button_ids.o] = btn_states.just_released, - [button_ids.x] = btn_states.pressed - }, - input.players_btn_states[1]) - end) - - end) - - describe('(when button has just been pressed but is incorrect state because btnp counter is wrong)', function () - - before_each(function () - input.players_btn_states[0][button_ids.left] = btn_states.released - pico8.keypressed[0][button_ids.left] = true - -- leave pico8.keypressed.counter at 0 - end) - - it('should detect and assert if btnp returns false while our model says it should be true', function () - assert.has_error(function() - input:_process_player_inputs(0) - end, - "input:_update_button_state: button 0 was released and is now pressed, but btnp(0) returns false") - end) - - end) - - end) - - describe('(when input mode is simulated)', function () - - setup(function () - input.mode = input_modes.simulated - end) - - teardown(function () - input.mode = input_modes.native - end) - - describe('(when player 0 has some simulated input)', function () - - setup(function () - input.players_btn_states[0][button_ids.up] = btn_states.just_pressed - input.simulated_buttons_down[0][button_ids.left] = true - input.simulated_buttons_down[0][button_ids.up] = true - end) - - teardown(function () - input.players_btn_states[0][button_ids.up] = btn_states.released - input.simulated_buttons_down[0][button_ids.left] = false - input.simulated_buttons_down[0][button_ids.up] = false - end) - - it('should update the buttons states for player 0 based on the simulated button static states', function () - input:_process_player_inputs(0) - assert.are_same({ - btn_states.just_pressed, - btn_states.pressed, - }, - { - input.players_btn_states[0][button_ids.left], - input.players_btn_states[0][button_ids.up], - }) - end) - - end) - - describe('(when player 1 has some simulated input)', function () - - setup(function () - input.players_btn_states[1][button_ids.down] = btn_states.just_released - input.players_btn_states[1][button_ids.o] = btn_states.pressed - input.simulated_buttons_down[1][button_ids.down] = true - end) - - teardown(function () - input.players_btn_states[1][button_ids.down] = btn_states.released - input.players_btn_states[1][button_ids.o] = btn_states.released - input.simulated_buttons_down[1][button_ids.down] = false - end) - - it('should update the buttons states for player 1 based on the simulated button static states', function () - input:_process_player_inputs(1) - assert.are_same({ - btn_states.just_pressed, - btn_states.just_released - }, - { - input.players_btn_states[1][button_ids.down], - input.players_btn_states[1][button_ids.o] - }) - end) - - end) - - end) - - end) - - describe('_btn_proxy', function () - - after_each(function () - end) - - describe('(when input mode is native)', function () - - setup(function () - pico8.keypressed[0][button_ids.up] = true - pico8.keypressed[1][button_ids.o] = true - end) - - teardown(function () - clear_table(pico8.keypressed[0]) - clear_table(pico8.keypressed[1]) - end) - - it('should return btn(button_id, player_id)', function () - assert.are_same( - { - false, - false, - true, - true, - false, - true - }, - { - input:_btn_proxy(button_ids.left), - input:_btn_proxy(button_ids.left, 0), - input:_btn_proxy(button_ids.up), - input:_btn_proxy(button_ids.up, 0), - input:_btn_proxy(button_ids.down, 1), - input:_btn_proxy(button_ids.o, 1), - }) - end) - - end) - - describe('(when input mode is simulated)', function () - - describe('(in initial state)', function () - - it('should return false for all buttons', function () - assert.are_same( - { - false, - false, - false, - false, - false, - false, - false, - false - }, - { - input:_btn_proxy(button_ids.left), - input:_btn_proxy(button_ids.left, 0), - input:_btn_proxy(button_ids.up), - input:_btn_proxy(button_ids.up, 0), - input:_btn_proxy(button_ids.x, 0), - input:_btn_proxy(button_ids.down, 1), - input:_btn_proxy(button_ids.o, 1), - input:_btn_proxy(button_ids.x, 1), - }) - end) - - end) - - describe('(when some simulated buttons are down)', function () - - setup(function () - input.mode = input_modes.simulated - input.simulated_buttons_down[0][button_ids.up] = true - input.simulated_buttons_down[1][button_ids.o] = true - end) - - teardown(function () - input.mode = input_modes.native - input.simulated_buttons_down[0][button_ids.up] = false - input.simulated_buttons_down[1][button_ids.o] = false - end) - - it('should return true if simulated input is down', function () - - assert.are_same( - { - false, - false, - true, - true, - false, - true - }, - { - input:_btn_proxy(button_ids.left), - input:_btn_proxy(button_ids.left, 0), - input:_btn_proxy(button_ids.up), - input:_btn_proxy(button_ids.up, 0), - input:_btn_proxy(button_ids.down, 1), - input:_btn_proxy(button_ids.o, 1), - }) - end) - - end) - - end) - - end) - - describe('_compute_next_button_state', function () - - it('was released & now up => released', function () - assert.are_equal(btn_states.released, - input:_compute_next_button_state(btn_states.released, false)) - end) - - it('was released & now down => just pressed', function () - assert.are_equal(btn_states.just_pressed, - input:_compute_next_button_state(btn_states.released, true)) - end) - - it('was just_pressed & now up => just_released', function () - assert.are_equal(btn_states.just_released, - input:_compute_next_button_state(btn_states.just_pressed, false)) - end) - - it('was just_pressed & now down => pressed', function () - assert.are_equal(btn_states.pressed, - input:_compute_next_button_state(btn_states.just_pressed, true)) - end) - - it('was pressed & now up => just_released', function () - assert.are_equal(btn_states.just_released, - input:_compute_next_button_state(btn_states.pressed, false)) - end) - - it('was pressed & now down => pressed', function () - assert.are_equal(btn_states.pressed, - input:_compute_next_button_state(btn_states.pressed, true)) - end) - - it('was just_released & now up => released', function () - assert.are_equal(btn_states.released, - input:_compute_next_button_state(btn_states.just_released, false)) - end) - - it('was just_released & now down => just pressed', function () - assert.are_equal(btn_states.just_pressed, - input:_compute_next_button_state(btn_states.just_released, true)) - end) - - end) - -end) diff --git a/src/engine/tests/utestintegrationtest.lua b/src/engine/tests/utestintegrationtest.lua deleted file mode 100644 index 2a7cf4f7..00000000 --- a/src/engine/tests/utestintegrationtest.lua +++ /dev/null @@ -1,1274 +0,0 @@ -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 input = require("engine/input/input") - -local function repeat_callback(time, callback) - -- ceil is just for times with precision of 0.01 or deeper, - -- so the last frame is reached (e.g. an action at t=0.01 is applied) - -- caution: this may make fractional times advance too much and apply actions they shouldn't, - -- so tune your times carefully for testing - for i = 1, ceil(time*fps) do - callback() - end -end - - -describe('itest_manager', function () - - after_each(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 () - local function setup_fn() end - local function action1() end - local function action2() end - local function action3() end - local function action4() end - local function final_assert_fn() end - itest_manager:register_itest('test 1', {'titlemenu'}, function () - setup_callback(setup_fn) - act(action1) -- test immediate action - wait(0.5) - wait(0.6) -- test closing previous wait - act(action2) -- test action with previous wait - act(action3) -- test immediate action - add_action(time_trigger(1.0), action4) -- test retro-compatible function - wait(0.7) -- test wait-action closure - final_assert(final_assert_fn) - end) - local created_itest = itest_manager.itests[1] - assert.are_same({ - 'test 1', - {'titlemenu'}, - setup_fn, - { - scripted_action(time_trigger(0.0), action1), - scripted_action(time_trigger(0.5), dummy), - scripted_action(time_trigger(0.6), action2), - scripted_action(time_trigger(0.0), action3), - scripted_action(time_trigger(1.0), action4), - scripted_action(time_trigger(0.7), dummy) - }, - final_assert_fn - }, - { - created_itest.name, - created_itest.active_gamestates, - created_itest.setup, - created_itest.action_sequence, - created_itest.final_assertion - }) - end) - - end) - - describe('register', function () - - it('should register a new test', function () - local itest = integration_test('test 1', {'titlemenu'}) - itest_manager:register(itest) - assert.are_equal(itest, itest_manager.itests[1]) - end) - - it('should register a 2nd test', function () - local itest = integration_test('test 1', {'titlemenu'}) - local itest2 = integration_test('test 2', {'titlemenu'}) - itest_manager:register(itest) - itest_manager:register(itest2) - assert.are_same({itest, itest2}, itest_manager.itests) - end) - - end) - - describe('init_game_and_start_by_index', function () - - setup(function () - itest_runner_own_method = stub(itest_runner, "init_game_and_start") - end) - - teardown(function () - itest_runner_own_method:revert() - end) - - after_each(function () - itest_runner_own_method:clear() - end) - - it('should delegate to itest runner', function () - local itest = integration_test('test 1', {'titlemenu'}) - 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(itest_runner), itest) - end) - - it('should assert if the index is invalid', function () - local itest = integration_test('test 1', {'titlemenu'}) - itest_manager:register(itest) - assert.has_error(function () - itest_manager:init_game_and_start_by_index(2) - end, - "itest_manager:init_game_and_start_by_index: index is 2 but only 1 were registered.") - end) - - end) - -end) - - -describe('itest_runner', function () - - -- prepare mock app with default implementation - local mock_app = gameapp() - - local test - - before_each(function () - test = integration_test('character walks', {'stage'}) - end) - - after_each(function () - -- full reset - itest_runner:init() - input.mode = input_modes.native - 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 () - stub(gameapp, "reset") - stub(gameapp, "start") - stub(itest_runner, "stop") - stub(itest_runner, "start") - end) - - teardown(function () - gameapp.reset:revert() - gameapp.start:revert() - itest_runner.stop:revert() - itest_runner.start:revert() - end) - - after_each(function () - gameapp.reset:clear() - gameapp.start:clear() - itest_runner.stop:clear() - itest_runner.start:clear() - end) - - 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) - - describe('(with mock app)', function () - - before_each(function () - itest_runner.app = mock_app - end) - - describe('(when current_test is already set)', function () - - before_each(function () - itest_runner.current_test = test - end) - - it('should reset the app', function () - itest_runner:init_game_and_start(test) - - local s = assert.spy(gameapp.reset) - s.was_called(1) - s.was_called_with(match.ref(mock_app)) - end) - - it('should stop', function () - itest_runner:init_game_and_start(test) - - local s = assert.spy(itest_runner.stop) - s.was_called(1) - s.was_called_with(match.ref(itest_runner)) - end) - - end) - - it('should start the gameapp', function () - itest_runner:init_game_and_start(test) - - local s = assert.spy(gameapp.start) - s.was_called(1) - s.was_called_with(match.ref(mock_app)) - end) - - 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) - - end) - - describe('(with mock app)', function () - - before_each(function () - itest_runner.app = mock_app - end) - - describe('update_game_and_test', function () - - setup(function () - stub(gameapp, "update") - spy.on(itest_runner, "update") - end) - - teardown(function () - gameapp.update:revert() - itest_runner.update:revert() - end) - - after_each(function () - gameapp.update:clear() - itest_runner.update:clear() - end) - - 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) - - describe('(when state is running for some actions)', function () - - before_each(function () - test:add_action(time_trigger(1.0), function () end, 'some_action') - end) - - it('should update the set gameapp and the passed test', function () - itest_runner:start(test) - - itest_runner:update_game_and_test() - - 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) - - 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) - - end) - - 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) - - describe('draw_game_and_test', function () - - setup(function () - stub(gameapp, "draw") - stub(itest_runner, "draw") - end) - - teardown(function () - gameapp.draw:revert() - itest_runner.draw:revert() - end) - - after_each(function () - gameapp.draw:clear() - itest_runner.draw: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) - - end) - - end) -- (with mock app) - - describe('start', function () - - setup(function () - spy.on(itest_runner, "_initialize") - spy.on(itest_runner, "_check_end") - spy.on(itest_runner, "_check_next_action") - end) - - teardown(function () - itest_runner._initialize:revert() - itest_runner._check_end:revert() - itest_runner._check_next_action:revert() - end) - - before_each(function () - test.setup = spy.new(function () end) - end) - - after_each(function () - 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 () - itest_runner:start(test) - assert.are_equal(test, itest_runner.current_test) - end) - - it('should initialize state vars', function () - itest_runner:start(test) - assert.are_same({0, 0, 1}, { - itest_runner.current_frame, - itest_runner._last_trigger_frame, - itest_runner._next_action_index - }) - end) - - it('should call the test setup callback', function () - 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 () - 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 () - 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 () - 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 () - itest_runner:start(test) - assert.are_not_equal(test_states.running, itest_runner.current_state) - end) - - end) - - describe('(when some actions)', function () - - before_each(function () - test:add_action(time_trigger(1.0), function () end, 'some_action') - end) - - it('should check the next action immediately (if at time 0, will also call it)', function () - 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 () - itest_runner:start(test) - assert.are_equal(test_states.running, itest_runner.current_state) - end) - - end) - - describe('(after a first start)', function () - - before_each(function () - test:add_action(time_trigger(1.0), function () end, 'restart_action') - -- some progress - itest_runner:start(test) - repeat_callback(1.0, function () - itest_runner:update() - end) - 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 - itest_runner._initialize:clear() - itest_runner:start(test) - assert.spy(itest_runner._initialize).was_not_called() - end) - - end) - - end) - - describe('update', function () - - it('should assert when no test has been started', function () - assert.has_error(function() - itest_runner:update() - end, - "itest_runner:update: current_test is not set") - end) - - describe('(after test started)', function () - - local action_callback = spy.new(function () end) - - before_each(function () - -- need at least 1/60=0.1666s above 1.0s so it's not called after 1.0s converted to frames - test:add_action(time_trigger(1.02), action_callback, 'update_test_action') - end) - - teardown(function () - action_callback:revert() - end) - - before_each(function () - itest_runner:start(test) - end) - - after_each(function () - action_callback:clear() - end) - - it('should advance the current time by 1', function () - 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 () - itest_runner:update() - assert.spy(action_callback).was_not_called() - 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 () - itest_runner:update() - end) - assert.spy(action_callback).was_not_called() - 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 () - itest_runner:update() - end) - assert.spy(action_callback).was_called(1) - 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 () - itest_runner:update() - end) - 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 () - - before_each(function () - test:add_action(time_trigger(3.0), function () end, 'more action') - test:set_timeout(2.0) - end) - - describe('(when next frame is below 120)', function () - - before_each(function () - itest_runner.current_frame = 118 - end) - - it('should call next action (no time out)', function () - itest_runner:update() - assert.are_equal(test_states.running, itest_runner.current_state) - assert.spy(action_callback).was_called(1) - end) - - end) - - describe('(when next frame is 120 or above)', function () - - before_each(function () - itest_runner.current_frame = 119 - end) - - it('should time out without calling next action', function () - itest_runner:update() - assert.are_equal(test_states.timeout, itest_runner.current_state) - assert.spy(action_callback).was_not_called() - end) - - end) - - end) - - end) - - describe('(after test ended)', function () - - before_each(function () - -- without any action, start should end the test immediately - itest_runner:start(test) - end) - - it('should do nothing', function () - 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) - - end) - - describe('draw', function () - - describe('(stubbing api.print)', function () - - setup(function () - stub(api, "print") - end) - - teardown(function () - api.print:revert() - end) - - after_each(function () - api.print:clear() - end) - - it('should draw "no itest running"', function () - itest_runner:draw() - 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 () - - before_each(function () - itest_runner.current_test = test - itest_runner.current_state = test_states.running - end) - - it('should draw information on the current test', function () - itest_runner:draw() - assert.spy(api.print).was_called(2) - end) - - end) - - end) - - end) - - describe('_get_test_state_color', function () - - it('should return white for none', function () - 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, itest_runner:_get_test_state_color(test_states.running)) - end) - - it('should return green for success', function () - 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, itest_runner:_get_test_state_color(test_states.failure)) - end) - - it('should return dark purple for timeout', function () - assert.are_equal(colors.dark_purple, itest_runner:_get_test_state_color(test_states.timeout)) - end) - - end) - - describe('_initialize', function () - - it('should set the input mode to simulated', function () - 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 () - 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 - logging.logger.active_categories["trace"] = false - assert.are_same({ - default = false, - flow = false, - player = false, - ui = false, - codetuner = false, - 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 () - itest_runner:_initialize() - assert.is_true(itest_runner.initialized) - end) - - end) - - - 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) - - setup(function () - -- don't stub a function if the return value matters, as in start - spy.on(itest_runner, "_check_end") - end) - - teardown(function () - action_callback:revert() - action_callback2:revert() - itest_runner._check_end:revert() - end) - - before_each(function () - 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() - itest_runner._check_end:clear() - end) - - describe('(when next action index is 1/1)', function () - - before_each(function () - 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 - itest_runner.current_frame = 158 - itest_runner._last_trigger_frame = 100 - end) - - it('should not call the action nor advance the time/index', function () - 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, 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) - - 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 - itest_runner.current_frame = 160 - itest_runner._last_trigger_frame = 100 - end) - - it('should call the action and advance the timeindex', function () - 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, 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) - - end) - - describe('(when next action index is 2/1)', function () - - before_each(function () - -- we still have the dummy action from the outer scope - itest_runner._next_action_index = 2 -- we are now at 2/1 - end) - - it('should assert', function () - assert.has_error(function () - itest_runner:_check_next_action() - end, - "self._next_action_index (2) is out of bounds for self.current_test.action_sequence (size 1)") - end) - - end) - - describe('(with 2nd dummy action immediately after the other)', function () - - describe('(when next action index is 1/1)', function () - - before_each(function () - 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 - test:add_action(time_trigger(0.0), action_callback2, 'action_callback2') - itest_runner.current_frame = 158 - itest_runner._last_trigger_frame = 100 - end) - - it('should not call any actions nor advance the time/index', function () - 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, 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) - - 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') - itest_runner.current_frame = 160 - itest_runner._last_trigger_frame = 100 - end) - - it('should call both actions and advance the timeindex by 2', function () - 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, 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) - - 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 () - 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 - test:add_action(time_trigger(0.2), action_callback2, 'action_callback2') - itest_runner.current_frame = 158 - itest_runner._last_trigger_frame = 100 - end) - - it('should not call any actions nor advance the time/index', function () - 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, 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) - - 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') - itest_runner.current_frame = 160 - itest_runner._last_trigger_frame = 100 - end) - - it('should call only the first action and advance the timeindex', function () - 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, 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) - - end) - - end) - - end) - - describe('(with empty action)', function () - - before_each(function () - -- empty actions are useful to just wait until the test end and delay the final assertion - test:add_action(time_trigger(1, true), nil, 'empty action') - end) - - it('should recognize next empty action and do nothing', function () - itest_runner:start(test) - itest_runner.current_frame = 2 -- to trigger action to do at end of frame 1 - - assert.has_no_errors(function () - itest_runner:_check_next_action() - end) - end) - - end) - - end) - - describe('_check_end', function () - - before_each(function () - itest_runner:start(test) - end) - - describe('(when no actions left)', function () - - describe('(when no final assertion)', function () - - it('should make test end immediately with success and return true', function () - local result = itest_runner:_check_end(test) - assert.is_true(result) - assert.are_same({test_states.success, nil}, - {itest_runner.current_state, itest_runner.current_message}) - end) - - end) - - describe('(when final assertion passes)', function () - - before_each(function () - test.final_assertion = function () - return true - end - end) - - it('should check the final assertion immediately, end with success and return true', function () - local result = itest_runner:_check_end(test) - assert.is_true(result) - assert.are_same({test_states.success, nil}, - {itest_runner.current_state, itest_runner.current_message}) - end) - - end) - - describe('(when final assertion passes)', function () - - before_each(function () - test.final_assertion = function () - return false, "error message" - end - end) - - it('should check the final assertion immediately, end with failure and return true', function () - local result = itest_runner:_check_end(test) - assert.is_true(result) - assert.are_equal(test_states.failure, itest_runner.current_state) - end) - - end) - - end) - - describe('(when some actions left)', function () - - before_each(function () - test:add_action(time_trigger(1.0), function () end, 'check_end_test_action') - end) - - it('should return false', function () - assert.is_false(itest_runner:_check_end(test)) - end) - - end) - - end) - - describe('_end_with_final_assertion', function () - - before_each(function () - -- 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 - 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 () - itest_runner:_end_with_final_assertion(test) - assert.are_equal(test_states.success, itest_runner.current_state) - end) - - end) - - describe('(when final assertion passes)', function () - - before_each(function () - test.final_assertion = function () - return true - end - end) - - it('should check the final assertion and end with success', function () - itest_runner:_end_with_final_assertion(test) - assert.are_equal(test_states.success, itest_runner.current_state) - end) - - end) - - describe('(when final assertion passes)', function () - - before_each(function () - test.final_assertion = function () - return false, "error message" - end - end) - - it('should check the final assertion and end with failure', function () - itest_runner:_end_with_final_assertion(test) - assert.are_same({test_states.failure, "error message"}, - {itest_runner.current_state, itest_runner.current_message}) - end) - - end) - - end) - - describe('stop', function () - - before_each(function () - itest_runner:start(test) - end) - - it('should reset the current test', function () - itest_runner:stop(test) - assert.is_nil(itest_runner.current_test) - end) - - it('should reset state vars', function () - itest_runner:stop(test) - assert.are_same({0, 0, 1, test_states.none}, { - itest_runner.current_frame, - itest_runner._last_trigger_frame, - itest_runner._next_action_index, - itest_runner.current_state - }) - end) - - - describe('(when teardown is set)', function () - - before_each(function () - test.teardown = spy.new(function () end) - end) - - it('should call teardown', function () - itest_runner:stop(test) - assert.spy(test.teardown).was_called(1) - assert.spy(test.teardown).was_called_with() - end) - - end) - - end) - -end) - -describe('time_trigger', function () - - describe('_init', function () - it('should create a time trigger with a time in seconds', function () - local time_t = time_trigger(1.0) - assert.is_not_nil(time_t) - assert.are_equal(time_t.frames, 60) - end) - it('should create a time trigger with a time in frames if wanted', function () - local time_t = time_trigger(55, true) - assert.is_not_nil(time_t) - assert.are_equal(time_t.frames, 55) - end) - end) - - describe('_tostring', function () - it('should return "time_trigger({self.time})"', function () - assert.are_equal("time_trigger(120)", time_trigger(2.0):_tostring()) - 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)) - end) - it('should return true if elapsed time is greater than {self.frames}', function () - assert.is_true(time_trigger(2.0):_check(121)) - end) - it('should return false if elapsed time is less than {self.frames}', function () - assert.is_false(time_trigger(2.0):_check(119)) - end) - end) - -end) - -describe('scripted_action', function () - - describe('_init', function () - it('should create a scripted action with a trigger and callback (unnamed)', function () - local do_something = function () end - local act = scripted_action(time_trigger(2.0), do_something) - assert.is_not_nil(act) - assert.are_same({time_trigger(2.0), do_something, "unnamed"}, {act.trigger, act.callback, act.name}) - end) - it('should create a scripted action with a trigger, callback and name', function () - local do_something = function () end - local act = scripted_action(time_trigger(2.0), do_something, "do_something") - assert.is_not_nil(act) - assert.are_same({time_trigger(2.0), do_something, "do_something"}, {act.trigger, act.callback, act.name}) - end) - end) - - describe('_tostring', function () - it('should return "scripted_action \'unnamed\' @ {self.trigger}"" if no name', function () - local act = scripted_action(time_trigger(2.0), function () end) - assert.are_equal("[scripted_action 'unnamed' @ time_trigger(120)]", act:_tostring()) - end) - it('should return "scripted_action \'{self.name}\' @ {self.trigger}" if some name', function () - local act = scripted_action(time_trigger(2.0), function () end, 'do_something') - assert.are_equal("[scripted_action 'do_something' @ time_trigger(120)]", act:_tostring()) - end) - end) -end) - - -describe('integration_test', function () - - describe('_init', function () - - it('should create an integration test with a name (and active gamestates for non-pico8 build)', function () - local test = integration_test('character follows ground', {'stage'}) - assert.is_not_nil(test) - assert.are_same({'character follows ground', nil, {}, nil, 0, {'stage'}}, - {test.name, test.setup, test.action_sequence, test.final_assertion, test.timeout_frames, test.active_gamestates}) - end) - - it('should assert if active gamestates is nil for non-pico8 build', function () - assert.has_error(function () - integration_test('missing active gamestates') - end, - "integration_test._init: non-pico8 build requires active_gamestates to define them at runtime") - end) - - end) - - describe('_tostring', function () - it('should return "integration_test \'{self.name}\'', function () - local test = integration_test('character follows ground', function () end) - assert.are_equal("[integration_test 'character follows ground']", test:_tostring()) - end) - end) - - describe('add_action', function () - it('should add a scripted action in the action sequence', function () - local test = integration_test('character follows ground', function () end) - action_callback = function () end - test:add_action(time_trigger(1.0), action_callback, 'my_action') - assert.are_equal(1, #test.action_sequence) - assert.are_same({time_trigger(1.0), action_callback, 'my_action'}, {test.action_sequence[1].trigger, test.action_sequence[1].callback, test.action_sequence[1].name}) - end) - end) - - describe('set_timeout', function () - it('should set the timeout by converting time in s to frames', function () - local test = integration_test('character follows ground', function () end) - test:set_timeout(2.0) - assert.are_equal(120, test.timeout_frames) - end) - end) - - describe('check_timeout', function () - - it('should return false if timeout is 0', function () - local test = integration_test('character follows ground', function () end) - test:set_timeout(0.0) - assert.is_false(test:check_timeout(50)) - end) - - it('should return false if frame is less than timeout (119 < 120)', function () - local test = integration_test('character follows ground', function () end) - test:set_timeout(2.0) - assert.is_false(test:check_timeout(119)) - end) - - it('should return true if frame is greater than or equal to timeout', function () - local test = integration_test('character follows ground', function () end) - test:set_timeout(2.0) - assert.is_true(test:check_timeout(120)) - end) - - end) - - describe('_check_final_assertion', function () - it('should call the final assertion and return the result', function () - local test = integration_test('character follows ground', function () end) - test.final_assertion = function() - return false, 'error message' - end - assert.are_same({false, 'error message'}, {test:_check_final_assertion()}) - end) - end) - - -end) diff --git a/src/engine/tests/utestlogging.lua b/src/engine/tests/utestlogging.lua deleted file mode 100644 index 3b474e3c..00000000 --- a/src/engine/tests/utestlogging.lua +++ /dev/null @@ -1,964 +0,0 @@ -require("engine/test/bustedhelper") -require("engine/core/math") -local logging = require("engine/debug/logging") - --- bustedhelper require affects logging, so reset the state -logging.logger:init() - -describe('logging', function () - - local log_msg, log_stream, file_log_stream = get_members(logging, - "log_msg", "log_stream", "file_log_stream") - - describe('log_msg', function () - - describe('_init', function () - - it('should create a log_msg with level, category, message content', function () - local lm = log_msg(logging.level.info, "character", "moving") - assert.is_not_nil(lm) - assert.are_same({logging.level.info, "character", "moving"}, - {lm.level, lm.category, lm.text}) - end) - - end) - - describe('_tostring', function () - - it('should return "log_msg({self.level}, {self.category}, {self.message})"', function () - local lm = log_msg(logging.level.info, "character", "moving") - assert.are_equal('log_msg(1, "character", "moving")', lm:_tostring()) - end) - - end) - - end) - - describe('compound_message', function () - - it('should return a string concatenating [category] and message for info', function () - local lm = log_msg(logging.level.info, "default", "hello") - assert.are_equal("[default] hello", logging.compound_message(lm)) - end) - - it('should return a string concatenating [category], log level and message for warning', function () - local lm = log_msg(logging.level.warning, "player", "caution") - assert.are_equal("[player] warning: caution", logging.compound_message(lm)) - end) - - it('should return a string concatenating [category], log level and message for error', function () - local lm = log_msg(logging.level.error, "flow", "danger") - assert.are_equal("[flow] error: danger", logging.compound_message(lm)) - end) - - end) - - describe('log_stream (testing implemented base methods)', function () - - local dummy_log_stream = derived_singleton(log_stream) - - dummy_log_stream.on_log = spy.new(function () end) - - after_each(function () - dummy_log_stream:init() - end) - - describe('init', function () - - it('should initialize the singleton active', function () - assert.is_true(dummy_log_stream.active) - end) - - end) - - describe('log', function () - - teardown(function () - dummy_log_stream.on_log:revert() - end) - - after_each(function () - dummy_log_stream.on_log:clear() - end) - - it('should do nothing is inactive', function () - dummy_log_stream.active = false - local lm = log_msg(logging.level.warning, "player", "caution") - dummy_log_stream:log(lm) - assert.spy(dummy_log_stream.on_log).was_called(0) - end) - - it('should call on_log callback if active', function () - local lm = log_msg(logging.level.warning, "player", "caution") - dummy_log_stream:log(lm) - assert.spy(dummy_log_stream.on_log).was_called(1) - assert.spy(dummy_log_stream.on_log).was_called_with(dummy_log_stream, match.ref(lm)) - end) - - end) - - end) - - describe('logger', function () - - local logger = logging.logger - - after_each(function () - logger:init() - end) - - describe('init', function () - - it('should set all active categories flags to true, except trace', function () - for category, _ in pairs(logger.active_categories) do - 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) - - 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('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" - - 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 () - logger:deactivate_all_categories() - for category, _ in pairs(logger.active_categories) do - assert.is_false(logger.active_categories[category]) - end - end) - - end) - - describe('register_stream', function () - - it('should add a valid stream to the streams', function () - local spied_fun = spy.new(function () end) - - local fake_stream = derived_singleton(log_stream) - - function fake_stream:on_log(lm) - spied_fun(2, lm.level, lm.category, lm.text) - end - - logger:register_stream(fake_stream) - - -- implementation - assert.are_same({fake_stream}, logger._streams) - - -- interface - log("text", "default") - assert.spy(spied_fun).was_called(1) - assert.spy(spied_fun).was_called_with(2, logging.level.info, "default", "text") - assert.spy(spied_fun).was_called_with(2, logging.level.info, "default", "text") - end) - - it('should assert if nil is passed', function () - assert.has_error(function () - logger:register_stream(nil) - end, - "logger:register_stream: passed stream is nil") - end) - - it('should assert if an invalid stream table is passed', function () - -- don't define on_log! - local invalid_stream = {} - - assert.has_error(function () - logger:register_stream(invalid_stream) - end, - "logger:register_stream: passed stream is invalid: on_log member is nil or not a callable") - end) - - end) - - describe('_generic_log', function () - - local spied_fun = spy.new(function (var, message, category, level) end) - - fake_stream_class = new_class() - - function generate_fake_stream(value) - local fake_stream = derived_singleton(log_stream) - function fake_stream:on_log(lm) - spied_fun(value, lm.level, lm.category, lm.text) - end - return fake_stream - end - - local fake_stream1 = generate_fake_stream(1) - local fake_stream2 = generate_fake_stream(2) - - setup(function () - spy.on(fake_stream1, "log") - spy.on(fake_stream2, "log") - end) - - after_each(function () - fake_stream1.log:clear() - fake_stream2.log:clear() - spied_fun:clear() - end) - - describe('(when category A is active, B inactive and logging level is 2)', function () - - before_each(function () - logger.active_categories.flow = true -- A - logger.active_categories.player = false -- B - logger.current_level = 2 -- warning level - logger:register_stream(fake_stream1) - logger:register_stream(fake_stream2) - end) - - -- generate tests for log levels equal to the threshold or higher - for log_level = 2, 3 do - - it('should call log on all streams for category A and logging level '..tostr(log_level), function () - logger:_generic_log(log_level, "flow", "text") - - -- implementation - local lm = log_msg(log_level, "flow", "text") - assert.spy(fake_stream1.log).was_called(1) - assert.spy(fake_stream1.log).was_called_with(match.ref(fake_stream1), lm) - assert.spy(fake_stream2.log).was_called(1) - assert.spy(fake_stream2.log).was_called_with(match.ref(fake_stream2), lm) - - -- interface - assert.spy(spied_fun).was_called(2) - assert.spy(spied_fun).was_called_with(1, log_level, "flow", "text") - assert.spy(spied_fun).was_called_with(2, log_level, "flow", "text") - end) - - end - - it('should not call log for category B (even for logging level 2)', function () - logger:_generic_log(2, "player", "text") - - -- implementation - assert.spy(fake_stream1.log).was_not_called() - assert.spy(fake_stream2.log).was_not_called() - - -- interface - assert.spy(spied_fun).was_not_called() - end) - - it('should not call log for logging level 1 (even for category B)', function () - logger:_generic_log(1, "flow", "text") - - -- implementation - assert.spy(fake_stream1.log).was_not_called() - assert.spy(fake_stream2.log).was_not_called() - - -- interface - assert.spy(spied_fun).was_not_called() - end) - - end) - - 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 () - printh_stub = stub(_G, "printh") - end) - - teardown(function () - printh_stub:revert() - end) - - before_each(function () - logger.active_categories.flow = true - logger:register_stream(console_log_stream) - end) - - after_each(function () - printh_stub:clear() - end) - - describe('(logger level set to info)', function () - - before_each(function () - logger.current_level = logging.level.info - end) - - describe('(default category active)', function () - - before_each(function () - logger.active_categories.default = true - end) - - describe('log', function () - - it('should print with no argument (default)', function () - log("message1") - assert.spy(printh_stub).was_called(1) - assert.spy(printh_stub).was_called_with("[default] message1") - end) - - it('should print with explicit category: default', function () - log("message1") - assert.spy(printh_stub).was_called(1) - assert.spy(printh_stub).was_called_with("[default] message1") - end) - - end) - - describe('warn', function () - - it('should print with no argument (default)', function () - warn("message1") - assert.spy(printh_stub).was_called(1) - assert.spy(printh_stub).was_called_with("[default] warning: message1") - end) - - it('should print with explicit category: default', function () - warn("message1") - assert.spy(printh_stub).was_called(1) - assert.spy(printh_stub).was_called_with("[default] warning: message1") - end) - - end) - - describe('err', function () - - it('should print with no argument (default)', function () - err("message1") - assert.spy(printh_stub).was_called(1) - assert.spy(printh_stub).was_called_with("[default] error: message1") - end) - - it('should print with explicit category: default', function () - err("message1") - assert.spy(printh_stub).was_called(1) - assert.spy(printh_stub).was_called_with("[default] error: message1") - end) - - end) - - end) - - describe('(flow category active)', function () - - before_each(function () - logger.active_categories.flow = true - end) - - describe('log', function () - - it('should print with explicit category: flow', function () - log("message1", "flow") - assert.spy(printh_stub).was_called(1) - assert.spy(printh_stub).was_called_with("[flow] message1") - end) - - end) - - describe('warn', function () - - it('should print with explicit category: flow', function () - warn("message1", "flow") - assert.spy(printh_stub).was_called(1) - assert.spy(printh_stub).was_called_with("[flow] warning: message1") - end) - - end) - - describe('err', function () - - it('should print with explicit category: flow', function () - err("message1", "flow") - assert.spy(printh_stub).was_called(1) - assert.spy(printh_stub).was_called_with("[flow] error: message1") - end) - - end) - - end) - - describe('(default category inactive)', function () - - before_each(function () - logger.active_categories.default = false - end) - - describe('log', function () - - it('should not print with no argument (default)', function () - log("message1") - assert.spy(printh_stub).was_not_called() - end) - - it('should not print with explicit category: default', function () - log("message1") - assert.spy(printh_stub).was_not_called() - end) - - end) - - describe('warn', function () - - it('should not print with no argument (default)', function () - warn("message1") - assert.spy(printh_stub).was_not_called() - end) - - it('should not print with explicit category: default', function () - warn("message1") - assert.spy(printh_stub).was_not_called() - end) - - end) - - describe('err', function () - - it('should not print with no argument (default)', function () - err("message1") - assert.spy(printh_stub).was_not_called() - end) - - it('should not print with explicit category: default', function () - err("message1") - assert.spy(printh_stub).was_not_called() - end) - - end) - - end) - - describe('(flow category inactive)', function () - - before_each(function () - logger.active_categories.flow = false - end) - - describe('log', function () - - it('should not print with explicit category: flow', function () - log("message1", "flow") - assert.spy(printh_stub).was_not_called() - end) - - end) - - describe('warn', function () - - it('should not print with explicit category: flow', function () - warn("message1", "flow") - assert.spy(printh_stub).was_not_called() - end) - - end) - - describe('err', function () - - it('should not print with explicit category: flow', function () - err("message1", "flow") - assert.spy(printh_stub).was_not_called() - end) - - end) - - end) - - end) - - describe('(logger level set to warning)', function () - - before_each(function () - logger.current_level = logging.level.warning - end) - - describe('log', function () - - it('should never print', function () - log("message1") - log("message1", "default") - log("message1", "flow") - assert.spy(printh_stub).was_not_called() - end) - - end) - - describe('(default category active)', function () - - before_each(function () - logger.active_categories.default = true - end) - - describe('warn', function () - - it('should print with no argument (default)', function () - warn("message1") - assert.spy(printh_stub).was_called(1) - assert.spy(printh_stub).was_called_with("[default] warning: message1") - end) - - it('should print with explicit category: default', function () - warn("message1") - assert.spy(printh_stub).was_called(1) - assert.spy(printh_stub).was_called_with("[default] warning: message1") - end) - - end) - - describe('err', function () - - it('should print with no argument (default)', function () - err("message1") - assert.spy(printh_stub).was_called(1) - assert.spy(printh_stub).was_called_with("[default] error: message1") - end) - - it('should print with explicit category: default', function () - err("message1") - assert.spy(printh_stub).was_called(1) - assert.spy(printh_stub).was_called_with("[default] error: message1") - end) - - end) - - end) - - describe('(flow category active)', function () - - before_each(function () - logger.active_categories.flow = true - end) - - describe('warn', function () - - it('should print with explicit category: flow', function () - warn("message1", "flow") - assert.spy(printh_stub).was_called(1) - assert.spy(printh_stub).was_called_with("[flow] warning: message1") - end) - - end) - - describe('err', function () - - it('should print with explicit category: flow', function () - err("message1", "flow") - assert.spy(printh_stub).was_called(1) - assert.spy(printh_stub).was_called_with("[flow] error: message1") - end) - - end) - - end) - - describe('(default category inactive)', function () - - before_each(function () - logger.active_categories.default = false - end) - - describe('warn', function () - - it('should not print with no argument (default)', function () - warn("message1") - assert.spy(printh_stub).was_not_called() - end) - - it('should not print with explicit category: default', function () - warn("message1") - assert.spy(printh_stub).was_not_called() - end) - - end) - - describe('err', function () - - it('should not print with no argument (default)', function () - err("message1") - assert.spy(printh_stub).was_not_called() - end) - - it('should not print with explicit category: default', function () - err("message1") - assert.spy(printh_stub).was_not_called() - end) - - end) - - end) - - describe('(flow category inactive)', function () - - before_each(function () - logger.active_categories.flow = false - end) - - describe('warn', function () - - it('should not print with explicit category: flow', function () - warn("message1", "flow") - assert.spy(printh_stub).was_not_called() - end) - - end) - - describe('err', function () - - it('should not print with explicit category: flow', function () - err("message1", "flow") - assert.spy(printh_stub).was_not_called() - end) - - end) - - end) - - end) - - describe('(logger level set to error)', function () - - before_each(function () - logger.current_level = logging.level.error - end) - - describe('log', function () - - it('should never print', function () - log("message1") - log("message1", "default") - log("message1", "flow") - assert.spy(printh_stub).was_not_called() - end) - - end) - - describe('warn', function () - - it('should never print', function () - warn("message1") - warn("message1", "default") - warn("message1", "flow") - assert.spy(printh_stub).was_not_called() - end) - - end) - - describe('(default category active)', function () - - before_each(function () - logger.active_categories.default = true - end) - - describe('err', function () - - it('should print with no argument (default)', function () - err("message1") - assert.spy(printh_stub).was_called(1) - assert.spy(printh_stub).was_called_with("[default] error: message1") - end) - - it('should print with explicit category: default', function () - err("message1") - assert.spy(printh_stub).was_called(1) - assert.spy(printh_stub).was_called_with("[default] error: message1") - end) - - end) - - end) - - describe('(flow category active)', function () - - before_each(function () - logger.active_categories.flow = true - end) - - describe('err', function () - - it('should print with explicit category: flow', function () - err("message1", "flow") - assert.spy(printh_stub).was_called(1) - assert.spy(printh_stub).was_called_with("[flow] error: message1") - end) - - end) - - end) - - describe('(default category inactive)', function () - - before_each(function () - logger.active_categories.default = false - end) - - describe('err', function () - - it('should not print with no argument (default)', function () - err("message1") - assert.spy(printh_stub).was_not_called() - end) - - it('should not print with explicit category: default', function () - err("message1") - assert.spy(printh_stub).was_not_called() - end) - - end) - - end) - - describe('(flow category inactive)', function () - - before_each(function () - logger.active_categories.flow = false - end) - - describe('err', function () - - it('should not print with explicit category: flow', function () - err("message1", "flow") - assert.spy(printh_stub).was_not_called() - end) - - end) - - end) - - end) - - describe('(logger level set to none)', function () - - before_each(function () - logger.current_level = logging.level.none - end) - - describe('log', function () - - it('should never print', function () - log("message1") - log("message1", "default") - log("message1", "flow") - assert.spy(printh_stub).was_not_called() - end) - - end) - - describe('warn', function () - - it('should never print', function () - warn("message1") - warn("message1", "default") - warn("message1", "flow") - assert.spy(printh_stub).was_not_called() - end) - - end) - - describe('err', function () - - it('should never print', function () - err("message1") - err("message1", "default") - err("message1", "flow") - assert.spy(printh_stub).was_not_called() - end) - - end) - - end) - - end) - - describe('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) - - -- basic types - - it('nil => "[nil]"', function () - assert.are_equal("[nil]", dump(nil)) - end) - it('"string" => ""string""', function () - assert.are_equal("\"string\"", dump("string")) - end) - it('true => "true"', function () - assert.are_equal("true", dump(true)) - end) - it('false => "false"', function () - assert.are_equal("false", dump(false)) - end) - it('56 => "56"', function () - assert.are_equal("56", dump(56)) - end) - it('56.2 => "56.2"', function () - assert.are_equal("56.2", dump(56.2)) - end) - - -- as_key: used to mimic key representation in lua tables - - it('"string" => "string"', function () - assert.are_equal("string", dump("string", true)) - end) - it('true => "[true]"', function () - assert.are_equal("[true]", dump(true, true)) - end) - it('56.2 => "[56.2]"', function () - assert.are_equal("[56.2]", dump(56.2, true)) - end) - - -- 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 - assert.is_true(contains_with_message({"{[7] = 5, string = \"other\"}", "{string = \"other\", [7] = 5}"}, - 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) - - it('{...} => "{[1] = 2 mytable = {[1] = 1 [2] = 3 key = "value"}}', 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) - - it('{...} => "{{[1] = 1 [2] = 3 key = "value"} = {[1] = true [2] = false}}', function () - assert.are_equal("{[{[1] = 1, [2] = 3, key = \"value\"}] = {[1] = true, [2] = false}}", dump({[{1, 3, key = "value"}] = {true, false}})) - end) - - -- 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) - - -- 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 - - it('at level 0: {} => [table]', function () - assert.are_same({"[table]", "[table]"}, {dump({}, false, 0), dump({}, true, 0)}) - end) - it('at level 1: {1, {}} => {1, [table]}', function () - assert.are_same({"{[1] = 1, [2] = [table]}", "[{[1] = 1, [2] = [table]}]"}, {dump({1, {}}, false, 1), dump({1, {}}, true, 1)}) - end) - it('at level 2: {...} => "{{[1] = 1 [2] = [table] [3] = "rest"} = {idem}', function () - assert.are_equal("{[{[1] = 1, [2] = [table], [3] = \"rest\"}] = {[1] = 1, [2] = [table], [3] = \"rest\"}}", dump({[{1, {2, {3, {4}}}, "rest"}] = {1, {2, {3, {4}}}, "rest"}}, false, 2)) - end) - it('without level arg, use default level (2): {...} => "{{[1] = 1 [2] = [table] [3] = "rest"} = {idem}', function () - logger.dump_max_recursion_level = 2 -- it is now more, so simulate a default level of 2 manually - assert.are_equal("{[{[1] = 1, [2] = [table], [3] = \"rest\"}] = {[1] = 1, [2] = [table], [3] = \"rest\"}}", dump({[{1, {2, {3, {4}}}, "rest"}] = {1, {2, {3, {4}}}, "rest"}})) - end) - - -- function - - it('function => "[function]"', function () - local f = function () - end - assert.are_same({"[function]", "[function]"}, {dump(f, false), dump(f, true)}) - 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) diff --git a/src/engine/tests/utestmath.lua b/src/engine/tests/utestmath.lua deleted file mode 100644 index 2af4482e..00000000 --- a/src/engine/tests/utestmath.lua +++ /dev/null @@ -1,557 +0,0 @@ -require("engine/test/bustedhelper") -require("engine/core/math") - -describe('almost_eq', function () - it('2.506 ~ 2.515', function () - assert.is_true(almost_eq(2.506, 2.515)) - end) - it('2.505 ~! 2.516', function () - assert.is_false(almost_eq(2.505, 2.516)) - end) - it('-5.984 ~ -5.9835 with eps=0.001', function () - assert.is_true(almost_eq(-5.984, -5.9835, 0.001)) - end) - it('-5.984 !~ -5.9828 with eps=0.001', function () - assert.is_false(almost_eq(-5.984, -5.9828, 0.001)) - end) - it('(-5.984, ) !~ -5.9828 with eps=0.001', function () - assert.is_false(almost_eq(-5.984, -5.9828, 0.001)) - end) - - it('vector(2.50501 5.8) ~ vector(2.515 5.79)', function () - assert.is_true(almost_eq(vector(2.50501, 5.8), vector(2.515, 5.79))) - end) - it('vector(2.505 5.8) !~ vector(2.515 5.788)', function () - assert.is_false(almost_eq(vector(2.505, 5.8), vector(2.515, 5.788))) - end) - it('vector(2.505 5.8) ~ vector(2.5049 5.799) with eps=0.001', function () - assert.is_true(almost_eq(vector(2.505, 5.8), vector(2.5049, 5.799), 0.001)) - end) - it('vector(2.505 5.8) !~ vector(2.5047 5.789) with eps=0.001', function () - assert.is_false(almost_eq(vector(2.505, 5.8), vector(2.5047, 5.789), 0.001)) - end) - - it('should fail when comparing non-number, non-vector types', function () - assert.has_error(function () - almost_eq("text", 68) - end, - "almost_eq cannot compare text and 68") - end) - -end) - -describe('tile_vector', function () - - describe('_init', function () - it('should create a new tile vector with the right coordinates', function () - local loc = tile_vector(2, -6) - assert.are_same({2, -6}, {loc.i, loc.j}) - end) - end) - - describe('_tostring', function () - it('should return a string representation with the right coordinates', function () - local tile_vec = tile_vector(2, -6) - assert.are_equal("tile_vector(2, -6)", tile_vec:_tostring()) - end) - end) - - describe('to_topleft_position', function () - it('(1 2) => (8 16)', function () - assert.are_equal(vector(8, 16), location(1, 2):to_topleft_position()) - end) - end) - -end) - -describe('sprite_id_location', function () - - describe('_tostring', function () - it('should return a string representation with the right coordinates', function () - local sprite_id_loc = sprite_id_location(2, -6) - assert.are_equal("sprite_id_location(2, -6)", sprite_id_loc:_tostring()) - end) - end) - - describe('to_sprite_id', function () - it('(2 2) => 34', function () - assert.are_equal(34, sprite_id_location(2, 2):to_sprite_id()) - end) - it('(15 1) => 31', function () - assert.are_equal(31, sprite_id_location(15, 1):to_sprite_id()) - end) - end) - -end) - -describe('location', function () - - describe('_tostring', function () - it('should return a string representation with the right coordinates', function () - local loc = location(2, -6) - assert.are_equal("location(2, -6)", loc:_tostring()) - end) - end) - - describe('to_center_position', function () - it('(1 2) => (12 20)', function () - assert.are_equal(vector(12, 20), location(1, 2):to_center_position()) - end) - end) - -end) - -describe('vector', function () - - describe('_init', function () - it('should create a new vector with the right coordinates', function () - local vec = vector(2, -6) - assert.are_same({2, -6}, {vec.x, vec.y}) - end) - end) - - describe('_tostring', function () - it('should return a string representation with the right coordinates', function () - local vec = vector(2, -6) - assert.are_equal("vector(2, -6)", vec:_tostring()) - end) - end) - - describe('almost_eq', function () - it('vector(2.50501 5.8) ~ vector(2.515 5.79) (static version)', function () - -- due to precision issues, 2.505 !~ 2.515 with default eps=0.01! - assert.is_true(vector.almost_eq(vector(2.50501, 5.8), vector(2.515, 5.79))) - end) - it('vector(2.50501 5.8) ~ vector(2.515 5.79)', function () - assert.is_true( vector(2.50501, 5.8):almost_eq(vector(2.515, 5.79))) - end) - it('vector(2.505 5.8) !~ vector(2.515 5.788)', function () - assert.is_false(vector(2.505, 5.8):almost_eq(vector(2.515, 5.788))) - end) - it('vector(2.505 5.8) ~ vector(2.5049 5.799) with eps=0.001', function () - assert.is_true( vector(2.505, 5.8):almost_eq(vector(2.5049, 5.799), 0.001)) - end) - it('vector(2.505 5.8) !~ vector(2.5047 5.789) with eps=0.001', function () - assert.is_false(vector(2.505, 5.8):almost_eq(vector(2.5047, 5.789), 0.001)) - end) - end) - - describe('__add', function () - it('(3 2) + (5 3) => (8 5)', function () - assert.are_equal(vector(8, 5), vector(3, 2) + vector(5, 3)) - end) - end) - - describe('add_inplace', function () - it('(3 2):add_inplace((5 3)) => in-place (8 5)', function () - local v = vector(3, 2) - v:add_inplace(vector(5, 3)) - assert.are_equal(vector(8, 5), v) - end) - end) - - describe('__sub', function () - it('(3 2) - (5 3) => (-2 -1)', function () - assert.are_equal(vector(-2, -1), vector(3, 2) - vector(5, 3)) - end) - end) - - describe('sub_inplace', function () - it('(3 2):sub_inplace((5 3)) => in-place (-2 -1)', function () - local v = vector(3, 2) - v:sub_inplace(vector(5, 3)) - assert.are_equal(vector(-2, -1), v) - end) - end) - - describe('__unm', function () - it('- (5 -3) => (-5 3)', function () - assert.are_equal(vector(-5, 3), - vector(5, -3)) - end) - end) - - describe('__mul', function () - it('(3 2) * -2 => (-6 -4)', function () - assert.are_equal(vector(-6, -4), vector(3, 2) * -2) - end) - it('4 * (-3 2) => (-12 8)', function () - assert.are_equal(vector(-12, 8), 4 * vector(-3, 2)) - end) - it('(-3 2) * (-12 8) => assert', function () - assert.has_error(function () - local _ = vector(-3, 2) * vector(-12, 8) - end, - "vector multiplication is only supported with a scalar, tried to multiply vector(-3, 2) and vector(-12, 8)") - end) - - it('(3 2):mul_inplace(-2) => in-place (-6 -4)', function () - local v = vector(3, 2) - v:mul_inplace(-2) - assert.are_equal(vector(-6, -4), v) - end) - it('(-3 2):mul_inplace((-12 8)) => assert', function () - assert.has_error(function () - vector(-3, 2):mul_inplace(vector(-12, 8)) - end, - "vector multiplication is only supported with a scalar, tried to multiply vector(-3, 2) and vector(-12, 8)") - end) - end) - - describe('__div', function () - it('(3, 2) / -2 => (-1.5, -1)', function () - assert.are_equal(vector(-1.5, -1), vector(3, 2) / -2) - end) - it('4 / (-3, 2) => assert', function () - assert.has_error(function () - local _ = 4 / vector(-3, 2) - end, - "vector division is only supported with a scalar as rhs, tried to multiply 4 and vector(-3, 2)") - end) - it('(-3 2) / (-3, 2) => assert', function () - assert.has_error(function () - local _ = vector(-3, 2) / vector(-3, 2) - end, - "vector division is only supported with a scalar as rhs, tried to multiply vector(-3, 2) and vector(-3, 2)") - end) - it('(-3 2) / 0 => assert', function () - assert.has_error(function () - local _ = vector(-3, 2) / 0 - end, - "cannot divide vector vector(-3, 2) by zero") - end) - - it('(3 2):div_inplace(-2) => in-place (-6 -4)', function () - local v = vector(3, 2) - v:div_inplace(-2) - assert.are_equal(vector(-1.5, -1), v) - end) - it('(-3 2):div_inplace(-3, 2) => assert', function () - assert.has_error(function () - local _ = vector(-3, 2):div_inplace(vector(-3, 2)) - end, - "vector division is only supported with a scalar as rhs, tried to multiply vector(-3, 2) and vector(-3, 2)") - end) - it('(-3 2):div_inplace(0) => assert', function () - assert.has_error(function () - vector(-3, 2):div_inplace(0) - end, - "cannot divide vector vector(-3, 2) by zero") - end) - end) - - describe('zero()', function () - it('should be vector(0, 0)', function () - assert.are_equal(vector(0, 0), vector.zero()) - end) - it('should be mutable', function () - local z = vector.zero() - z.x = 5 - assert.are_equal(5, z.x) - end) - end) - - describe('is_zero()', function () - it('vector.zero() is zero', function () - assert.is_true(vector.zero():is_zero()) - end) - it('(0 0) is zero', function () - assert.is_true(vector(0, 0):is_zero()) - end) - it('(2 3) is not zero', function () - assert.is_false(vector(2, 3):is_zero()) - end) - end) - - describe('sqr_magnitude', function () - it('(4 3) => 25', function () - assert.are_equal(25, vector(4, 3):sqr_magnitude()) - end) - it('(-4 3) => 25', function () - assert.are_equal(25, vector(-4, 3):sqr_magnitude()) - end) - it('(9 -14.2) => 282.64', function () - assert.is_true(almost_eq(vector(9, -14.2):sqr_magnitude(), 282.64)) - end) - it('(0 0) => 0', function () - assert.are_equal(0, vector.zero():sqr_magnitude()) - end) - end) - - describe('magnitude', function () - it('(4 3) => 5', function () - assert.is_true(almost_eq(vector(4, 3):magnitude(), 5)) - end) - it('(-4 3) => 5', function () - assert.is_true(almost_eq(vector(-4, 3):magnitude(), 5)) - end) - it('(9 -14.2) => 16.811900547', function () - assert.is_true(almost_eq(vector(9, -14.2):magnitude(), 16.811900547)) - end) - it('(0 0) => 0', function () - assert.are_equal(0, vector.zero():magnitude()) - end) - end) - - describe('normalized', function () - it('(1 -1) => (0.707... -0.707...)', function () - assert.is_true(vector(1, -1):normalized():almost_eq(vector(0.707, -0.707))) - end) - it('(4 3) => (0.8 0.6)', function () - assert.is_true(vector(4, 3):normalized():almost_eq(vector(0.8, 0.6))) - end) - it('(0 0) => (0 0)', function () - assert.is_true(vector.zero():normalized():almost_eq(vector.zero())) - end) - end) - - describe('normalize', function () - it('(1 -1) => (0.707... -0.707...) in place', function () - local v = vector(1, -1) - v:normalize() - assert.is_true(v:almost_eq(vector(0.707, -0.707))) - end) - it('(4 3) => (0.8 0.6) in place', function () - local v = vector(4, 3) - v:normalize() - assert.is_true(v:almost_eq(vector(0.8, 0.6))) - end) - it('(0 0) => (0 0) in place', function () - local v = vector(0, 0) - v:normalize() - assert.are_equal(vector(0, 0), v) - end) - end) - - describe('with_clamped_magnitude', function () - it('(1 -1).with_clamped_magnitude(1) => (0.707... -0.707...)', function () - assert.is_true(vector(1, -1):with_clamped_magnitude(1):almost_eq(vector(0.707, -0.707))) - end) - it('(4 3).with_clamped_magnitude(10) => (4 3)', function () - assert.is_true(vector(4, 3):with_clamped_magnitude(10):almost_eq(vector(4, 3))) - end) - it('(4 3).with_clamped_magnitude(5) => (4 3)', function () - assert.is_true(vector(4, 3):with_clamped_magnitude(5):almost_eq(vector(4, 3))) - end) - it('(4 3).with_clamped_magnitude(0) => (0 0)', function () - assert.is_true(vector(4, 3):with_clamped_magnitude(0):almost_eq(vector(0, 0))) - end) - it('(0 0).with_clamped_magnitude(5) => (0 0)', function () - assert.is_true(vector(0, 0):with_clamped_magnitude(5):almost_eq(vector(0, 0))) - end) - end) - - describe('clamp_magnitude', function () - it('(4 -3).clamp_magnitude(2) => (1.6 -1.2)', function () - local v = vector(4, -3) - v:clamp_magnitude(2) - assert.is_true(v:almost_eq(vector(1.6, -1.2))) - end) - it('(4 3).clamp_magnitude(10) => (4 3)', function () - local v = vector(4, 3) - v:clamp_magnitude(10) - assert.is_true(v:almost_eq(vector(4, 3))) - end) - it('(4 3).clamp_magnitude(5) => (4 3)', function () - local v = vector(4, 3) - v:clamp_magnitude(5) - assert.is_true(v:almost_eq(vector(4, 3))) - end) - it('(4 3).clamp_magnitude(0) => (0 0)', function () - local v = vector(4, 3) - v:clamp_magnitude(0) - assert.is_true(v:almost_eq(vector(0, 0))) - end) - it('(0 0).clamp_magnitude(5) => (0 0)', function () - local v = vector(0, -0) - v:clamp_magnitude(5) - assert.is_true(v:almost_eq(vector(0, 0))) - end) - end) - - describe('with_clamped_magnitude_cardinal', function () - it('(1 -1).with_clamped_magnitude_cardinal(1) => (1 -1)', function () - assert.is_true(vector(1, -1):with_clamped_magnitude_cardinal(1):almost_eq(vector(1, -1))) - end) - it('(4 -3).with_clamped_magnitude_cardinal(2) => (2 -2)', function () - assert.is_true(vector(4, -3):with_clamped_magnitude_cardinal(2):almost_eq(vector(2, -2))) - end) - it('(-4 2).with_clamped_magnitude_cardinal(3) => (-3 3)', function () - assert.is_true(vector(-4, 2):with_clamped_magnitude_cardinal(3):almost_eq(vector(-3, 2))) - end) - it('(4 -8).with_clamped_magnitude_cardinal(3 5) => (3 -5)', function () - assert.is_true(vector(4, -8):with_clamped_magnitude_cardinal(3, 5):almost_eq(vector(3, -5))) - end) - it('(0 0).with_clamped_magnitude_cardinal(5) => (0 0)', function () - assert.is_true(vector(0, 0):with_clamped_magnitude_cardinal(5):almost_eq(vector(0, 0))) - end) - end) - - describe('clamp_magnitude_cardinal', function () - it('(4 -3).clamp_magnitude_cardinal(2 6) => (2, -3)', function () - local v = vector(4, -3) - v:clamp_magnitude_cardinal(2, 6) - assert.is_true(v:almost_eq(vector(2, -3))) - end) - it('(4 3).clamp_magnitude_cardinal(10) => (4 3)', function () - local v = vector(4, 3) - v:clamp_magnitude_cardinal(10) - assert.is_true(v:almost_eq(vector(4, 3))) - end) - it('(-4 3).clamp_magnitude_cardinal(5 1) => (-4 1)', function () - local v = vector(-4, 3) - v:clamp_magnitude_cardinal(5, 1) - assert.is_true(v:almost_eq(vector(-4, 1))) - end) - it('(-4 -3).clamp_magnitude_cardinal(2) => (-2 -2)', function () - local v = vector(-4, -3) - v:clamp_magnitude_cardinal(2) - assert.is_true(v:almost_eq(vector(-2, -2))) - end) - it('(0 0).clamp_magnitude_cardinal(5) => (0 0)', function () - local v = vector(0, 0) - v:clamp_magnitude_cardinal(5) - assert.is_true(v:almost_eq(vector(0, 0))) - end) - end) - - describe('mirror_x', function () - it('(1 -3).mirror_x() => (-1, -3)', function () - local v = vector(1, -3) - v:mirror_x() - assert.are_equal(vector(-1, -3), v) - end) - end) - - describe('mirror_y', function () - it('(1 -3).mirror_y() => (1, 3)', function () - local v = vector(1, -3) - v:mirror_y() - assert.are_equal(vector(1, 3), v) - end) - end) - - describe('? rotate_90_cw', function () - it('(1 -3).rotated_90_cw() => (3, 1)', function () - assert.are_equal(vector(3, 1), vector(1, -3):rotated_90_cw()) - end) - end) - - describe('? rotate_90_cw_inplace', function () - it('(1 -3).rotate_90_cw_inplace() => (3, 1)', function () - local v = vector(1, -3) - v:rotate_90_cw_inplace() - assert.are_equal(vector(3, 1), v) - end) - end) - - describe('? rotated_90_ccw', function () - it('(1 -3).rotated_90_ccw() => (-3, -1)', function () - assert.are_equal(vector(-3, -1), vector(1, -3):rotated_90_ccw()) - end) - end) - - describe('? rotate_90_ccw_inplace', function () - it('(1 -3).rotate_90_ccw_inplace() => (-3, -1)', function () - local v = vector(1, -3) - v:rotate_90_ccw_inplace() - assert.are_equal(vector(-3, -1), v) - end) - end) - - describe('to_location', function () - it('(12, -5) => (1, -1)', function () - local v = vector(12, -5) - assert.are_equal(location(1, -1), v:to_location()) - end) - end) - -end) - -describe('signed_speed_to_dir', function () - it('should -5 => left', function () - assert.are_equal(horizontal_dirs.left, signed_speed_to_dir(-5)) - end) - it('should 2 => right', function () - assert.are_equal(horizontal_dirs.right, signed_speed_to_dir(2)) - end) - it('should 0 => assert', function () - assert.has_error(function () - signed_speed_to_dir(0) - end) - end) -end) - -describe('oppose_dir', function () - it('should left => right', function () - assert.are_equal(directions.right, oppose_dir(directions.left)) - end) - it('should right => left', function () - assert.are_equal(directions.left, oppose_dir(directions.right)) - end) - it('should up => down', function () - assert.are_equal(directions.down, mirror_dir_y(directions.up)) - end) - it('should down => up', function () - assert.are_equal(directions.up, mirror_dir_y(directions.down)) - end) -end) - -describe('mirror_dir_x', function () - it('should left => right', function () - assert.are_equal(directions.right, mirror_dir_x(directions.left)) - end) - it('should right => left', function () - assert.are_equal(directions.left, mirror_dir_x(directions.right)) - end) - it('should up => up', function () - assert.are_equal(directions.up, mirror_dir_x(directions.up)) - end) - it('should down => down', function () - assert.are_equal(directions.down, mirror_dir_x(directions.down)) - end) -end) - -describe('+ mirror_dir_y', function () - it('should up => down', function () - assert.are_equal(directions.down, mirror_dir_y(directions.up)) - end) - it('should down => up', function () - assert.are_equal(directions.up, mirror_dir_y(directions.down)) - end) - it('should left => left', function () - assert.are_equal(directions.left, mirror_dir_y(directions.left)) - end) - it('should right => right', function () - assert.are_equal(directions.right, mirror_dir_y(directions.right)) - end) -end) - -describe('rotate_dir_90_cw', function () - it('should left => up', function () - assert.are_equal(directions.up, rotate_dir_90_cw(directions.left)) - end) - it('should up => right', function () - assert.are_equal(directions.right, rotate_dir_90_cw(directions.up)) - end) - it('should right => down', function () - assert.are_equal(directions.down, rotate_dir_90_cw(directions.right)) - end) - it('should down => left', function () - assert.are_equal(directions.left, rotate_dir_90_cw(directions.down)) - end) -end) - -describe('rotate_dir_90_ccw', function () - it('should left => down', function () - assert.are_equal(directions.down, rotate_dir_90_ccw(directions.left)) - end) - it('should down => right', function () - assert.are_equal(directions.right, rotate_dir_90_ccw(directions.down)) - end) - it('should right => up', function () - assert.are_equal(directions.up, rotate_dir_90_ccw(directions.right)) - end) - it('should up => left', function () - assert.are_equal(directions.left, rotate_dir_90_ccw(directions.up)) - end) -end) diff --git a/src/engine/tests/utestpico8api.lua b/src/engine/tests/utestpico8api.lua deleted file mode 100644 index 89cebf41..00000000 --- a/src/engine/tests/utestpico8api.lua +++ /dev/null @@ -1,1190 +0,0 @@ -require("engine/test/bustedhelper") -require("engine/core/math") - -describe('pico8api', function () - - describe('camera', function () - - after_each(function () - camera() - end) - - it('should the camera to a floored pixel-perfect position', function () - camera(5.1, -11.5) - assert.are_same({5, -12}, {pico8.camera_x, pico8.camera_y}) - end) - - it('should reset the camera with no arguments', function () - camera(5.1, -11.5) - camera() - assert.are_same({0, 0}, {pico8.camera_x, pico8.camera_y}) - end) - - end) - - describe('clip', function () - - after_each(function () - clip() - end) - - it('should set clip to floored pixel-perfect (x, y, w, h)', function () - clip(5.7, 12.4, 24.2, 48.1) - assert.are_same({5, 12, 24, 48}, pico8.clip) - end) - - it('should reset the clip with no arguments', function () - clip() - assert.are_same(nil, pico8.clip) - end) - - end) - - describe('cls', function () - - it('should clear the clip', function () - cls() - assert.is_nil(pico8.clip) - end) - - end) - - describe('pset', function () - - it('should set the current color', function () - pset(5, 8, 7) - assert.are_equal(7, pico8.color) - end) - - end) - - describe('pget', function () - - it('should return 0 (no simulation)', function () - assert.are_equal(0, pget(8, 5)) - end) - - end) - - describe('color', function () - - it('should set the current color', function () - color(9) - assert.are_equal(9, pico8.color) - end) - - it('should set the current color (with modulo 16)', function () - color(17) - assert.are_equal(1, pico8.color) - end) - - it('should reset the current color with no arguments', function () - color() - assert.are_equal(0, pico8.color) - end) - - end) - - describe('tonum', function () - - it('should return the number corresponding to a number', function () - assert.are_equal(-25.34, tonum(-25.34)) - end) - - it('should return the positive number corresponding to a string', function () - assert.are_equal(25, tonum("25")) - end) - - 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) - - -- 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)) -- 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")) - end) - it('true => "true"', function () - assert.are_equal("true", tostr(true)) - end) - it('false => "false"', function () - assert.are_equal("false", tostr(false)) - end) - it('56 => "56"', function () - assert.are_equal("56", tostr(56)) - end) - it('56.2 => "56.2"', function () - assert.are_equal("56.2", tostr(56.2)) - end) - 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) - it('function => "[function]"', function () - local f = function () - end - assert.are_equal("[function]", tostr(f)) - end) - - end) - - describe('(testing color)', function () - - after_each(function () - color() - end) - - describe('api.print', function () - - it('should set the color if passed', function () - api.print("hello", 45, 78, 7) - assert.are_equal(7, pico8.color) - end) - - it('should preserve the color if not passed', function () - color(5) - api.print("hello", 45, 78) - assert.are_equal(5, pico8.color) - end) - - end) - - describe('rect', function () - - it('should set the color if passed', function () - rect(1, 2, 3, 4, 7) - assert.are_equal(7, pico8.color) - end) - - it('should preserve the color if not passed', function () - color(5) - rect(1, 2, 3, 4) - assert.are_equal(5, pico8.color) - end) - - end) - - describe('rectfill', function () - - it('should set the color if passed', function () - rectfill(1, 2, 3, 4, 7) - assert.are_equal(7, pico8.color) - end) - - it('should preserve the color if not passed', function () - color(5) - rectfill(1, 2, 3, 4) - assert.are_equal(5, pico8.color) - end) - - end) - - describe('circ', function () - - it('should set the color if passed', function () - circ(1, 2, 10, 7) - assert.are_equal(7, pico8.color) - end) - - it('should preserve the color if not passed', function () - color(5) - circ(1, 2, 3) - assert.are_equal(5, pico8.color) - end) - - end) - - describe('circfill', function () - - it('should set the color if passed', function () - circfill(1, 2, 10, 7) - assert.are_equal(7, pico8.color) - end) - - it('should preserve the color if not passed', function () - color(5) - circfill(1, 2, 10) - assert.are_equal(5, pico8.color) - end) - - end) - - describe('line', function () - - it('should set the color if passed', function () - line(1, 2, 10, 12, 7) - assert.are_equal(7, pico8.color) - end) - - it('should preserve the color if not passed', function () - color(5) - line(1, 2, 10, 12) - assert.are_equal(5, pico8.color) - end) - - end) - - describe('pal', function () - - it('should reset the transparency with no arguments', function () - pal() - assert.are_same({[0] = true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false}, - pico8.pal_transparent) - end) - - end) - - describe('palt', function () - - it('should set the color to transparent', function () - palt(3, true) - assert.are_equal(true, pico8.pal_transparent[3]) - end) - - it('should set the color to opaque', function () - palt(3, false) - assert.are_equal(false, pico8.pal_transparent[3]) - end) - - it('should reset the transparency with no arguments', function () - palt() - assert.are_same({[0] = true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false}, - pico8.pal_transparent) - end) - - end) - - end) - - describe('mget', function () - - before_each(function () - pico8.map[14] = { [27] = 5 } - end) - - after_each(function () - pico8.map[14][27] = 0 - end) - - it('should return the sprite id at a map coordinate', function () - assert.are_equal(5, mget(27, 14)) - end) - - it('should return 0 if out of bounds', function () - assert.are_equal(0, mget(-1, 15)) - end) - - end) - - describe('mset', function () - - after_each(function () - pico8.map[14] = 0 - end) - - it('should set the sprite id at a map coordinate', function () - mset(27, 14, 9) - assert.are_equal(9, mget(27, 14)) - end) - - end) - - describe('fget', function () - - before_each(function () - pico8.spriteflags[3] = 0xa2 - end) - - after_each(function () - pico8.spriteflags[3] = 0 - end) - - it('should return the sprite flags for the passed sprite id', function () - assert.are_equal(0xa2, fget(3)) - end) - - it('should return 0 for a sprite id outside [0-255]', function () - assert.are_same({0x0, 0x0}, {fget(-1), fget(256)}) - end) - - it('should return if a specific sprite flag is set on a passed sprite id', function () - assert.are_same({true, true, true, false}, {fget(3, 1), fget(3, 5), fget(3, 7), fget(3, 6)}) - end) - - it('should return false for unset sprite flag (just to simplify simulation without setup)', function () - assert.are_same({false, false, false, false}, {fget(1, 1), fget(2, 5), fget(4, 7), fget(5, 6)}) - end) - - it('should return false for unset sprite flag (just to simplify simulation without setup)', function () - assert.are_same({false, false, false, false}, {fget(1, 1), fget(2, 5), fget(4, 7), fget(5, 6)}) - end) - - it('should return false for any flag for a sprite id outside [0-255]', function () - assert.are_same({false, false}, {fget(-1, 1), fget(256, 10)}) - end) - - end) - - describe('fset', function () - - after_each(function () - pico8.spriteflags[3] = 0 - end) - - it('should set the sprite flags for the passed sprite id', function () - fset(3, 0xa2) - assert.are_equal(0xa2, fget(3)) - end) - - it('should set a specific sprite flag on a passed sprite id', function () - fset(3, 1, false) - fset(3, 3, true) - fset(3, 5, true) - fset(3, 7, true) - fset(3, 7, false) - assert.are_same({false, true, true, false, false}, {fget(3, 1), fget(3, 3), fget(3, 5), fget(3, 6), fget(3, 7)}) - end) - - end) - - describe('sget', function () - - it('should return 0 (no simulation)', function () - assert.are_equal(0, sget(3)) - end) - - end) - - describe('music', function () - - it('should set the current music with defaults', function () - music(4, 14, 6) - assert.are_same({music=4, fadems=14, channel_mask=6}, pico8.current_music) - music(7) - assert.are_same({music=7, fadems=0, channel_mask=0}, pico8.current_music) - end) - - it('should reset the current music with no arguments', function () - music(-1) - assert.is_nil(pico8.current_music) - end) - - it('should fallback to music pattern 0 if < -1 is passed', function () - music(-2) - assert.are_same({music=0, fadems=0, channel_mask=0}, pico8.current_music) - end) - - end) - - describe('(memory setup)', function () - - before_each(function () - pico8.poked_addresses[4] = 0xa2 - pico8.poked_addresses[5] = 0x00 - pico8.poked_addresses[6] = 0x10 - pico8.poked_addresses[7] = 0x01 - pico8.poked_addresses[8] = 0x00 - pico8.poked_addresses[9] = 0x00 - pico8.poked_addresses[10] = 0x00 - pico8.poked_addresses[11] = 0x00 - pico8.poked_addresses[12] = 0x14 - pico8.poked_addresses[13] = 0xde - pico8.poked_addresses[14] = 0x48 - pico8.poked_addresses[15] = 0x3a - pico8.poked_addresses[16] = 0xc5 - pico8.poked_addresses[17] = 0x97 - pico8.poked_addresses[18] = 0xb2 - pico8.poked_addresses[19] = 0x01 - end) - - after_each(function () - clear_table(pico8.poked_addresses) - end) - - describe('peek', function () - - it('should return the memory at the address', function () - assert.are_equal(0xa2, peek(4)) - end) - - end) - - describe('poke', function () - - it('should set the memory at the address', function () - poke(4, 0xb3) - assert.are_equal(0xb3, peek(4)) - end) - - end) - - describe('peek4', function () - - it('should return the batch memory at the address', function () - assert.are_equal(0x0110.00a2, peek4(4)) - end) - - end) - - describe('poke4', function () - - it('should set the batch memory at the address', function () - poke4(4, 0x12bc.30b3) - assert.are_equal(0x12bc.30b3, peek4(4)) - assert.are_same({0x12, 0xbc, 0x30, 0xb3}, {peek(7), peek(6), peek(5), peek(4)}) - end) - - end) - - describe('memcpy', function () - - it('should copy the memory at the address for length', function () - memcpy(8, 4, 4) - assert.are_equal(0x0110.00a2, peek4(4)) - assert.are_equal(0x0110.00a2, peek4(8)) - end) - - it('should copy the memory at the address for length to address earlier in memory with overlap', function () - memcpy(8, 12, 8) - assert.are_equal(0x01b2.97c5, peek4(16)) - assert.are_equal(0x01b2.97c5, peek4(12)) - assert.are_equal(0x3a48.de14, peek4(8)) - end) - - it('should do nothing if len < 1 or dest_addr == source_addr', function () - memcpy(8, 12, 0) - memcpy(8, 8, 0) - assert.are_equal(0x0, peek4(8)) - end) - - end) - - describe('memset', function () - - it('should set the same byte in memory at the address along length', function () - memset(8, 0x24, 4) - assert.are_equal(0x2424.2424, peek4(8)) - end) - - it('should do nothing if len < 1', function () - memset(8, 0x24, 0) - assert.are_equal(0x0, peek4(8)) - end) - - end) - - end) - - describe('rnd', function () - - it('should return a number between 0 and 1 by default', function () - assert.is_true(0 <= rnd()) - assert.is_true(rnd() < 1) - end) - - it('should return a number between 0 and x (positive)', function () - assert.is_true(0 <= rnd(10)) - assert.is_true(rnd() < 10) - end) - - -- negative input returns a random float from MIN to MAX, but this is undocumented - - end) - - describe('srand', function () - - local randomseed_stub - - setup(function () - randomseed_stub = stub(math, "randomseed") - end) - - teardown(function () - randomseed_stub:revert() - end) - - after_each(function () - randomseed_stub:clear() - end) - - it('should call math.randomseed', function () - srand(0x.425a) - assert.spy(randomseed_stub).was_called(1) - assert.spy(randomseed_stub).was_called_with(0x425a) - end) - - end) - - describe('flr', function () - - it('should return a floored value', function () - assert.are_same({2, 5, -4}, {flr(2), flr(5.5), flr(-3.1)}) - end) - - it('should return 0 by default', function () - assert.are_equal(0, flr()) - end) - - end) - - describe('ceil', function () - - it('should return a ceiled value', function () - assert.are_same({-1, 6, -3}, {ceil(-1), ceil(5.5), ceil(-3.1)}) - end) - - it('should return 0 by default', function () - assert.are_equal(0, ceil()) - end) - - end) - - describe('sgn', function () - - it('should return 1 for a positive value', function () - assert.are_equal(1, sgn(1.5)) - end) - - it('should return 1 for 0', function () - assert.are_equal(1, sgn(0)) - end) - - it('should return -1 for a negative value', function () - assert.are_equal(-1, sgn(-1.5)) - end) - - end) - - describe('min', function () - - it('should return the minimum of two values', function () - assert.are_equal(-4, min(-4, 1.5)) - assert.are_equal(1.5, min(5, 1.5)) - end) - - it('should return 0 by default', function () - assert.are_equal(0, min()) - end) - - end) - - describe('max', function () - - it('should return the maximum of two values', function () - assert.are_equal(1.5, max(-4, 1.5)) - end) - - it('should return 0 by default', function () - assert.are_equal(0, max()) - end) - - end) - - describe('mid', function () - - it('should return the mid of 3 values', function () - assert.are_equal(1.5, mid(3, -4, 1.5)) - assert.are_equal(3, mid(3, 5, 1.5)) - assert.are_equal(2, mid(3, 2, 1.5)) - end) - - end) - - describe('cos', function () - - it('should return 1 by default (0 angle)', function () - assert.are_equal(1, cos()) - end) - - it('should return 1 for 0 turn ratio', function () - assert.are_equal(1, cos(0)) - end) - - it('should return 0 for 0.25 turn ratio', function () - assert.is_true(almost_eq_with_message(0, cos(0.25))) - end) - - it('should return -1 for 0.5 turn ratio', function () - assert.is_true(almost_eq_with_message(-1, cos(0.5))) - end) - - it('should return 0 for 0.75 turn ratio', function () - assert.is_true(almost_eq_with_message(0, cos(0.75))) - end) - - it('should return 1 for 1 turn ratio', function () - assert.is_true(almost_eq_with_message(1, cos(1))) - end) - - end) - - describe('sin (clockwise)', function () - - it('should return 0 by default (0 angle)', function () - assert.are_equal(0, sin()) - end) - - it('should return 0 for 0 turn ratio', function () - assert.are_equal(0, sin(0)) - end) - - it('should return -1 for 0.25 turn ratio', function () - assert.is_true(almost_eq_with_message(-1, sin(0.25))) - end) - - it('should return 0 for 0.5 turn ratio', function () - assert.is_true(almost_eq_with_message(0, sin(0.5))) - end) - - it('should return 1 for 0.75 turn ratio', function () - assert.is_true(almost_eq_with_message(1, sin(0.75))) - end) - - it('should return 0 for 1 turn ratio', function () - assert.is_true(almost_eq_with_message(0, sin(1))) - end) - - end) - - describe('atan2 (clockwise)', function () - - it('should return 0 for (1, 0)', function () - assert.is_true(almost_eq_with_message(0, atan2(1, 0))) - end) - it('should return 0.875 for (1, 1)', function () - assert.is_true(almost_eq_with_message(0.875, atan2(1, 1))) - end) - it('should return 0.75 for (0, 1)', function () - assert.is_true(almost_eq_with_message(0.75, atan2(0, 1))) - end) - it('should return 0.625 for (-1, 1)', function () - assert.is_true(almost_eq_with_message(0.625, atan2(-1, 1))) - end) - it('should return.0.5 for (-1, 0)', function () - assert.is_true(almost_eq_with_message(0.5, atan2(-1, 0))) - end) - it('should return 0.375 for (-1, -1)', function () - assert.is_true(almost_eq_with_message(0.375, atan2(-1, -1))) - end) - it('should return 0.25 for (0, -1)', function () - assert.is_true(almost_eq_with_message(0.25, atan2(0, -1))) - end) - it('should return 0.125 for (1, -1)', function () - assert.is_true(almost_eq_with_message(0.125, atan2(1, -1))) - end) - it('should return 0.875 for (99, 99)', function () - assert.is_true(almost_eq_with_message(0.875, atan2(99, 99))) - end) - it('should return 0.75 for (0, 0) (special case)', function () - assert.is_true(almost_eq_with_message(0.75, atan2(0, 0))) - end) - - end) - - describe('band', function () - - it('should return binary and result', function () - assert.are_equal(0xa0, band(0xa2, 0xa0)) - assert.are_equal(0, band(0xa2, 0x01)) - end) - - it('should return binary or result', function () - assert.are_equal(0xa2, bor(0xa2, 0xa0)) - assert.are_equal(0xa3, bor(0xa2, 0x01)) - end) - it('should return binary xor result', function () - assert.are_equal(0x2, bxor(0xa2, 0xa0)) - assert.are_equal(0xa3, bxor(0xa2, 0x01)) - assert.are_equal(0xa2, bxor(0xa3, 0x01)) - end) - - -- be careful, as native Lua doesn't use the same float representation - -- we simulate what we can in pico8api but negative values will appear differently - -- so instead of playing with ffff... we use the minus sign for testing - - it('should return binary not result', function () - assert.are_equal(-0xb.0001, bnot(0xb)) - end) - - it('should return binary shl result', function () - assert.are_equal(0xa200, shl(0xa2, 8)) - end) - it('should return binary right arithmetic shift result', function () - assert.are_equal(0x2, shr(0x200, 8)) - assert.are_equal(0xffa2, shr(0xa200, 8)) - end) - it('should return binary right logical shift result', function () - assert.are_equal(0xa2, lshr(0xa200, 8)) - end) - it('should return binary rol result', function () - assert.are_equal(0x0000.0f00, rotl(0xf000.0000, 12)) - end) - it('should return binary ror result', function () - assert.are_equal(0xf000.0000, rotr(0x0000.0f00, 12)) - end) - - end) - - describe('time', function () - - setup(function () - pico8.frames = 120 - end) - - teardown(function () - pico8.frames = 0 - end) - - it('should return the time in sec', function () - assert.are_equal(2, time()) - end) - - end) - - describe('buttons', function () - - setup(function () - pico8.keypressed[0] = { - [0] = true, -- left - false, -- right - true, -- up - false, -- down - false, -- o - false -- x - } - pico8.keypressed[1] = { - [0] = false, -- left - false, -- right - true, -- up - true, -- down - false, -- o - true -- x - } - end) - - teardown(function () - clear_table(pico8.keypressed[0]) - clear_table(pico8.keypressed[1]) - end) - - after_each(function () - pico8.keypressed.counter = 0 - end) - - describe('btn', function () - - - it('should return the pressed buttons bitfield for both players with no arguments', function () - assert.are_equal(1 << 0 | 1 << 2 | 1 << (8+2)| 1 << (8+3) | 1 << (8+5), btn()) - end) - - it('should return whether a player pressed a button (player 0 by default)', function () - assert.is_true(btn(2)) - assert.is_false(btn(1)) - end) - - it('should return whether a player pressed a button', function () - assert.is_true(btn(2, 0)) - assert.is_true(btn(5, 1)) - assert.is_false(btn(5, 2)) - end) - - end) - - describe('btnp', function () - - it('should return the just pressed buttons bitfield for both players with no arguments', function () - pico8.keypressed.counter = 1 - assert.are_equal(1 << 0 | 1 << 2 | 1 << (8+2)| 1 << (8+3) | 1 << (8+5), btnp()) - pico8.keypressed.counter = 0 - assert.are_equal(0x0, btnp()) - end) - - it('should return whether a player pressed a button (player 0 by default)', function () - pico8.keypressed.counter = 1 - assert.is_true(btnp(2)) - assert.is_false(btnp(1)) - pico8.keypressed.counter = 0 - assert.is_false(btnp(2)) - end) - - it('should return whether a player pressed a button', function () - pico8.keypressed.counter = 1 - assert.is_true(btnp(2, 0)) - assert.is_true(btnp(5, 1)) - assert.is_false(btnp(5, 2)) - pico8.keypressed.counter = 0 - assert.is_false(btnp(2, 0)) - assert.is_false(btnp(5, 1)) - end) - - - end) - - end) - - describe('cartridge data', function () - - before_each(function () - pico8.cartdata[60] = 468 - end) - - after_each(function () - pico8.cartdata[60] = nil - end) - - describe('dget', function () - - it('should return persistent cartridge data at the given index', function () - assert.are_equal(468, dget(60)) - end) - - it('should return nil for index out of range', function () - assert.is_nil(dget(70)) - end) - - end) - - describe('dset', function () - - it('should set persistent cartridge data at the given index', function () - dset(60, 42) - assert.are_equal(42, dget(60)) - end) - - it('should do nothing if index is out of range', function () - dset(70, 42) - assert.is_nil(pico8.cartdata[70]) - end) - - end) - - end) - - describe('stat data', function () - - setup(function () - pico8.memory_usage = 124 - pico8.total_cpu = 542 - pico8.system_cpu = 530 - pico8.clipboard = "nice" - pico8.mousepos.x = 78 - pico8.mousepos.y = 54 - pico8.mousebtnpressed = {false, true, false} - pico8.mwheel = -2 - end) - - after_each(function () - pico8.cartdata[60] = nil - end) - - describe('stat', function () - - it('0: memory usage', function () - assert.are_equal(124, stat(0)) - end) - it('1: total cpu', function () - assert.are_equal(542, stat(1)) - end) - it('2: system cpu', function () - assert.are_equal(530, stat(2)) - end) - it('4: clipboard', function () - assert.are_equal("nice", stat(4)) - end) - it('6: 0 (load param not supported)', function () - assert.are_equal(0, stat(6)) - end) - it('7-9: fps (don\'t mind variants)', function () - assert.are_equal(60, stat(7)) - assert.are_equal(60, stat(8)) - assert.are_equal(60, stat(9)) - end) - it('16-23: 0 (audio channels not supported)', function () - assert.are_equal(0, stat(20)) - end) - it('30: 0 (devkit keyboard not supported)', function () - assert.are_equal(0, stat(30)) - end) - it('31: "" (devkit keyboard not supported)', function () - assert.are_equal("", stat(31)) - end) - it('32-33: mouse position', function () - assert.are_same({78, 54}, {stat(32), stat(33)}) - end) - it('31: "" (devkit keyboard not supported)', function () - assert.are_equal("", stat(31)) - end) - it('34: devkit mouse bitfield', function () - assert.are_equal(2, stat(34)) - end) - it('34: devkit mousewheel speed', function () - assert.are_equal(-2, stat(36)) - end) - it('80-85: utc time', function () - assert.are_equal(os.date("!*t")["year"], stat(80)) - end) - it('90-95: local time', function () - assert.are_equal(os.date("*t")["year"], stat(90)) - end) - it('100: nil (load breadcrumb not supported)', function () - assert.is_nil(stat(100)) - end) - it('other: 0', function () - assert.are_equal(0, stat(257)) - end) - - end) -- stat - - end) -- stat data - - describe('all', function () - - it('should return an iterator function over a sequence', function () - local t = {4, 5, 9} - local result = {} - for value in all(t) do - result[#result+1] = value - end - assert.are_same(t, result) - end) - - 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) - - end) - - describe('foreach', function () - - it('should apply a callback function to a sequence', function () - local t = {4, 5, 9} - local result = {} - foreach(t, function (value) - result[#result+1] = value - end) - assert.are_same(t, result) - end) - - end) - - describe('count', function () - - it('should return the number of non-nil elements in a sequence', function () - local t = {1, 2, 3, 4, nil} - assert.are_equal(4, count(t)) - end) - - end) - - describe('add', function () - - it('should add an element in a sequence', function () - local t = {1, 2, 3, 4} - add(t, 5) - assert.are_same({1, 2, 3, 4, 5}, t) - end) - - end) - - describe('del', function () - - it('should remove an element from a sequence', function () - local t = {1, 2, 3, 4} - del(t, 2) - assert.are_same({1, 3, 4}, t) - end) - - it('should remove an element from a sequence (by equality)', function () - local t = {1, 2, vector(4, 5), 4} - del(t, vector(4, 5)) - assert.are_same({1, 2, 4}, t) - end) - - end) - - describe('printh', function () - - -- 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 () - - local native_print_stub - - setup(function () - native_print_stub = stub(_G, "print") -- native print - end) - - teardown(function () - native_print_stub:revert() - 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) - - 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 - - -- 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 - -- 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 - lfs.rmdir(dirpath) - end - - 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 - - local function get_lines(file) - local lines = {} - for line in file:lines() do - add(lines, line) - end - return lines - end - - before_each(function () - remove_if_exists(temp_dirname) - 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 - -- this is never called in working tests - 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) - - end) - - end) - -end) diff --git a/src/engine/tests/utestpico8wtk.lua b/src/engine/tests/utestpico8wtk.lua deleted file mode 100644 index 449e8a20..00000000 --- a/src/engine/tests/utestpico8wtk.lua +++ /dev/null @@ -1,277 +0,0 @@ -require("engine/test/bustedhelper") -local wtk = require("engine/wtk/pico8wtk") - -describe('wtk', function () - - describe('vertical_layout', function () - - describe('new', function () - - it('should create a new vertical layout', function () - local vl = wtk.vertical_layout.new(10, 3) - assert.is_not_nil(vl) - assert.are_equal(10, vl.w) - assert.are_equal(0, vl.h) - assert.are_equal(3, vl.c) - end) - - end) - - describe('add_child', function () - - local vl - - before_each(function () - vl = wtk.vertical_layout.new(10, 3) - end) - - describe('(when no children)', function () - - it('should add a child at the origin ', function () - local label = wtk.label.new("hello", 4) -- width: 19, height: 5 - vl:add_child(label) - - assert.are_equal(1, #vl.children) - assert.are_equal(label, vl.children[1]) - assert.are_same({0, 0}, {vl.children[1].x, vl.children[1].y}) - end) - - it('should expand size to the max width and by child height', function () - local label = wtk.label.new("hello", 4) -- width: 19, height: 5 - vl:add_child(label) - - assert.are_equal(19, vl.w) - assert.are_equal(5, vl.h) - end) - - end) - - describe('(when some children)', function () - - before_each(function () - local label = wtk.label.new("hello", 4) -- width: 19, height: 5 - vl:add_child(label) - end) - - it('should add a child under the previous one', function () - local label2 = wtk.label.new("hello again", 4) -- width: 43, height: 5 - vl:add_child(label2) - - assert.are_equal(2, #vl.children) - assert.are_equal(label2, vl.children[2]) - assert.are_same({0, 6}, {vl.children[2].x, vl.children[2].y}) - end) - - it('should expand size to the max width and by vertical padding + child height', function () - local label2 = wtk.label.new("hello again", 4) -- width: 43, height: 5 - vl:add_child(label2) - - assert.are_equal(43, vl.w) - assert.are_equal(11, vl.h) - end) - - it('should adapt to new lines via label height', function () - local label2 = wtk.label.new("hello\nagain", 4) -- width: 19, height: 11 - vl:add_child(label2) - local label3 = wtk.label.new("more\nlines", 4) -- width: 19, height: 11 - vl:add_child(label3) - - assert.are_equal(3, #vl.children) - assert.are_equal(label2, vl.children[2]) - assert.are_same({0, 6}, {vl.children[2].x, vl.children[2].y}) - assert.are_equal(label3, vl.children[3]) - assert.are_same({0, 18}, {vl.children[3].x, vl.children[3].y}) - - assert.are_equal(19, vl.w) - assert.are_equal(29, vl.h) - end) - - end) - - end) - - describe('remove_child', function () - - local vl - local widget_remove_child_stub - - setup(function () - widget_remove_child_stub = stub(wtk.widget, "remove_child") - end) - - teardown(function () - widget_remove_child_stub:revert() - end) - - before_each(function () - vl = wtk.vertical_layout.new(10, 3, 2) -- padding of 2 - end) - - after_each(function () - widget_remove_child_stub:clear() - end) - - it('should call base method implementation', function () - local some_icon = wtk.icon.new(2, 3) - vl:remove_child(some_icon) - assert.spy(widget_remove_child_stub).was_called(1) - assert.spy(widget_remove_child_stub).was_called_with(vl, some_icon) - end) - - it('should do nothing when trying to remove a non-child element (except unsetting its parent due to base method implementation)', function () - local icon_not_here = wtk.icon.new(2, 3) - assert.has_no_errors(function () - vl:remove_child(icon_not_here) - end) - end) - - describe('(when some children)', function () - - local label1, label2, label3, icon1, icon2 - - before_each(function () - icon1 = wtk.icon.new(2, 3) -- height: 8 - icon2 = wtk.icon.new(3, 4) -- height: 8 - label1 = wtk.label.new("hello1", 4) -- height: 5 - label2 = wtk.label.new("hello2", 4) -- height: 5 - vl:add_child(icon1) -- y: 0 - vl:add_child(icon2) -- y: 9 - vl:add_child(label1) -- y: 15 - vl:add_child(label2) -- y: 21 - end) - - it('should move all children below that child up', function () - assert.are_same( - {0, 10, 20, 27}, - {icon1.y, icon2.y, label1.y, label2.y}) - vl:remove_child(icon2) - assert.are_same( - {0, 10, 17}, - {icon1.y, label1.y, label2.y}) - end) - - end) - - end) - - describe('draw', function () - - local rectfill_stub - - setup(function () - rectfill_stub = stub(_G, "rectfill") - end) - - teardown(function () - rectfill_stub:revert() - end) - - it('should call rectfill', function () - local vl = wtk.vertical_layout.new(10, 3) - vl:draw(5, 8) - assert.spy(rectfill_stub).was_called() - assert.spy(rectfill_stub).was_called_with(5, 8, 14, 7, 3) - end) - - end) - - end) - - describe('label', function () - - describe('new', function () - - it('should create a new label', function () - local label = wtk.label.new("fixed", 4) - assert.are_equal(wtk.label, getmetatable(label)) - end) - it('should create a new label with fixed text', function () - local label = wtk.label.new("fixed", 4) - assert.are_equal("fixed", label.text) - end) - it('should create a new label with fixed text from concatenable type (at least on the right)', function () - local concatenable = {} - setmetatable(concatenable, {__concat = function (lhs, rhs) - return lhs.."100" - end}) - local label = wtk.label.new(concatenable, 4) - assert.are_equal("100", label.text) - end) - it('should create a new label with dynamic text method', function () - local text_callback = function () return "fixed dynamic text" end - local label = wtk.label.new(text_callback, 4) - assert.are_equal("fixed dynamic text", label.text()) - end) - it('should create a new label with dynamic text method using self (not recommended because object it not fully created yet)', function () - local text_callback = function (self) return tostr(self.c) end - local label = wtk.label.new(text_callback, 4) - assert.are_equal("4", label:text()) - end) - it('should create a new label with color 0 by default', function () - local label = wtk.label.new("fixed") - assert.are_equal(0, label.c) - end) - it('should create a new label with color', function () - local label = wtk.label.new("fixed", 4) - assert.are_equal(4, label.c) - end) - it('should create a new label with optional function that wants mouse', function () - local func = function () end - local label = wtk.label.new("fixed", 4, func) - assert.are_same({true, func}, {label.wants_mouse, label.func}) - end) - it('should create a new label with width based on text', function () - local label = wtk.label.new("12345", 4) - assert.are_same(19, label.w) - end) - it('should create a new label with height based on text', function () - local label = wtk.label.new("12345", 4) - assert.are_same(5, label.h) - end) - it('should create a new label with width based on dynamic text method', function () - local text_callback = function (self) return "123"..tostr(self.c) end - local label = wtk.label.new(text_callback, 4) - assert.are_same(15, label.w) - end) - it('should create a new label with height based on dynamic text method', function () - local text_callback = function (self) return "123"..tostr(self.c) end - local label = wtk.label.new(text_callback, 4) - assert.are_same(5, label.h) - end) - it('should create a new label with width based on multiline text', function () - local label = wtk.label.new("shorter\nvery long string\nshort", 4) - assert.are_same(63, label.w) - end) - it('should create a new label with height based on multiline text', function () - local label = wtk.label.new("shorter\nvery long string\nshort", 4) - assert.are_same(17, label.h) - end) - - end) - - describe('compute_size', function () - - it('"" => 0, 5', function () - assert.are_same({0, 5}, {wtk.label.compute_size("")}) - end) - it('"hello" => 19, 5', function () - assert.are_same({19, 5}, {wtk.label.compute_size("hello")}) - end) - it('"hello\n" => 19, 11', function () - assert.are_same({19, 11}, {wtk.label.compute_size("hello\n")}) - end) - it('"hello\nworld" => 23, 11', function () - assert.are_same({23, 11}, {wtk.label.compute_size("short\nlonger")}) - end) - it('"hello\nworld" => 23, 11', function () - assert.are_same({23, 11}, {wtk.label.compute_size("longer\nshort")}) - end) - it('"\n\n\n" => 0, 23', function () - assert.are_same({0, 23}, {wtk.label.compute_size("\n\n\n")}) - end) - - end) - - end) - -end) diff --git a/src/engine/tests/utestprofiler.lua b/src/engine/tests/utestprofiler.lua deleted file mode 100644 index 7ed983c8..00000000 --- a/src/engine/tests/utestprofiler.lua +++ /dev/null @@ -1,35 +0,0 @@ -require("engine/test/bustedhelper") -local profiler = require("engine/debug/profiler") - -describe('profiler', function () - - setup(function () - pico8.memory_usage = 152 - end) - - describe('get_stat_function', function () - - it('should return a function that returns a stat name padded', function () - local mem_stat_function = profiler.get_stat_function(1) - assert.are_equal("memory 152", mem_stat_function()) - end) - - end) - - describe('window', function () - - it('should initialize the profiler, invisible, with stat labels and correct callbacks', function () - local add_label_global_stub = stub(profiler.window, "add_label") - profiler.window:init() -- was already called, but recall it to spy this time - assert.spy(add_label_global_stub).was_called(6) - assert.spy(add_label_global_stub).was_called_with(match.ref(profiler.window), profiler.stat_functions[1], colors.light_gray, 1, 1) - assert.spy(add_label_global_stub).was_called_with(match.ref(profiler.window), profiler.stat_functions[2], colors.light_gray, 1, 7) - assert.spy(add_label_global_stub).was_called_with(match.ref(profiler.window), profiler.stat_functions[3], colors.light_gray, 1, 13) - assert.spy(add_label_global_stub).was_called_with(match.ref(profiler.window), profiler.stat_functions[4], colors.light_gray, 1, 19) - assert.spy(add_label_global_stub).was_called_with(match.ref(profiler.window), profiler.stat_functions[5], colors.light_gray, 1, 25) - assert.spy(add_label_global_stub).was_called_with(match.ref(profiler.window), profiler.stat_functions[6], colors.light_gray, 1, 31) - end) - - end) - -end) diff --git a/src/engine/tests/utestsprite_data.lua b/src/engine/tests/utestsprite_data.lua deleted file mode 100644 index 7bf3e61b..00000000 --- a/src/engine/tests/utestsprite_data.lua +++ /dev/null @@ -1,101 +0,0 @@ -require("engine/test/bustedhelper") -local sprite_data = require("engine/render/sprite_data") - -describe('sprite', function () - - describe('_init', function () - it('should init a sprite with an id_loc', function () - local spr_data = sprite_data(sprite_id_location(1, 3)) - assert.are_equal(sprite_id_location(1, 3), spr_data.id_loc) - end) - it('should init a sprite with the passed span', function () - local spr_data = sprite_data(sprite_id_location(1, 3), tile_vector(4, 5)) - assert.are_equal(tile_vector(4, 5), spr_data.span) - end) - it('should init a sprite with a span of (1, 1) by default', function () - local spr_data = sprite_data(sprite_id_location(1, 3)) - assert.are_equal(tile_vector(1, 1), spr_data.span) - end) - it('should init a sprite with the passed pivot', function () - local spr_data = sprite_data(sprite_id_location(1, 3), nil, vector(2, 4)) - assert.are_equal(vector(2, 4), spr_data.pivot) - end) - it('should init a sprite with a pivot of (0, 0) by default', function () - local spr_data = sprite_data(sprite_id_location(1, 3), nil) - assert.are_equal(vector.zero(), spr_data.pivot) - end) - it('should init a sprite with the correct values', function () - local spr_data = sprite_data(sprite_id_location(1, 3), tile_vector(2, 3), vector(2, 4)) - assert.are_same({sprite_id_location(1, 3), tile_vector(2, 3), vector(2, 4)}, {spr_data.id_loc, spr_data.span, spr_data.pivot}) - end) - end) - - describe('_tostring', function () - - it('sprite_data((1, 3) ...) => "sprite_data(sprite_id_location(1, 3) ...)"', function () - local spr_data = sprite_data(sprite_id_location(1, 3), tile_vector(2, 3), vector(2, 4)) - assert.are_equal("sprite_data(sprite_id_location(1, 3), tile_vector(2, 3), vector(2, 4))", spr_data:_tostring()) - end) - - end) - - describe('render', function () - - 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 () - spr_stub = stub(_G, "spr") - end) - - teardown(function () - spr_stub:revert() - end) - - after_each(function () - spr_stub:clear() - end) - - 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) - -- 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) - -end) diff --git a/src/engine/tests/utesttilemap.lua b/src/engine/tests/utesttilemap.lua deleted file mode 100644 index 62477ce4..00000000 --- a/src/engine/tests/utesttilemap.lua +++ /dev/null @@ -1,46 +0,0 @@ -require("engine/test/bustedhelper") -local tilemap = require("engine/data/tilemap") - -describe('tilemap', function () - - describe('_init', function () - it('should create a new tilemap with content', function () - local tm = tilemap({{1, 2, 3}, {4, 5, 6}}) - assert.is_not_nil(tm) - assert.are_same({{1, 2, 3}, {4, 5, 6}}, tm.content) - end) - end) - - describe('load', function () - it('should reset the current map to tile ids stored in content', function () - -- initial dirty map to clean - mset(0, 0, 50) - local tm = tilemap({{1, 2, 3}, {4, 5, 6}}) - tm:load() - assert.are_same({1, 2, 3, 4, 5, 6}, - {mget(0, 0), mget(1, 0), mget(2, 0), mget(0, 1), mget(1, 1), mget(2, 1)}) - 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/src/engine/tests/utestui.lua b/src/engine/tests/utestui.lua deleted file mode 100644 index 297b8aa9..00000000 --- a/src/engine/tests/utestui.lua +++ /dev/null @@ -1,273 +0,0 @@ -require("engine/test/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 - -describe('ui', function () - - describe('label', function () - - describe('_init', function () - - it('should init label with layer', function () - local lab = label("great", vector(24, 68), colors.red) - assert.are_same({"great", vector(24, 68), colors.red}, {lab.text, lab.position, lab.colour}) - end) - - end) - - describe('_tostring', function () - - it('should return "label(\'[text]\' @ [position] in [colour])"', function () - assert.are_equal("label('good' @ vector(22, 62) in yellow)", label("good", vector(22, 62), colors.yellow):_tostring()) - end) - - end) - - end) - - describe('overlay', function () - - describe('_init', function () - - it('should init overlay with layer', function () - assert.are_equal(6, overlay(6).layer) - end) - - end) - - describe('_tostring', function () - - it('should return "overlay(layer [layer])"', function () - assert.are_equal("overlay(layer: 8)", overlay(8):_tostring()) - end) - - end) - - describe('(overlay instance)', function () - - local overlay_instance - - setup(function () - overlay_instance = overlay(4) - end) - - describe('(no labels)', function () - - teardown(function () - clear_table(overlay_instance.labels) - end) - - describe('add_label', function () - - local warn_stub - - setup(function () - warn_stub = stub(_G, "warn") - end) - - teardown(function () - warn_stub:revert() - end) - - after_each(function () - warn_stub:clear() - end) - - it('should add a new label', function () - overlay_instance:add_label("test", "content", vector(2, 4), colors.red) - assert.are_equal(label("content", vector(2, 4), colors.red), overlay_instance.labels["test"]) - end) - - it('should add a new black label with warning if no colour is passed', function () - overlay_instance:add_label("test", "content", vector(2, 4)) - assert.spy(warn_stub).was_called(1) - assert.spy(warn_stub).was_called_with('overlay:add_label no colour passed, will default to black (0)', 'ui') - assert.are_equal(label("content", vector(2, 4), colors.black), overlay_instance.labels["test"]) - end) - - end) - - end) - - describe('(label "mock" and "mock2" added)', function () - - before_each(function () - overlay_instance:add_label("mock", "mock content", vector(1, 1), colors.blue) - overlay_instance:add_label("mock2", "mock content 2", vector(2, 2), colors.dark_purple) - end) - - after_each(function () - clear_table(overlay_instance.labels) - end) - - describe('add_label', function () - - it('should replace an existing label', function () - overlay_instance:add_label("mock", "mock content 2", vector(3, 7), colors.white) - assert.are_equal(label("mock content 2", vector(3, 7), colors.white), overlay_instance.labels["mock"]) - end) - - end) - - describe('remove_label', function () - - local warn_stub - - setup(function () - warn_stub = stub(_G, "warn") - end) - - teardown(function () - warn_stub:revert() - end) - - after_each(function () - warn_stub:clear() - end) - - it('should remove an existing label', function () - overlay_instance:remove_label("mock") - assert.is_nil(overlay_instance.labels["mock"]) - end) - - it('should warn if the label name is not found', function () - overlay_instance:remove_label("test") - assert.spy(warn_stub).was_called(1) - assert.spy(warn_stub).was_called_with('overlay:remove_label: could not find label with name: \'test\'', 'ui') - assert.is_nil(overlay_instance.labels["test"]) - end) - - end) - - describe('clear_labels', function () - - it('should clear any existing label', function () - overlay_instance:clear_labels() - return is_empty(overlay_instance.labels) - end) - - end) - - describe('draw_labels', function () - - local api_print_stub - - setup(function () - api_print_stub = stub(api, "print") - end) - - teardown(function () - api_print_stub:revert() - end) - - after_each(function () - api_print_stub:clear() - end) - - it('should call print', function () - overlay_instance:draw_labels() - assert.spy(api_print_stub).was_called(2) - assert.spy(api_print_stub).was_called_with("mock content", 1, 1, colors.blue) - assert.spy(api_print_stub).was_called_with("mock content 2", 2, 2, colors.dark_purple) - end) - - end) - - end) -- (label "mock" and "mock2" added) - - end) -- (overlay instance) - - end) -- overlay - - describe('render_mouse', function () - - describe('(without cursor sprite data)', function () - - describe('(mouse off)', function () - - it('should not error (by testing for nil)', function () - assert.has_no_errors(function () ui:render_mouse() end) - end) - - end) - - describe('(mouse on at (12, 48))', function () - - setup(function () - input:toggle_mouse(true) - pico8.mousepos.x = 12 - pico8.mousepos.y = 48 - end) - - teardown(function () - input:toggle_mouse(false) - end) - - it('should not error (by testing for nil)', function () - assert.has_no_errors(function () ui:render_mouse() end) - end) - - end) - - end) -- (without cursor sprite data) - - describe('(with cursor sprite data)', function () - - local cursor_render_stub - - setup(function () - ui:set_cursor_sprite_data(sprite_data(sprite_id_location(1, 0))) - cursor_render_stub = stub(ui.cursor_sprite_data, "render") - end) - - teardown(function () - cursor_render_stub:revert() - end) - - after_each(function () - cursor_render_stub:clear() - end) - - describe('(mouse off)', function () - - it('should not call cursor sprite render', function () - ui:render_mouse() - assert.spy(cursor_render_stub).was_not_called() - end) - - end) - - describe('(mouse shown at (12, 48))', function () - - setup(function () - input:toggle_mouse(true) - pico8.mousepos.x = 12 - pico8.mousepos.y = 48 - end) - - teardown(function () - input:toggle_mouse(false) - end) - - -- bugfix history: - -- .. i forgot to use match.ref, which was ok until struct_eq uses are_same with compare_raw_content: true - -- which causes infinite recursion when trying to compare a spied method on a struct (as it contains a ref to itself) - it('should call cursor sprite render at (12, 48)', function () - ui:render_mouse() - assert.are_same({0, 0}, {pico8.camera_x, pico8.camera_y}) - assert.spy(cursor_render_stub).was_called(1) - assert.spy(cursor_render_stub).was_called_with(match.ref(ui.cursor_sprite_data), vector(12, 48)) - end) - - end) - - end) -- (with cursor sprite data) - - end) -- ui.render_mouse - -end) diff --git a/src/engine/tests/utestunittest.lua b/src/engine/tests/utestunittest.lua deleted file mode 100644 index 2346af7c..00000000 --- a/src/engine/tests/utestunittest.lua +++ /dev/null @@ -1,81 +0,0 @@ -require("engine/test/bustedhelper") -local unittest = require("engine/test/unittest") -local utest_manager, unit_test, time_trigger = unittest.utest_manager, unittest.unit_test - -describe('utest_manager', function () - - after_each(function () - utest_manager:init() - end) - - describe('init', function () - - it('should initialize utest_manager with no utests', function () - assert.are_same({}, utest_manager.utests) - end) - - end) - - describe('register', function () - - it('should register a new utest', function () - local utest = unit_test("test name", function () end) - utest_manager:register(utest) - assert.are_equal(1, #utest_manager.utests) - assert.are_equal(utest, utest_manager.utests[1]) - end) - - end) - - describe('run_all_tests', function () - - it('should run all the registered utests', function () - local spy1 = spy.new(function () end) - local spy2 = spy.new(function () end) - utest_manager.utests = { - unit_test("test 1", spy1), - unit_test("test 2", spy2) - } - utest_manager:run_all_tests() - assert.spy(spy1).was_called(1) - assert.spy(spy2).was_called(1) - end) - - end) - -end) - -describe('unit_test', function () - - describe('_init', function () - - it('should init a unit test with a name and callback', function () - local callback = function () end - local utest = unit_test("test name", callback) - assert.are_same({"test name", callback}, {utest.name, utest.callback}) - end) - - end) - -end) - -describe('check', function () - - local register_stub - - setup(function () - register_stub = stub(utest_manager, "register") - end) - - teardown(function () - register_stub:revert() - end) - - it('should call utest_manager:register on a new test with passed name and callback', function () - local callback = function () end - check("test name", callback) - assert.spy(register_stub).was_called(1) - assert.spy(register_stub).was_called_with(match.ref(utest_manager), unit_test("test name", callback)) - end) - -end) diff --git a/src/engine/tests/utestunittest_helper.lua b/src/engine/tests/utestunittest_helper.lua deleted file mode 100644 index ba6b93fe..00000000 --- a/src/engine/tests/utestunittest_helper.lua +++ /dev/null @@ -1,25 +0,0 @@ -require("engine/test/bustedhelper") -require("engine/test/unittest_helper") - -describe('unittest_helper', function () - - describe('are_same_with_message', function () - it('should return (true, "Expected...") when the values are the same', function () - local expected_message = "Expected objects to not be the same (compare_raw_content: false).\nPassed in:\n{[1] = 1, [2] = 2, [3] = 3}\nDid not expect:\n{[1] = 1, [2] = 2, [3] = 3}" - assert.are_same({true, expected_message}, {are_same_with_message({1, 2, 3}, {1, 2, 3})}) - end) - it('should return (false, "Expected...") when the values are not the same', function () - local expected_message = "Expected objects to be the same (compare_raw_content: false).\nPassed in:\n{[1] = 1, [2] = 3, [3] = 2}\nExpected:\n{[1] = 1, [2] = 2, [3] = 3}" - assert.are_same({false, expected_message}, {are_same_with_message({1, 2, 3}, {1, 3, 2})}) - end) - it('should return (true, "Expected...") when the values are the same', function () - local expected_message = "Expected objects to not be the same (compare_raw_content: true).\nPassed in:\n{[1] = 1, [2] = 2, [3] = 3}\nDid not expect:\n{[1] = 1, [2] = 2, [3] = 3}" - assert.are_same({true, expected_message}, {are_same_with_message({1, 2, 3}, {1, 2, 3}, true)}) - end) - it('should return (false, "Expected...") when the values are not the same', function () - local expected_message = "Expected objects to be the same (compare_raw_content: true).\nPassed in:\n{[1] = 1, [2] = 3, [3] = 2}\nExpected:\n{[1] = 1, [2] = 2, [3] = 3}" - assert.are_same({false, expected_message}, {are_same_with_message({1, 2, 3}, {1, 3, 2}, true)}) - end) - end) - -end) diff --git a/src/engine/tests/utestvisual_logger.lua b/src/engine/tests/utestvisual_logger.lua deleted file mode 100644 index 08d91832..00000000 --- a/src/engine/tests/utestvisual_logger.lua +++ /dev/null @@ -1,204 +0,0 @@ -require("engine/test/bustedhelper") -local vlogger = require("engine/debug/visual_logger") -local logging = require("engine/debug/logging") -local wtk = require("engine/wtk/pico8wtk") - -describe('vlogger', function () - - local log_msg = logging.log_msg - local logger = logging.logger - local window = vlogger.window - - describe('window (with buffer size 3)', function () - - local old_buffer_size - - setup(function () - -- to make sure that we control buffer size in test code, - -- we set it here and reinit the visual logger window - old_buffer_size = vlogger.buffer_size - vlogger.buffer_size = 3 - window:init() - end) - - teardown(function () - vlogger.buffer_size = old_buffer_size - window:init() - end) - - after_each(function () - window:init() - end) - - describe('init', function () - - it('should initialize a message queue of size 3', function () - -- implementation - assert.are_equal(3, window._msg_queue.max_length) - assert.are_equal(0, #window._msg_queue) - end) - - it('should create a vertical layout to put the messages in', function () - assert.are_equal(1, #window.gui.children) - assert.are_equal(wtk.vertical_layout, getmetatable(window.gui.children[1])) - end) - end) - - describe('push_msg', function () - - local msg_queue - - setup(function () - spy.on(window, "_on_msg_pushed") - spy.on(window, "_on_msg_popped") - end) - - teardown(function () - window._on_msg_pushed:revert() - window._on_msg_popped:revert() - end) - - before_each(function () - -- we don't clear the _message_queue but rather reconstruct it on each test in init() - -- therefore a new instance is created each time and we need to respy that new instance - msg_queue = window._msg_queue - spy.on(msg_queue, "push") - end) - - after_each(function () - msg_queue.push:revert() -- just in case - end) - - describe('(when queue is empty)', function () - - it('should push a message to queue and vertical layout', function () - local lm = log_msg(logging.level.info, "flow", "enter stage state") - window:push_msg(lm) - assert.spy(msg_queue.push).was_called(1) - assert.spy(msg_queue.push).was_called_with(match.ref(window._msg_queue), lm) - assert.spy(window._on_msg_pushed).was_called(1) - assert.spy(window._on_msg_pushed).was_called_with(match.ref(window), lm) - assert.spy(window._on_msg_popped).was_not_called() - end) - - end) - - describe('(when queue has 2 entries (not full))', function () - - before_each(function () - window:push_msg(log_msg(logging.level.info, "flow", "enter stage state")) - window:push_msg(log_msg(logging.level.warning, "player", "player character spawner")) - msg_queue.push:clear() - window._on_msg_pushed:clear() - window._on_msg_popped:clear() - end) - - it('should push a message to queue and vertical layout', function () - local lm = log_msg(logging.level.warning, "default", "danger") - window:push_msg(lm) - - assert.spy(msg_queue.push).was_called(1) - assert.spy(msg_queue.push).was_called_with(match.ref(msg_queue), lm) - assert.spy(window._on_msg_pushed).was_called(1) - assert.spy(window._on_msg_pushed).was_called_with(match.ref(window), lm) - assert.spy(window._on_msg_popped).was_not_called() - end) - - end) - - describe('(when queue has 3 entries (full))', function () - - before_each(function () - for i = 1, vlogger.buffer_size do - window:push_msg(log_msg(logging.level.info, "flow", "enter stage state")) - end - msg_queue.push:clear() - window._on_msg_pushed:clear() - window._on_msg_popped:clear() - end) - - it('should push a message to queue and vertical layout, detect overwriting and pop the oldest label', function () - local lm = log_msg(logging.level.warning, "default", "danger") - window:push_msg(lm) - - assert.spy(msg_queue.push).was_called(1) - assert.spy(msg_queue.push).was_called_with(match.ref(msg_queue), lm) - assert.spy(window._on_msg_pushed).was_called(1) - assert.spy(window._on_msg_pushed).was_called_with(match.ref(window), lm) - assert.spy(window._on_msg_popped).was_called(1) - assert.spy(window._on_msg_popped).was_called_with(match.ref(window)) - end) - - end) - - end) - - describe('_on_msg_pushed', function () - - local add_child_stub = stub(window.v_layout, "add_child") - - setup(function () - add_child_stub = stub(window.v_layout, "add_child") - end) - - teardown(function () - add_child_stub:revert() - end) - - it('should call add_child with a white label({msg})', function () - window:_on_msg_pushed(log_msg(logging.level.info, "flow", "enter stage state")) - - local log_label = wtk.label.new("enter stage state", colors.white) - assert.spy(add_child_stub).was_called(1) - assert.spy(add_child_stub).was_called_with(match.ref(window.v_layout), log_label) - end) - - end) - - describe('_on_msg_popped', function () - - local remove_child_stub - - setup(function () - -- add a message to avoid assertion in _on_msg_popped - window:_on_msg_pushed(log_msg(logging.level.info, "flow", "enter stage state")) - - remove_child_stub = stub(window.v_layout, "remove_child") - end) - - teardown(function () - remove_child_stub:revert() - end) - - it('should call remove_child on the first child', function () - window:_on_msg_popped() - assert.spy(remove_child_stub).was_called(1) - assert.spy(remove_child_stub).was_called_with(match.ref(window.v_layout), window.v_layout.children[1]) - end) - - end) - - end) - - describe('vlog_stream', function () - - local push_msg_stub - - setup(function () - push_msg_stub = stub(window, "push_msg") - end) - - teardown(function () - push_msg_stub:revert() - end) - - it('should call window.push_msg', function () - local lm = log_msg(logging.level.info, "flow", "enter stage state") - vlogger.vlog_stream:on_log(lm) - assert.spy(push_msg_stub).was_called(1) - assert.spy(push_msg_stub).was_called_with(match.ref(window), lm) - end) - - end) - -end) diff --git a/src/engine/ui/ui.lua b/src/engine/ui/ui.lua deleted file mode 100644 index 0abac02f..00000000 --- a/src/engine/ui/ui.lua +++ /dev/null @@ -1,116 +0,0 @@ ---#if log -local logging = require("engine/debug/logging") ---#endif - -local input = require("engine/input/input") - -local ui = { - cursor_sprite_data = nil -} - --- setup - ---#if mouse - --- injection function: call it from game to set the sprite data --- for the mouse cursor. this avoids accessing game data --- from an engine script -function ui:set_cursor_sprite_data(cursor_sprite_data) - self.cursor_sprite_data = cursor_sprite_data -end - --- helper functions - -function ui:render_mouse() - if input.mouse_active and self.cursor_sprite_data then - camera(0, 0) - local cursor_position = input.get_cursor_position() - self.cursor_sprite_data:render(cursor_position) - end -end - ---#endif - --- label struct: container for a text to draw at a given position -local label = new_struct() - --- text printable text content to draw (mainly string or number) --- position vector position to draw the label at --- colour int color index to draw the label with -function label:_init(text, position, colour) - self.text = text - self.position = position - self.colour = colour -end - ---#if log -function label:_tostring() - return "label('"..self.text.."' @ "..self.position.." in "..color_tostring(self.colour)..")" -end ---#endif - --- overlay class: allows to draw labels on top of the screen -local overlay = new_class() - --- parameters --- layer int level at which the overlay should be drawn, higher on top --- state vars --- labels {string: label} table of labels to draw, identified by name -function overlay:_init(layer) - self.layer = layer - self.labels = {} -end - ---#if log -function overlay:_tostring() - return "overlay(layer: "..self.layer..")" -end ---#endif - --- add a label identified by a name, containing a text string, --- at a position vector, with a given color --- if a label with the same name already exists, replace it -function overlay:add_label(name, text, position, colour) - if not colour then - colour = colors.black - warn("overlay:add_label no colour passed, will default to black (0)", "ui") - end - if self.labels[name] == nil then - -- create new label and add it - self.labels[name] = label(text, position, colour) - else - -- set existing label properties - local label = self.labels[name] - label.text = text - label.position = position - label.colour = colour - end -end - --- remove a label identified by a name --- if the label is not found, fails with warning -function overlay:remove_label(name, text, position) - if self.labels[name] ~= nil then - self.labels[name] = nil - else - warn("overlay:remove_label: could not find label with name: '"..name.."'", "ui") - end -end - --- remove all the labels -function overlay:clear_labels() - clear_table(self.labels) -end - --- draw all labels in the overlay. order is not guaranteed -function overlay:draw_labels() - for name, label in pairs(self.labels) do - api.print(label.text, label.position.x, label.position.y, label.colour) - end -end - - --- export -ui.label = label -ui.overlay = overlay -return ui diff --git a/src/engine/wtk b/src/engine/wtk deleted file mode 160000 index a7a3adec..00000000 --- a/src/engine/wtk +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a7a3adec106cdd459f5aa2887ab47d7388001348 diff --git a/src/game/application/dummy_gamestate.lua b/src/game/application/dummy_gamestate.lua deleted file mode 100644 index 5ac8cf6f..00000000 --- a/src/game/application/dummy_gamestate.lua +++ /dev/null @@ -1,27 +0,0 @@ -require("engine/core/class") -require("engine/render/color") - --- class for dummy gamestates --- you don't need to derive it, just create an instance of dummy_gamestate --- passing the appropriate type to create one -local dummy_gamestate = new_class() - -function dummy_gamestate:_init(type) - self.type = type -end - -function dummy_gamestate:on_enter() -end - -function dummy_gamestate:on_exit() -end - -function dummy_gamestate:update() -end - -function dummy_gamestate:render() - color(colors.white) - api.print(self.type.." state", 4*11, 6*12) -end - -return dummy_gamestate diff --git a/src/game/application/gamestate.lua b/src/game/application/gamestate.lua deleted file mode 100644 index 5315e8c3..00000000 --- a/src/game/application/gamestate.lua +++ /dev/null @@ -1,16 +0,0 @@ -local gamestate = {} - -gamestate.types = { - titlemenu = 'titlemenu', - credits = 'credits', - stage = 'stage', -} - --- abstract gamestate singleton --- 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 - -return gamestate diff --git a/src/game/application/gamestate_proxy.lua b/src/game/application/gamestate_proxy.lua deleted file mode 100644 index ef294dc3..00000000 --- a/src/game/application/gamestate_proxy.lua +++ /dev/null @@ -1,61 +0,0 @@ -require("engine/core/class") - --- an intermediate module that breaks dependencies by providing the wanted --- version of each gamestate for the current build --- each module is a lua gamestate module, either the authentic or a dummy one -local gamestate_proxy = singleton(function (self) - self._state_modules = {} - self._state_modules.titlemenu = nil - self._state_modules.credits = nil - self._state_modules.stage = nil -end) - --- load a particular version of the gamestate: standard or dummy --- in pico8 builds, pass nothing, as the preprocess step will determine what is required --- in busted tests, pass the list of gamestates to use, by name (e.g. {"titlemenu", "credits"}) -function gamestate_proxy:require_gamestates(active_gamestates) - ---[[#pico8 - self._state_modules.titlemenu = require("game/menu/titlemenu$titlemenu_ver") - self._state_modules.credits = require("game/menu/credits$credits_ver") - self._state_modules.stage = require("game/ingame/stage$stage_ver") ---#pico8]] - ---#ifn pico8 - require("engine/test/assertions") -- for "contains" - - -- busted runs directly on the scripts, so there is no need to preprocess - -- to exclude unused gamestates and require minimal files as in the built .p8 - -- instead, we need to require_gamestates with the list of active gamestates - -- for pico8 versions, active_gamestates can be nil, it won't be used anyway - local dirs = { - titlemenu = "menu", - credits = "menu", - stage = "ingame" - } - - local versions = {} - for gamestate in all({"titlemenu", "credits", "stage"}) do - if contains(active_gamestates, gamestate) then - version_suffix = "" - else - version_suffix = "_dummy" - end - self._state_modules[gamestate] = require("game/"..dirs[gamestate].."/"..gamestate..version_suffix) - end ---#endif - -end - --- return the gamestate with given name to use for that build --- normally, this is the gamestate in the module of the same name, --- but in minimal builds, unused gamestates are replaced --- 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(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 - -return gamestate_proxy diff --git a/src/game/ingame/stage_dummy.lua b/src/game/ingame/stage_dummy.lua deleted file mode 100644 index 028c860f..00000000 --- a/src/game/ingame/stage_dummy.lua +++ /dev/null @@ -1,8 +0,0 @@ -local dummy_gamestate = require("game/application/dummy_gamestate") -local gamestate = require("game/application/gamestate") - -local dummy_stage = {} - -dummy_stage.state = dummy_gamestate(gamestate.types.stage) - -return dummy_stage diff --git a/src/game/itest_main.lua b/src/game/itest_main.lua deleted file mode 100644 index d0357e79..00000000 --- a/src/game/itest_main.lua +++ /dev/null @@ -1,89 +0,0 @@ --- main source file for all itests, used to run itests in pico8 --- each itest should be put inside the tests/itests folder with the name itest{module}.lua --- and its first line should be "-- gamestates: state1, state2, ..." with the list of states --- to use for the build. other states will be replaced with dummy equivalents. - -require("engine/test/integrationtest") -local picosonic_app = require("game/application/picosonic_app") -require("game/itests/itest$itest") -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) ---#endif - -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 - - itest_runner.app = picosonic_app() - - -- require only gamestate modules written on first line of the required $itest (pico8-build way) - gamestate_proxy:require_gamestates() - - -- start first itest - init_game_and_start_next_itest() -end - -function _update60() - handle_input() - itest_runner:update_game_and_test() -end - -function _draw() - itest_runner:draw_game_and_test() -end - -function init_game_and_start_next_itest() - 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 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() - end - end -end diff --git a/src/game/itests/itestvisual_logger.lua b/src/game/itests/itestvisual_logger.lua deleted file mode 100644 index b90e3b2e..00000000 --- a/src/game/itests/itestvisual_logger.lua +++ /dev/null @@ -1,59 +0,0 @@ --- gamestates: titlemenu -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 vlogger = require("engine/debug/visual_logger") -local flow = require("engine/application/flow") -local gamestate = require("game/application/gamestate") - -local itest = integration_test('human test: visual logger is displayed correctly', {gamestate.types.titlemenu}) - -itest.setup = function () - logging.logger:register_stream(logging.console_log_stream) - logging.logger:register_stream(vlogger.vlog_stream) - - -- in case default changes, set it here for precise testing - vlogger.buffer_size = 5 - vlogger.window:init() - - vlogger.window:show() - flow:change_gamestate_by_type(gamestate.types.titlemenu) -end - -local old_console_log_stream_active - -itest:add_action(time_trigger(0.5), function () - old_console_log_stream_active = logging.console_log_stream.active - logging.console_log_stream.active = false -- hide those messages from the console log, we want to test the visual log - log("info message", "itest") -end) -itest:add_action(time_trigger(0.5), function () - log("info message 2", "itest") -end) -itest:add_action(time_trigger(0.5), function () - warn("warning message\none 2 lines", "itest") -end) -itest:add_action(time_trigger(0.5), function () - err("error message 1", "itest") -end) -itest:add_action(time_trigger(0.5), function () - err(wwrap("very long error message", 5), "itest") -end) -itest:add_action(time_trigger(0.7), function () - log("pushing up 1", "itest") -end) -itest:add_action(time_trigger(0.7), function () - log("pushing up 2", "itest") -end) -itest:add_action(time_trigger(0.7), function () - log("pushing up 3", "itest") - logging.console_log_stream.active = old_console_log_stream_active - vlogger.vlog_stream.active = false -- hide visual log again, so we don't print the success message in the vertical layout -end) - --- human check: before "pushing up" messages, add new messages. then pop top messages each time. -itest.final_assertion = function () - return true, "" -end - -itest_manager:register(itest) diff --git a/src/game/main.lua b/src/game/main.lua deleted file mode 100644 index 881b66e3..00000000 --- a/src/game/main.lua +++ /dev/null @@ -1,58 +0,0 @@ -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") -logging.logger:register_stream(logging.console_log_stream) -logging.logger:register_stream(logging.file_log_stream) -logging.logger.active_categories["trace"] = false - ---#if visual_logger -local vlogger = require("engine/debug/visual_logger") -logging.logger:register_stream(vlogger.vlog_stream) -vlogger.window:show() ---#endif - ---#endif - ---#if profiler -local profiler = require("engine/debug/profiler") -profiler.window:show() ---#endif - --- 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 - ---#if mouse -local input = require("engine/input/input") -input:toggle_mouse(true) ---#endif - --- 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 - logging.file_log_stream:clear() ---#endif - - -- require all gamestate modules, according to preprocessing step - gamestate_proxy:require_gamestates() - app.initial_gamestate = gamestate.types.titlemenu - app:start() -end - -function _update60() - app:update() -end - -function _draw() - app:draw() -end diff --git a/src/game/menu/credits.lua b/src/game/menu/credits.lua deleted file mode 100644 index 0efbfbcd..00000000 --- a/src/game/menu/credits.lua +++ /dev/null @@ -1,29 +0,0 @@ -require("engine/application/flow") -require("engine/core/class") -require("engine/render/color") -local gamestate = require("game/application/gamestate") - -local credits = {} - --- game state -credits.state = singleton(function (self) - self.type = gamestate.types.credits -end) - -function credits.state:on_enter() -end - -function credits.state:on_exit() -end - -function credits.state:update() -end - -function credits.state:render() - color(colors.white) - api.print("credits state", 4*11, 6*12) -end - --- export - -return credits diff --git a/src/game/menu/credits_dummy.lua b/src/game/menu/credits_dummy.lua deleted file mode 100644 index 3b5d5fac..00000000 --- a/src/game/menu/credits_dummy.lua +++ /dev/null @@ -1,8 +0,0 @@ -local dummy_gamestate = require("game/application/dummy_gamestate") -local gamestate = require("game/application/gamestate") - -local dummy_credits = {} - -dummy_credits.state = dummy_gamestate(gamestate.types.credits) - -return dummy_credits diff --git a/src/game/menu/titlemenu_dummy.lua b/src/game/menu/titlemenu_dummy.lua deleted file mode 100644 index 71048b24..00000000 --- a/src/game/menu/titlemenu_dummy.lua +++ /dev/null @@ -1,8 +0,0 @@ -local dummy_gamestate = require("game/application/dummy_gamestate") -local gamestate = require("game/application/gamestate") - -local dummy_titlemenu = {} - -dummy_titlemenu.state = dummy_gamestate(gamestate.types.titlemenu) - -return dummy_titlemenu diff --git a/src/game/tests/utestcredits.lua b/src/game/tests/utestcredits.lua deleted file mode 100644 index ae78b66a..00000000 --- a/src/game/tests/utestcredits.lua +++ /dev/null @@ -1,47 +0,0 @@ -require("engine/test/bustedhelper") -local credits = require("game/menu/credits") -local gamestate = require("game/application/gamestate") - -describe('credits.state', function () - - describe('type', function () - it('should be gamestate.types.credits', function () - assert.are_equal(gamestate.types.credits, credits.state.type) - end) - end) - - describe('on_enter', function () - end) - - describe('on_exit', function () - end) - - describe('update', function () - end) - - describe('render', function () - - local api_print_stub - - setup(function () - api_print_stub = stub(api, "print") - end) - - teardown(function () - api_print_stub:revert() - end) - - after_each(function () - api_print_stub:clear() - end) - - it('should print "credits state" in white', function () - credits.state:render() - assert.are_equal(colors.white, pico8.color) - assert.spy(api_print_stub).was_called(1) - assert.spy(api_print_stub).was_called_with("credits state", 4*11, 6*12) - end) - - end) - -end) diff --git a/src/game/tests/utestdummy_gamestate.lua b/src/game/tests/utestdummy_gamestate.lua deleted file mode 100644 index 73f8e83f..00000000 --- a/src/game/tests/utestdummy_gamestate.lua +++ /dev/null @@ -1,66 +0,0 @@ -require("engine/test/bustedhelper") -local dummy_gamestate = require("game/application/dummy_gamestate") - -describe('dummy_gamestate', function () - - describe('_init', function () - it('should create a dummy gamestate with passed type', function () - local dgs = dummy_gamestate("credits") - assert.is_not_nil(dgs) - assert.are_equal("credits", dgs.type) - end) - end) - - describe('on_enter', function () - - it('should do nothing', function () - - assert.has_no_errors(function () - local dgs = dummy_gamestate("credits") - dgs:on_enter() - end) - - end) - - end) - - describe('on_exit', function () - it('should do nothing', function () - assert.has_no_errors(function () - local dgs = dummy_gamestate("credits") - dgs:on_exit() - end) - end) - end) - - describe('update', function () - it('should do nothing', function () - assert.has_no_errors(function () - local dgs = dummy_gamestate("credits") - dgs:update() - end) - end) - end) - - describe('render', function () - - local api_print_stub - - setup(function () - api_print_stub = stub(api, "print") - end) - - teardown(function () - api_print_stub:revert() - end) - - it('should print the gamestate name', function () - local dgs = dummy_gamestate("credits") - dgs:render() - assert.spy(api_print_stub).was_called(1) - assert.spy(api_print_stub).was_called_with("credits state", match.is_number(), match.is_number()) - end) - - end) - -end) diff --git a/src/game/tests/utestgamestate_proxy.lua b/src/game/tests/utestgamestate_proxy.lua deleted file mode 100644 index 354b5bf8..00000000 --- a/src/game/tests/utestgamestate_proxy.lua +++ /dev/null @@ -1,60 +0,0 @@ -require("engine/test/bustedhelper") -local gamestate_proxy = require("game/application/gamestate_proxy") - -describe('gamestate_proxy', function () - - after_each(function () - gamestate_proxy:init() - end) - - describe('require_gamestates', function () - - - it('should require gamestates from the active_gamestates sequence', function () - gamestate_proxy:require_gamestates({"titlemenu", "stage"}) - - -- implementation - assert.are_same({ - titlemenu = require("game/menu/titlemenu"), - credits = require("game/menu/credits_dummy"), - stage = require("game/ingame/stage") - }, - gamestate_proxy._state_modules) - end) - - end) - - describe('get', function () - - it('should assert if module_name is invalid or require_gamestates has not been called (member is nil)', function () - assert.has_error(function () - gamestate_proxy:get("invalid") - end, - "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 () - - before_each(function () - gamestate_proxy:require_gamestates({"titlemenu", "stage"}) - end) - - it('should return a dummy gamestate when require_gamestates has been called', function () - -- interface - assert.are_same({ - require("game/menu/titlemenu").state, - require("game/menu/credits_dummy").state, - require("game/ingame/stage").state - }, - { - gamestate_proxy:get("titlemenu"), - gamestate_proxy:get("credits"), - gamestate_proxy:get("stage") - }) - end) - - end) - - end) - -end) diff --git a/src/game/tests/utestheadless_itests.lua b/src/game/tests/utestheadless_itests.lua deleted file mode 100644 index 0bf44a61..00000000 --- a/src/game/tests/utestheadless_itests.lua +++ /dev/null @@ -1,71 +0,0 @@ -require("engine/test/bustedhelper") -require("engine/test/integrationtest") -local picosonic_app = require("game/application/picosonic_app") -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') - for file in p:lines() do - add(files, file) - end - return files -end - --- require all itest scripts from the itests folder --- this will automatically register the itests to the itest_manager -local prefix = 'src/' -local suffix = '.lua' -local itest_scripts = find_all_scripts(prefix..'game/itests') -for itest_script in all(itest_scripts) do - -- truncate the src path prefix since we require from inside src/ - local require_path = itest_script:sub(prefix:len() + 1, - (suffix:len() + 1)) - require(require_path) -end - -describe('headless itest', function () - - 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 - for i = 1, #itest_manager.itests do - - 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 - itest_runner:update_game_and_test() - if should_render then - itest_runner:draw_game_and_test() - end - end - - local itest_fail_message = nil - 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, itest_runner.current_state, itest_fail_message) - - end) - - end - -end) diff --git a/src/game/tests/uteststage.lua b/src/game/tests/uteststage.lua deleted file mode 100644 index a783589a..00000000 --- a/src/game/tests/uteststage.lua +++ /dev/null @@ -1,915 +0,0 @@ -require("engine/test/bustedhelper") -local ui = require("engine/ui/ui") -local stage = require("game/ingame/stage") -local state = stage.state -local stage_data = require("game/data/stage_data") -local flow = require("engine/application/flow") -local titlemenu = require("game/menu/titlemenu") -local audio = require("game/resources/audio") -local gamestate = require("game/application/gamestate") --- only to stub class method in setup without needing instance (created in before_each) -local player_char = require("game/ingame/playercharacter") - -describe('stage', function () - - describe('state.type', function () - it('should be gamestate.types.stage', function () - assert.are_equal(gamestate.types.stage, state.type) - end) - end) - - describe('coroutine', function () - - describe('working coroutine function', function () - - local test_var = 0 - - local function set_var_after_delay_async(delay, value) - yield_delay(delay) - test_var = value - end - - describe('start_coroutine', function () - - setup(function () - state:start_coroutine(set_var_after_delay_async) - end) - - teardown(function () - clear_table(state.coroutine_curries) - end) - - it('should start a coroutine, stopping at the first yield', function () - assert.are_equal(1, #state.coroutine_curries) - assert.are_equal("suspended", costatus(state.coroutine_curries[1].coroutine)) - assert.are_equal(0, test_var) - end) - - end) - - describe('(2 coroutines started with yield_delays of 1.0 and 2.0 resp.)', function () - - before_each(function () - test_var = 0 - state:start_coroutine(set_var_after_delay_async, 1.0, 1) - state:start_coroutine(set_var_after_delay_async, 2.0, 2) - end) - - after_each(function () - clear_table(state.coroutine_curries) - end) - - describe('update_coroutines', function () - - it('should update all the coroutines (not enough time to finish any coroutine)', function () - for t = 1, 1.0 * fps - 1 do - state:update_coroutines() - end - assert.are_equal(2, #state.coroutine_curries) - assert.are_same({"suspended", "suspended"}, {costatus(state.coroutine_curries[1].coroutine), costatus(state.coroutine_curries[2].coroutine)}) - assert.are_equal(0, test_var) - end) - - it('should update all the coroutines (just enough time to finish the first one but not the second one)', function () - for t = 1, 1.0 * fps do - state:update_coroutines() - end - assert.are_equal(2, #state.coroutine_curries) - assert.are_same({"dead", "suspended"}, {costatus(state.coroutine_curries[1].coroutine), costatus(state.coroutine_curries[2].coroutine)}) - assert.are_equal(1, test_var) - end) - - it('should remove dead coroutines on the next call after finish (remove first one when dead)', function () - for t = 1, 1.0 * fps + 1 do - state:update_coroutines() - end - -- 1st coroutine has been removed, so the only coroutine left at index 1 is now the 2nd coroutine - assert.are_equal(1, #state.coroutine_curries) - assert.are_equal("suspended", costatus(state.coroutine_curries[1].coroutine)) - assert.are_equal(1, test_var) - end) - - it('should update all the coroutines (just enough time to finish the second one)', function () - for t = 1, 2.0 * fps do - state:update_coroutines() - end - assert.are_equal(1, #state.coroutine_curries) - assert.are_equal("dead", costatus(state.coroutine_curries[1].coroutine)) - assert.are_equal(2, test_var) - end) - - it('should remove dead coroutines on the next call after finish (remove second one when dead)', function () - for t = 1, 2.0 * fps + 1 do - state:update_coroutines() - end - assert.are_equal(0, #state.coroutine_curries) - assert.are_equal(2, test_var) - end) - - end) -- update_coroutines - - end) -- (2 coroutines started with yield_delays of 1.0 and 2.0 resp.) - - end) -- working coroutine function - - describe('coroutine updating coroutines', function () - - local test_var = 0 - local warn_stub - - local function update_coroutine_recursively_async() - test_var = test_var + 1 - state:update_coroutines() - end - - setup(function () - state:start_coroutine(update_coroutine_recursively_async) - warn_stub = stub(_G, "warn") - end) - - teardown(function () - clear_table(state.coroutine_curries) - warn_stub:revert() - end) - - after_each(function () - warn_stub:clear() - end) - - it('should resume the coroutine on 1 level only and warn that you shouldn\'t update resume already running coroutines', function () - state:update_coroutines() - assert.are_equal(1, test_var) -- proves we entered the coroutine function only once - assert.spy(warn_stub).was_called(1) - assert.spy(warn_stub).was_called_with(match.matches("stage.state:update_coroutines: coroutine should not be running outside its body: "), "flow") - end) - - end) - - describe('(failing coroutine started)', function () - - local function fail_async(delay) - yield_delay(delay) - error("fail_async finished") - end - - before_each(function () - state:start_coroutine(fail_async, 1.0) - end) - - after_each(function () - clear_table(state.coroutine_curries) - end) - - describe('update_coroutines', function () - - it('should not assert when an error doesn\'t occurs inside the coroutine resume yet', function () - assert.has_no_errors(function () state:update_coroutines() end) - end) - - it('should assert when an error occurs inside the coroutine resume', function () - assert.has_errors(function () - for t = 1, 1.0 * fps do - state:update_coroutines() - end - end, - "Assertion failed in coroutine update for: [coroutine_curry] (dead) (1.0)") - end) - - end) - - end) -- (failing coroutine started) - - describe('(coroutine method for custom class started with yield_delay of 1.0)', function () - - local test_class = new_class() - local test_instance - - function test_class:_init(value) - self.value = value - end - - function test_class:set_value_after_delay(new_value) - yield_delay(1.0) - self.value = new_value - end - - before_each(function () - -- create an instance and pass it to start_coroutine as the future self arg - -- (start_coroutine_method only works for the instance of stage.state itself) - test_instance = test_class(-10) - state:start_coroutine(test_class.set_value_after_delay, test_instance, 99) - end) - - after_each(function () - clear_table(state.coroutine_curries) - end) - - describe('update_coroutines', function () - - it('should update all the coroutines (not enough time to finish any coroutine)', function () - for t = 1, 1.0 * fps - 1 do - state:update_coroutines() - end - assert.are_equal(1, #state.coroutine_curries) - assert.are_equal("suspended", costatus(state.coroutine_curries[1].coroutine)) - assert.are_equal(-10, test_instance.value) - end) - - it('should update all the coroutines (just enough time to finish)', function () - for t = 1, 1.0 * fps do - state:update_coroutines() - end - assert.are_equal(1, #state.coroutine_curries) - assert.are_equal("dead", costatus(state.coroutine_curries[1].coroutine)) - assert.are_equal(99, test_instance.value) - end) - - it('should remove dead coroutines on the next call after finish after finish', function () - for t = 1, 1.0 * fps + 1 do - state:update_coroutines() - end - assert.are_equal(0, #state.coroutine_curries) - assert.are_equal(99, test_instance.value) - end) - - end) - - end) - - describe('stage coroutine method', function () - - -- create a dummy method and add it to state - function state:set_extra_value_after_delay(new_value) - yield_delay(1.0) - self.extra_value = new_value - end - - describe('start_coroutine_method', function () - - setup(function () - state.extra_value = -10 - state:start_coroutine_method(state.set_extra_value_after_delay, 99) - end) - - teardown(function () - clear_table(state.coroutine_curries) - end) - - it('should start a coroutine method, stopping at the first yield', function () - assert.are_equal(1, #state.coroutine_curries) - assert.are_equal("suspended", costatus(state.coroutine_curries[1].coroutine)) - assert.are_equal(-10, state.extra_value) - end) - - end) - - describe('(stage coroutine method started)', function () - - before_each(function () - state.extra_value = -10 - state:start_coroutine_method(state.set_extra_value_after_delay, 99) - end) - - after_each(function () - clear_table(state.coroutine_curries) - end) - - it('should start a coroutine method', function () - assert.are_equal(1, #state.coroutine_curries) - assert.are_equal("suspended", costatus(state.coroutine_curries[1].coroutine)) - assert.are_equal(-10, state.extra_value) - end) - - it('should not set self.extra_value to 99 only after 59 frames', function () - for t = 1, 1.0 * fps - 1 do - state:update_coroutines() - end - assert.are_equal(1, #state.coroutine_curries) - assert.are_equal("suspended", costatus(state.coroutine_curries[1].coroutine)) - assert.are_equal(-10, state.extra_value) - end) - - it('should set state.extra_value to 99 after 1s (60 frames) with coroutine dead not removed', function () - for t = 1, 1.0 * fps do - state:update_coroutines() - end - assert.are_equal(1, #state.coroutine_curries) - assert.are_equal("dead", costatus(state.coroutine_curries[1].coroutine)) - assert.are_equal(99, state.extra_value) - end) - - it('should remove the now dead coroutine', function () - for t = 1, 1.0 * fps + 1 do - state:update_coroutines() - end - assert.are_equal(0, #state.coroutine_curries) - assert.are_equal(99, state.extra_value) - end) - - end) -- (stage coroutine method started) - - end) -- 'stage coroutine method' - - end) -- coroutine - - describe('state', function () - - after_each(function () - state:init() - end) - - -- bugifx history: . - it('init', function () - assert.are_same({ - gamestate.types.stage, - {}, - 1, - stage.substates.play, - nil, - false, - vector.zero(), - ui.overlay(0) - }, - { - state.type, - state.coroutine_curries, - state.curr_stage_id, - state.current_substate, - state.player_char, - state.has_reached_goal, - state.camera_pos, - state.title_overlay - }) - end) - - describe('on_enter', function () - - local spawn_player_char_stub - local start_coroutine_method_stub - local play_bgm_stub - - setup(function () - spawn_player_char_stub = stub(state, "spawn_player_char") - start_coroutine_method_stub = stub(state, "start_coroutine_method") - play_bgm_stub = stub(state, "play_bgm") - end) - - teardown(function () - spawn_player_char_stub:revert() - start_coroutine_method_stub:revert() - play_bgm_stub:revert() - end) - - after_each(function () - spawn_player_char_stub:clear() - start_coroutine_method_stub:clear() - play_bgm_stub:clear() - end) - - before_each(function () - state:on_enter() - end) - - it('should enter the play substates', function () - assert.are_equal(stage.substates.play, state.current_substate) - end) - - it('should call spawn_player_char', function () - assert.spy(spawn_player_char_stub).was_called(1) - assert.spy(spawn_player_char_stub).was_called_with(match.ref(state)) - end) - - it('should set has_reached_goal to false', function () - assert.is_false(state.has_reached_goal) - end) - - it('should initialize camera at origin', function () - assert.are_equal(vector:zero(), state.camera_pos) - end) - - it('should call start_coroutine_method on show_stage_title_async', function () - assert.spy(start_coroutine_method_stub).was_called(1) - assert.spy(start_coroutine_method_stub).was_called_with(match.ref(state), state.show_stage_title_async) - end) - - it('should call start_coroutine_method on show_stage_title_async', function () - assert.spy(play_bgm_stub).was_called(1) - assert.spy(play_bgm_stub).was_called_with(match.ref(state)) - end) - - end) - - describe('on_exit', function () - - local title_overlay_clear_labels_stub - local start_coroutine_method_stub - local stop_bgm_stub - - setup(function () - title_overlay_clear_labels_stub = stub(state, "spawn_player_char") - start_coroutine_method_stub = stub(state, "start_coroutine_method") - stop_bgm_stub = stub(state, "stop_bgm") - end) - - teardown(function () - title_overlay_clear_labels_stub:revert() - start_coroutine_method_stub:revert() - stop_bgm_stub:revert() - end) - - after_each(function () - title_overlay_clear_labels_stub:clear() - start_coroutine_method_stub:clear() - stop_bgm_stub:clear() - end) - - before_each(function () - -- enter first, so we can check if on_exit cleans state correctly - state:on_enter() - state:on_exit() - end) - - it('should clear all the coroutines', function () - assert.are_same({}, state.coroutine_curries) - end) - - it('should clear the player character', function () - assert.is_nil(state.player_char) - end) - - it('should call title_overlay:clear_labels', function () - assert.spy(title_overlay_clear_labels_stub).was_called(1) - assert.spy(title_overlay_clear_labels_stub).was_called_with(match.ref(state)) - end) - - it('should reset pico8 camera', function () - assert.are_same({0, 0}, {pico8.camera_x, pico8.camera_y}) - end) - - it('should call stop_bgm', function () - assert.spy(stop_bgm_stub).was_called(1) - assert.spy(stop_bgm_stub).was_called_with(match.ref(state)) - end) - - end) - - describe('spawn_player_char', function () - - setup(function () - spy.on(player_char, "spawn_at") - end) - - teardown(function () - player_char.spawn_at:revert() - end) - - it('should spawn the player character at the stage spawn location', function () - state:spawn_player_char() - local player_char = state.player_char - assert.is_not_nil(player_char) - local spawn_position = state.curr_stage_data.spawn_location:to_center_position() - - -- interface - assert.are_equal(spawn_position, player_char.position) - -- we haven't initialized any map in busted, so the character is in the air and spawn_at detected this - assert.are_equal(motion_states.airborne, player_char.motion_state) - - -- implementation - assert.spy(player_char.spawn_at).was_called(1) - assert.spy(player_char.spawn_at).was_called_with(match.ref(state.player_char), spawn_position) - end) - - end) - - describe('(stage states added)', function () - - setup(function () - flow:add_gamestate(state) - flow:add_gamestate(titlemenu.state) -- for transition on reached goal - end) - - teardown(function () - clear_table(flow.gamestates) - end) - - describe('(stage state entered)', function () - - before_each(function () - flow:_change_state(state) - end) - - after_each(function () - flow.curr_state:on_exit() - flow.curr_state = nil - end) - - describe('update_camera', function () - - before_each(function () - state.player_char.position = vector(12, 24) - end) - - it('should move the camera to player position', function () - state:update_camera() - assert.are_equal(vector(12, 24), state.camera_pos) - end) - - end) - - describe('update', function () - - local update_coroutines_stub - local player_char_update_stub - local check_reached_goal_stub - local update_camera_stub - - setup(function () - update_coroutines_stub = stub(state, "update_coroutines") - player_char_update_stub = stub(player_char, "update") - check_reached_goal_stub = stub(state, "check_reached_goal") - update_camera_stub = stub(state, "update_camera") - end) - - teardown(function () - update_coroutines_stub:revert() - player_char_update_stub:revert() - check_reached_goal_stub:revert() - update_camera_stub:revert() - end) - - after_each(function () - update_coroutines_stub:clear() - player_char_update_stub:clear() - check_reached_goal_stub:clear() - update_camera_stub:clear() - end) - - describe('(current substate is play)', 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(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) - assert.spy(check_reached_goal_stub).was_called_with(match.ref(state)) - assert.spy(update_camera_stub).was_called(1) - assert.spy(update_camera_stub).was_called_with(match.ref(state)) end) - end) - - describe('(current substate is result)', 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(player_char_update_stub).was_not_called() - assert.spy(check_reached_goal_stub).was_not_called() - assert.spy(update_camera_stub).was_not_called() - end) - - end) - - end) -- update - - describe('render', function () - - local render_background_stub - local render_stage_elements_stub - local render_title_overlay_stub - - setup(function () - render_background_stub = stub(state, "render_background") - render_stage_elements_stub = stub(state, "render_stage_elements") - render_title_overlay_stub = stub(state, "render_title_overlay") - end) - - teardown(function () - render_background_stub:revert() - render_stage_elements_stub:revert() - render_title_overlay_stub:revert() - end) - - after_each(function () - render_background_stub:clear() - render_stage_elements_stub:clear() - render_title_overlay_stub:clear() - end) - - it('should reset camera, call render_background, render_stage_elements, render_title_overlay', function () - state:render() - assert.are_same({0, 0}, {pico8.camera_x, pico8.camera_y}) - assert.spy(render_background_stub).was_called(1) - assert.spy(render_background_stub).was_called_with(match.ref(state)) - assert.spy(render_stage_elements_stub).was_called(1) - assert.spy(render_stage_elements_stub).was_called_with(match.ref(state)) - assert.spy(render_title_overlay_stub).was_called(1) - assert.spy(render_title_overlay_stub).was_called_with(match.ref(state)) - end) - - end) -- state.render - - end) -- (enter stage state) - - describe('(enter stage state each time)', function () - - before_each(function () - flow:_change_state(state) - - -- clear any intro or back to title menu coroutines so the tests on coroutine curries are not messed up - clear_table(state.coroutine_curries) - end) - - after_each(function () - state.has_reached_goal = false - flow.curr_state:on_exit() -- whatever the current gamestate is - flow.curr_state = nil - end) - - describe('check_reached_goal', function () - - describe('(before the goal)', function () - - -- should be each - before_each(function () - state.player_char.position = vector(state.curr_stage_data.goal_x - 1, 0) - state:check_reached_goal() - end) - - it('should not set has_reached_goal to true', function () - assert.is_false(state.has_reached_goal) - end) - - it('should not start on_reached_goal_async', function () - assert.are_equal(0, #state.coroutine_curries) - end) - - end) - - describe('(just on the goal)', function () - - before_each(function () - state.player_char.position = vector(state.curr_stage_data.goal_x, 0) - state:check_reached_goal() - end) - - it('should set has_reached_goal to true', function () - assert.is_true(state.has_reached_goal) - end) - - it('should start on_reached_goal_async', function () - assert.are_equal(1, #state.coroutine_curries) - assert.are_equal("suspended", costatus(state.coroutine_curries[1].coroutine)) - end) - - end) - - describe('(after the goal)', function () - - before_each(function () - state.player_char.position = vector(state.curr_stage_data.goal_x + 1, 0) - state:check_reached_goal() - end) - - it('should set has_reached_goal to true', function () - assert.is_true(state.has_reached_goal) - end) - - it('should start on_reached_goal_async', function () - assert.are_equal(1, #state.coroutine_curries) - assert.are_equal("suspended", costatus(state.coroutine_curries[1].coroutine)) - end) - - end) - - end) - - describe('state.on_reached_goal_async', function () - - before_each(function () - state:start_coroutine_method(state.on_reached_goal_async) - end) - - it('should set substate to result after 1 update', function () - flow:update() - - assert.are_equal(stage.substates.result, state.current_substate) - end) - - it('should change gamestate to titlemenu after 1.0s + 1 update to apply the query next state', function () - for i = 1, stage_data.back_to_titlemenu_delay * fps + 1 do - flow:update() - end - assert.are_equal(gamestate.types.titlemenu, flow.curr_state.type) - end) - - end) - - describe('state.feedback_reached_goal', function () - local sfx_stub - - setup(function () - sfx_stub = stub(_G, "sfx") - end) - - teardown(function () - sfx_stub:revert() - end) - - after_each(function () - sfx_stub:clear() - end) - - it('should play goal_reached sfx', function () - state:feedback_reached_goal() - assert.spy(sfx_stub).was_called(1) - assert.spy(sfx_stub).was_called_with(audio.sfx_ids.goal_reached) - end) - - end) - - describe('back_to_titlemenu', function () - - it('should change gamestate to titlemenu on next update', function () - state:back_to_titlemenu() - flow:update() - assert.are_equal(gamestate.types.titlemenu, flow.curr_state.type) - end) - - end) - - describe('(no overlay labels)', function () - - before_each(function () - clear_table(state.title_overlay.labels) - end) - - it('show_stage_title_async should add a title label and remove it after global.show_stage_title_delay', function () - state:start_coroutine_method(state.show_stage_title_async) - state:update_coroutines() - assert.are_equal(ui.label(state.curr_stage_data.title, vector(50, 30), colors.white), state.title_overlay.labels["title"]) - for i = 2, stage_data.show_stage_title_delay*fps do - state:update_coroutines() - end - assert.is_nil(state.title_overlay.labels["title"]) - end) - - end) - - describe('state render methods', function () - - local map_stub - local player_char_render_stub - - setup(function () - rectfill_stub = stub(_G, "rectfill") - map_stub = stub(_G, "map") - spy.on(state, "render_environment") - player_char_render_stub = stub(player_char, "render") - title_overlay_draw_labels_stub = stub(ui.overlay, "draw_labels") - end) - - teardown(function () - rectfill_stub:revert() - map_stub:revert() - state.render_environment:revert() - player_char_render_stub:revert() - title_overlay_draw_labels_stub:revert() - end) - - after_each(function () - rectfill_stub:clear() - map_stub:clear() - state.render_environment:clear() - player_char_render_stub:clear() - title_overlay_draw_labels_stub:clear() - end) - - it('render_title_overlay should call title_overlay:draw_labels', function () - spy.on(state, "render_environment") - state:render_title_overlay() - assert.are_equal(vector.zero(), vector(pico8.camera_x, pico8.camera_y)) - assert.spy(title_overlay_draw_labels_stub).was_called(1) - assert.spy(title_overlay_draw_labels_stub).was_called_with(state.title_overlay) - end) - - it('render_background should reset camera position, call rectfill on the whole screen with stage background color', function () - state.camera_pos = vector(24, 13) - state:render_background() - assert.are_equal(vector(0, 0), vector(pico8.camera_x, pico8.camera_y)) - assert.spy(rectfill_stub).was_called(1) - assert.spy(rectfill_stub).was_called_with(0, 0, 127, 127, state.curr_stage_data.background_color) - end) - - it('render_stage_elements should set camera position, call map for environment and player_char:render', function () - state.camera_pos = vector(24, 13) - state:render_stage_elements() - assert.are_equal(vector(24 - 128 / 2, 13 - 128 / 2), vector(pico8.camera_x, pico8.camera_y)) - assert.spy(state.render_environment).was_called(1) - assert.spy(state.render_environment).was_called_with(match.ref(state)) - assert.spy(player_char_render_stub).was_called(1) - assert.spy(player_char_render_stub).was_called_with(match.ref(state.player_char)) - end) - - it('set_camera_offset_stage should set the pico8 camera so that it is centered on the camera position', function () - state.camera_pos = vector(24, 13) - state:set_camera_offset_stage() - assert.are_equal(vector(24 - 128 / 2, 13 - 128 / 2), vector(pico8.camera_x, pico8.camera_y)) - end) - - describe('(after set_camera_offset_stage)', function () - - before_each(function () - state:set_camera_offset_stage() - end) - - it('render_environment should call map', function () - state:render_environment() - assert.spy(map_stub).was_called(1) - assert.spy(map_stub).was_called_with(0, 0, 0, 0, state.curr_stage_data.width, state.curr_stage_data.height) - end) - - it('render_player_char should call player_char:render', function () - state:render_player_char() - assert.spy(player_char_render_stub).was_called(1) - assert.spy(player_char_render_stub).was_called_with(match.ref(state.player_char)) - end) - - end) - - end) -- state render methods - - describe('state audio methods', function () - - after_each(function () - pico8.current_music = nil - end) - - it('play_bgm should start level bgm', function () - state:play_bgm() - assert.are_same({music=audio.music_pattern_ids.green_hill, fadems=0, channel_mask=0}, pico8.current_music) - end) - - it('stop_bgm should stop level bgm if started, else do nothing', function () - state:stop_bgm() - assert.is_nil(pico8.current_music) - state:play_bgm() - state:stop_bgm() - assert.is_nil(pico8.current_music) - state:play_bgm() - state:stop_bgm(2.0) - assert.is_nil(pico8.current_music) - end) - - end) -- state audio methods - - describe('on exit stage state to enter titlemenu state', function () - - before_each(function () - flow:_change_state(titlemenu.state) - end) - - it('player character should be nil', function () - assert.is_nil(state.player_char) - end) - - it('title overlay should be empty', function () - assert.is_not_nil(state.title_overlay) - assert.is_not_nil(state.title_overlay.labels) - assert.is_true(is_empty(state.title_overlay.labels)) - end) - - describe('reenter stage state', function () - - -- should be each - before_each(function () - flow:_change_state(state) - end) - - it('current substate should be play', function () - assert.are_equal(stage.substates.play, state.current_substate) - end) - - it('player character should not be nil and respawned at the spawn location', function () - assert.is_not_nil(state.player_char) - assert.are_equal(state.curr_stage_data.spawn_location:to_center_position(), state.player_char.position) - end) - - it('should not have reached goal', function () - assert.is_false(state.has_reached_goal) - end) - - end) - - end) -- on exit stage state to enter titlemenu state - - end) -- (enter stage state each time) - - end) -- (stage states added) - - end) -- (stage state) - -end) diff --git a/src/game/tests/utesttitlemenu.lua b/src/game/tests/utesttitlemenu.lua deleted file mode 100644 index 7296c00c..00000000 --- a/src/game/tests/utesttitlemenu.lua +++ /dev/null @@ -1,268 +0,0 @@ -require("engine/test/bustedhelper") -local input = require("engine/input/input") -local titlemenu = require("game/menu/titlemenu") -local flow = require("engine/application/flow") -local gamestate = require("game/application/gamestate") -local credits = require("game/menu/credits_dummy") -local stage = require("game/ingame/stage_dummy") - -describe('titlemenu', function () - - describe('state.type', function () - it('should be gamestate.types.titlemenu', function () - assert.are_equal(gamestate.types.titlemenu, titlemenu.state.type) - end) - end) - - describe('(stage states added)', function () - - setup(function () - flow:add_gamestate(titlemenu.state) - flow:add_gamestate(credits.state) - flow:add_gamestate(stage.state) - end) - - teardown(function () - clear_table(flow.gamestates) - end) - - - describe('state:on_enter', function () - - setup(function () - titlemenu.state:on_enter() - end) - - teardown(function () - titlemenu.state.current_cursor_index = 0 - end) - - it('should initialize cursor at index 0', function () - titlemenu.state:on_enter() - assert.are_equal(0, titlemenu.state.current_cursor_index) - end) - - end) - - describe('state:on_exit', function () - end) - - describe('(titlemenu state entered)', function () - - setup(function () - flow:_change_state(titlemenu.state) - end) - - teardown(function () - flow.curr_state:on_exit() - flow.curr_state = nil - end) - - describe('state.current_cursor_index', function () - it('should be set to 0', function () - assert.are_equal(0, titlemenu.state.current_cursor_index) - end) - end) - - describe('state:update', function () - - local move_cursor_up_stub - - setup(function () - move_cursor_up_stub = stub(titlemenu.state, "move_cursor_up") - move_cursor_down_stub = stub(titlemenu.state, "move_cursor_down") - confirm_current_selection_stub = stub(titlemenu.state, "confirm_current_selection") - end) - - teardown(function () - move_cursor_up_stub:revert() - move_cursor_down_stub:revert() - confirm_current_selection_stub:revert() - end) - - after_each(function () - 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.x] = btn_states.released - - move_cursor_up_stub:clear() - move_cursor_down_stub:clear() - confirm_current_selection_stub:clear() - end) - - it('(when input up in down) it should be move cursor up', function () - input.players_btn_states[0][button_ids.up] = btn_states.just_pressed - titlemenu.state:update() - assert.spy(move_cursor_up_stub).was_called(1) - assert.spy(move_cursor_up_stub).was_called_with(match.ref(titlemenu.state)) - end) - - it('(when input down in down) it should be move cursor down', function () - input.players_btn_states[0][button_ids.down] = btn_states.just_pressed - titlemenu.state:update() - assert.spy(move_cursor_down_stub).was_called(1) - assert.spy(move_cursor_down_stub).was_called_with(match.ref(titlemenu.state)) - end) - - it('(when input x in down) it should be move cursor x', function () - input.players_btn_states[0][button_ids.x] = btn_states.just_pressed - titlemenu.state:update() - assert.spy(confirm_current_selection_stub).was_called(1) - assert.spy(confirm_current_selection_stub).was_called_with(match.ref(titlemenu.state)) - end) - - end) - - describe('(cursor start at index 0)', function () - - before_each(function () - titlemenu.state.current_cursor_index = 0 - end) - - after_each(function () - titlemenu.state.current_cursor_index = 0 - end) - - describe('state:move_cursor_up', function () - - it('should not change current_cursor_index due to clamping', function () - titlemenu.state:move_cursor_up() - assert.are_equal(0, titlemenu.state.current_cursor_index) - end) - - end) - - describe('state:move_cursor_down', function () - - it('should increase current_cursor_index', function () - titlemenu.state:move_cursor_down() - assert.are_equal(1, titlemenu.state.current_cursor_index) - end) - - end) - - describe('render', function () - - local api_print_stub - - setup(function () - api_print_stub = stub(api, "print") - end) - - teardown(function () - api_print_stub:revert() - end) - - after_each(function () - api_print_stub:clear() - end) - - it('should print "starts", "credits" and cursor ">" in front of start in white', function () - titlemenu.state:render() - assert.are_equal(colors.white, pico8.color) - assert.spy(api_print_stub).was_called(3) - assert.spy(api_print_stub).was_called_with("start", 4*11, 6*12) - assert.spy(api_print_stub).was_called_with("credits", 4*11, 6*13) - assert.spy(api_print_stub).was_called_with(">", 4*10, 6*12) - end) - - end) - - end) - - describe('(cursor start at index 1)', function () - - before_each(function () - titlemenu.state.current_cursor_index = 1 - end) - - after_each(function () - titlemenu.state.current_cursor_index = 0 - end) - - describe('state:move_cursor_up', function () - - it('should decrease current_cursor_index', function () - titlemenu.state:move_cursor_up() - assert.are_equal(0, titlemenu.state.current_cursor_index) - end) - - end) - - describe('state:move_cursor_down', function () - - it('should not change current_cursor_index due to clamping', function () - titlemenu.state:move_cursor_down() - assert.are_equal(1, titlemenu.state.current_cursor_index) - end) - - end) - - - describe('render', function () - - local api_print_stub - - setup(function () - api_print_stub = stub(api, "print") - end) - - teardown(function () - api_print_stub:revert() - end) - - after_each(function () - api_print_stub:clear() - end) - - it('should print "starts", "credits" and cursor ">" in front of credits in white', function () - titlemenu.state:render() - assert.are_equal(colors.white, pico8.color) - assert.spy(api_print_stub).was_called(3) - assert.spy(api_print_stub).was_called_with("start", 4*11, 6*12) - assert.spy(api_print_stub).was_called_with("credits", 4*11, 6*13) - assert.spy(api_print_stub).was_called_with(">", 4*10, 6*13) - end) - - end) - - end) - - end) -- (titlemenu state entered) - - describe('(enter titlemenu state each time)', function () - - before_each(function () - flow:_change_state(titlemenu.state) - end) - - after_each(function () - flow.curr_state:on_exit() -- whatever the current gamestate is - flow.curr_state = nil - end) - - describe('state:confirm_current_selection', function () - - it('should have queried stage state', function () - titlemenu.state.current_cursor_index = 0 - titlemenu.state:confirm_current_selection() - assert.are_equal(gamestate.types.stage, flow.next_state.type) - end) - - end) - - describe('state:confirm_current_selection', function () - - it('should have queried credits state', function () - titlemenu.state.current_cursor_index = 1 - titlemenu.state:confirm_current_selection() - assert.are_equal(gamestate.types.credits, flow.next_state.type) - end) - - end) - - end) -- (enter titlemenu state each time) - - end) -- (stage states added) - -end) diff --git a/src/game/ingame/playercharacter.lua b/src/ingame/playercharacter.lua similarity index 87% rename from src/game/ingame/playercharacter.lua rename to src/ingame/playercharacter.lua index 596c2e60..0edd926a 100644 --- a/src/game/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -2,12 +2,14 @@ require("engine/application/constants") require("engine/core/class") require("engine/core/helper") require("engine/core/math") +--#if log +local _logging = require("engine/debug/logging") +--#endif local input = require("engine/input/input") -local world = require("game/platformer/world") +local world = require("platformer/world") local animated_sprite = require("engine/render/animated_sprite") -local pc_data = require("game/data/playercharacter_data") -local motion = require("game/platformer/motion") - +local pc_data = require("data/playercharacter_data") +local motion = require("platformer/motion") -- enum for character control control_modes = { @@ -31,8 +33,9 @@ motion_modes = { -- enum for character motion state in platformer mode motion_states = { - grounded = 1, -- character is on the ground - airborne = 2 -- character is in the air + grounded = 1, -- character is idle or running on the ground + falling = 2, -- character is falling in the air, but not spinning + air_spin = 3 -- character is in the air after a jump } @@ -105,7 +108,23 @@ function player_char:_setup() self.anim_spr:play("idle") end --- spawn character at given position, and escape from ground / enter airborne state if needed +function player_char:is_grounded() + return self.motion_state == motion_states.grounded +end + +function player_char:is_compact() + return self.motion_state == motion_states.air_spin +end + +function player_char:get_center_height() + return self:is_compact() and pc_data.center_height_compact or pc_data.center_height_standing +end + +function player_char:get_full_height() + return self:is_compact() and pc_data.full_height_compact or pc_data.full_height_standing +end + +-- spawn character at given position, detecting ground/air on arrival function player_char:spawn_at(position) self:_setup() self:warp_to(position) @@ -113,35 +132,36 @@ end -- spawn character at given bottom position, with same post-process as spawn_at function player_char:spawn_bottom_at(bottom_position) - self:spawn_at(bottom_position - vector(0, pc_data.center_height_standing)) + self:spawn_at(bottom_position - vector(0, self:get_center_height())) end --- warp character to specific position, and update motion state +-- warp character to specific position, and update motion state (grounded/falling) +-- while escaping from ground if needed -- use this when you don't want to reset the character state as spawn_at does function player_char:warp_to(position) self.position = position -- character is initialized grounded, but let him fall if he is spawned in the air - local is_grounded = self:_check_escape_from_ground() + local new_is_grounded = self:_check_escape_from_ground() -- always enter new state depending on whether ground is detected, -- forcing state vars reset even if we haven't changed state - local new_state = is_grounded and motion_states.grounded or motion_states.airborne + local new_state = new_is_grounded and motion_states.grounded or motion_states.falling self:_enter_motion_state(new_state) end -- same as warp_to, but with bottom position function player_char:warp_bottom_to(bottom_position) - self:warp_to(bottom_position - vector(0, pc_data.center_height_standing)) + self:warp_to(bottom_position - vector(0, self:get_center_height())) end -- move the player character so that the bottom center is at the given position function player_char:get_bottom_center() - return self.position + vector(0, pc_data.center_height_standing) + return self.position + vector(0, self:get_center_height()) end -- move the player character so that the bottom center is at the given position function player_char:set_bottom_center(bottom_center_position) - self.position = bottom_center_position - vector(0, pc_data.center_height_standing) + self.position = bottom_center_position - vector(0, self:get_center_height()) end -- move the player character from delta_vector in px @@ -231,7 +251,7 @@ function player_char:_compute_ground_sensors_signed_distance(center_position) local min_signed_distance = 1 / 0 -- max (32768 in pico-8, but never enter it manually as it would be negative) local highest_ground_slope_angle = nil - -- check both ground sensors for ground. if any finds ground, return true + -- check both ground sensors for ground for i in all({horizontal_dirs.left, horizontal_dirs.right}) do -- check that ground sensor #i is on top of or below the mask column @@ -259,7 +279,7 @@ function player_char:_compute_ground_sensors_signed_distance(center_position) end function player_char:_get_prioritized_dir() - if self.motion_state == motion_states.grounded then + if self:is_grounded() then if self.ground_speed ~= 0 then return signed_speed_to_dir(self.ground_speed) end @@ -277,7 +297,7 @@ function player_char:_get_ground_sensor_position_from(center_position, horizonta -- 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) + local x_floored_bottom_center = x_floored_center_position + vector(0, self:get_center_height()) -- using a ground_sensor_extent_x in .5 and flooring +/- this value allows us to get the checked column x (the x corresponds to the left of that column) local offset_x = flr(horizontal_dir_signs[horizontal_dir] * pc_data.ground_sensor_extent_x) @@ -342,9 +362,14 @@ end -- enter motion state, reset state vars appropriately function player_char:_enter_motion_state(next_motion_state) - self.motion_state = next_motion_state - if next_motion_state == motion_states.airborne then - -- we have just left the ground, enter airborne state + if next_motion_state == motion_states.falling then + -- we have just left the ground without jumping, enter falling state + -- and since ground speed is now unused, reset it for clarity + self.ground_speed = 0 + self.slope_angle = nil + self.should_jump = false + elseif next_motion_state == motion_states.air_spin then + -- we have just jumped, enter air_spin state -- and since ground speed is now unused, reset it for clarity self.ground_speed = 0 self.slope_angle = nil @@ -358,6 +383,20 @@ function player_char:_enter_motion_state(next_motion_state) self.has_interrupted_jump = false self.anim_spr:play("idle") end + + local was_compact = self:is_compact() + + self.motion_state = next_motion_state + + if not was_compact and self:is_compact() then + -- character became compact (e.g. crouching or start jumping), + -- move it slightly down to keep center position continuity + self.position:add_inplace(vector(0, pc_data.center_height_standing - pc_data.center_height_compact)) + elseif was_compact and not self:is_compact() then + -- character is now standing (e.g. landing after air spin), + -- move it slightly up to keep center position continuity + self.position:add_inplace(vector(0, - pc_data.center_height_standing + pc_data.center_height_compact)) + end end -- update velocity, position and state based on current motion state @@ -366,12 +405,12 @@ function player_char:_update_platformer_motion() -- (as in classic Sonic), but also apply an initial impulse if character starts idle and -- left/right is pressed just when jumping (to fix classic Sonic missing a directional input frame there) if self.motion_state == motion_states.grounded then - self:_check_jump() -- this may change the motion state to airborne + self:_check_jump() -- this may change the motion state to air_spin end - if self.motion_state == motion_states.grounded then + if self:is_grounded() then self:_update_platformer_motion_grounded() - else -- self.motion_state == motion_states.airborne + else self:_update_platformer_motion_airborne() end end @@ -400,7 +439,7 @@ function player_char:_update_platformer_motion_grounded() -- (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) + self:_enter_motion_state(motion_states.falling) else -- we are still grounded, so: @@ -562,7 +601,7 @@ function player_char:_compute_ground_motion_result() -- 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] + -- note that this calculation equivalent to adding to ref_motion_result.position:get(coord) -- sign(signed_distance_x) * (max_distance_x - distance_to_floored_x) motion_result.position.x = self.position.x + signed_distance_x end @@ -635,11 +674,13 @@ function player_char:_next_ground_step(horizontal_dir, ref_motion_result) 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 ref_motion_result.is_blocked then + -- 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 + -- (unlike Classic Sonic, we do check for ceilings even when Sonic is grounded; + -- this case rarely happens in normally constructed levels though) 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, @@ -668,7 +709,7 @@ function player_char:_is_blocked_by_ceiling_at(center_position) -- check if ground sensor #i has ceiling closer than a character's height local sensor_position = self:_get_ground_sensor_position_from(center_position, i) - if player_char._is_column_blocked_by_ceiling_at(sensor_position) then + if self:_is_column_blocked_by_ceiling_at(sensor_position) then return true end @@ -683,7 +724,7 @@ end -- because we assume that if the character could step this up, it would have and the passed -- sensor_position would be the resulting position, so only higher tiles will be considered -- 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) +function player_char:_is_column_blocked_by_ceiling_at(sensor_position) assert(flr(sensor_position.x) == sensor_position.x, "player_char:_is_column_blocked_by_ceiling_at: sensor_position.x must be floored") @@ -710,7 +751,7 @@ function player_char._is_column_blocked_by_ceiling_at(sensor_position) -- 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 + if height_distance >= self:get_full_height() then return false end @@ -724,7 +765,7 @@ function player_char._is_column_blocked_by_ceiling_at(sensor_position) -- hits the head or not. if it doesn't stop here, return false, -- the head is below the ceiling: -- local height_distance = sensor_position.y - current_tile_bottom - -- return height_distance < pc_data.full_height_standing + -- return height_distance < self:get_full_height() end end @@ -743,7 +784,7 @@ function player_char:_check_jump_intention() end -- if character intends to jump, apply jump velocity from current ground --- and enter the airborne state +-- and enter the air_spin state -- return true iff jump was applied function player_char:_check_jump() if self.should_jump then @@ -756,7 +797,7 @@ function player_char:_check_jump() -- 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) + self:_enter_motion_state(motion_states.air_spin) self.has_jumped_this_frame = true return true end @@ -832,6 +873,7 @@ end -- - 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) + -- this is pretty rare in the air, but could happen when being pushed upward by fans if self.velocity == vector.zero() then return motion.air_motion_result( self.position, @@ -868,21 +910,21 @@ 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" +-- over velocity:get(coord) px, where coord is "x" or "y" function player_char:_advance_in_air_along(ref_motion_result, velocity, coord) log("_advance_in_air_along: "..joinstr(", ", ref_motion_result, velocity, coord), "trace") - if velocity[coord] == 0 then return end + if velocity:get(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]) + local initial_position_coord = ref_motion_result.position:get(coord) + local max_pixel_distance = player_char._compute_max_pixel_distance(initial_position_coord, velocity:get(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]) + ref_motion_result.position:set(coord, flr(ref_motion_result.position:get(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 @@ -894,7 +936,7 @@ function player_char:_advance_in_air_along(ref_motion_result, velocity, coord) else direction = directions.down end - if velocity[coord] < 0 then + if velocity:get(coord) < 0 then direction = oppose_dir(direction) end @@ -909,8 +951,8 @@ function player_char:_advance_in_air_along(ref_motion_result, velocity, coord) if not ref_motion_result:is_blocked_along(direction) then -- 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] + local are_subpixels_left = initial_position_coord + velocity:get(coord) > ref_motion_result.position:get(coord) + -- local are_subpixels_left = initial_position_coord + max_pixel_distance > ref_motion_result.position:get(coord) if are_subpixels_left then -- character has not been blocked and has some subpixels left to go -- *only* when moving in the positive sense (right/up), @@ -921,7 +963,7 @@ function player_char:_advance_in_air_along(ref_motion_result, velocity, coord) -- to the positive sense and should -- never hit a wall back local is_blocked_by_extra_step = false - if velocity[coord] > 0 then + if velocity:get(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") @@ -941,9 +983,9 @@ function player_char:_advance_in_air_along(ref_motion_result, velocity, coord) -- 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] + -- note that this calculation equivalent to adding to ref_motion_result.position:get(coord) + -- sign(velocity:get(coord)) * (max_distance - distance_to_floored_coord) + ref_motion_result.position:set(coord, initial_position_coord + velocity:get(coord)) end end end @@ -978,7 +1020,15 @@ function player_char:_next_air_step(direction, ref_motion_result) 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 + -- however, if we want to allow jump from an ascending sheer angle directly onto a platform, + -- as suggested by the SPG (http://info.sonicretro.org/SPG:Solid_Tiles#Ceiling_Sensors_.28C_and_D.29) + -- where ground detection from the air is done when moving downward or when moving upward, but faster horizontally + -- than vertically, then we would not only need to add the x vs y spd check but also enable *snap* to ground + -- from the last step position, since we may miss a few pixels from here to reach the ground + -- (otherwise, the obstacle may was well be considered as a wall, as we're doing now) + -- Then we would have a check more symmetrical to below, with + -- `if self.velocity.y > 0 or abs(self.velocity.x) > abs(self.velocity.y)` + -- 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 @@ -1004,13 +1054,21 @@ function player_char:_next_air_step(direction, ref_motion_result) 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 + -- Ceiling check + -- It 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 + -- The SPG (http://info.sonicretro.org/SPG:Solid_Tiles#Ceiling_Sensors_.28C_and_D.29) + -- remarks that ceiling detection is done when moving upward or when moving faster horizontally than vertically + -- Since it's just for this extra test, we check self.velocity directly instead of passing it as argument + -- Note that we don't check the exact step direction, if we happen to hit the ceiling during + -- the X motion, that's fine. + -- In practice, when approaching a ceiling from a descending direction with a sheer horizontal angle, + -- we will hit the block as a wall first; but that's because we consider blocks as wall and ceilings at the same time. + if not ref_motion_result.is_blocked_by_wall and + (self.velocity.y < 0 or abs(self.velocity.x) > abs(self.velocity.y)) 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 @@ -1019,6 +1077,9 @@ function player_char:_next_air_step(direction, ref_motion_result) 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 + -- 4-quadrant note: if moving diagonally downward, this will actually correspond to the SPG case + -- mentioned above where ysp >= 0 but abs(xsp) > abs(ysp) + -- in this case, we are really detecting the *ceiling*, but Sonic can also start running on it ref_motion_result.is_blocked_by_wall = true end end @@ -1040,7 +1101,10 @@ end -- update the velocity and position of the character following debug motion rules function player_char:_update_debug() self:_update_velocity_debug() - self:move_by(self.debug_velocity * delta_time) + -- it's much more complicated to access app from here (e.g. via flow.curr_state) + -- just to get delta_time, so we just use the constant as we know we are at 60 FPS + -- otherwise we'd have to change utests to init app+flow each time + self:move_by(self.debug_velocity * delta_time60) end function player_char:_update_velocity_debug() @@ -1055,15 +1119,15 @@ end -- 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) - if self.move_intention[coord] ~= 0 then + if self.move_intention:get(coord) ~= 0 then -- some input => accelerate (direction may still change or be opposed) - local clamped_move_intention_comp = mid(-1, self.move_intention[coord], 1) - self.debug_velocity[coord] = self.debug_velocity[coord] + self.debug_move_accel * delta_time * clamped_move_intention_comp - self.debug_velocity[coord] = mid(-self.debug_move_max_speed, self.debug_velocity[coord], self.debug_move_max_speed) + local clamped_move_intention_comp = mid(-1, self.move_intention:get(coord), 1) + self.debug_velocity:set(coord, self.debug_velocity:get(coord) + self.debug_move_accel * delta_time60 * clamped_move_intention_comp) + self.debug_velocity:set(coord, mid(-self.debug_move_max_speed, self.debug_velocity:get(coord), self.debug_move_max_speed)) else -- no input => decelerate - if self.debug_velocity[coord] ~= 0 then - self.debug_velocity[coord] = sgn(self.debug_velocity[coord]) * max(abs(self.debug_velocity[coord]) - self.debug_move_decel * delta_time, 0) + if self.debug_velocity:get(coord) ~= 0 then + self.debug_velocity:set(coord, sgn(self.debug_velocity:get(coord)) * max(abs(self.debug_velocity:get(coord)) - self.debug_move_decel * delta_time60, 0)) end end end diff --git a/src/game/tests/utestplayercharacter.lua b/src/ingame/playercharacter_utest.lua similarity index 91% rename from src/game/tests/utestplayercharacter.lua rename to src/ingame/playercharacter_utest.lua index f29a3e1a..529137fa 100644 --- a/src/game/tests/utestplayercharacter.lua +++ b/src/ingame/playercharacter_utest.lua @@ -1,11 +1,13 @@ require("engine/test/bustedhelper") require("engine/core/math") -local player_char = require("game/ingame/playercharacter") +local animated_sprite = require("engine/render/animated_sprite") + +local player_char = require("ingame/playercharacter") local input = require("engine/input/input") -local motion = require("game/platformer/motion") +local motion = require("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") +local pc_data = require("data/playercharacter_data") +local tile_test_data = require("test_data/tile_test_data") describe('player_char', function () @@ -181,6 +183,68 @@ describe('player_char', function () pc.debug_move_decel = 480. end) + describe('is_grounded', function () + + it('should return true when character is grounded', function () + assert.is_true(pc:is_grounded()) + end) + + it('should return false when character is falling', function () + pc.motion_state = motion_states.falling + assert.is_false(pc:is_grounded()) + end) + + it('should return false when character is in air spin', function () + pc.motion_state = motion_states.air_spin + assert.is_false(pc:is_grounded()) + end) + + end) + + describe('is_compact', function () + + it('should return false when character is grounded', function () + assert.is_false(pc:is_compact()) + end) + + it('should return false when character is falling', function () + pc.motion_state = motion_states.falling + assert.is_false(pc:is_compact()) + end) + + it('should return true when character is in air spin', function () + pc.motion_state = motion_states.air_spin + assert.is_true(pc:is_compact()) + end) + + end) + + describe('get_center_height', function () + + it('should return center height standing when standing', function () + assert.are_equal(pc_data.center_height_standing, pc:get_center_height()) + end) + + it('should return center height compact when compact', function () + pc.motion_state = motion_states.air_spin + assert.are_equal(pc_data.center_height_compact, pc:get_center_height()) + end) + + end) + + describe('get_full_height', function () + + it('should return full height standing when standing', function () + assert.are_equal(pc_data.full_height_standing, pc:get_full_height()) + end) + + it('should return full height compact when compact', function () + pc.motion_state = motion_states.air_spin + assert.are_equal(pc_data.full_height_compact, pc:get_full_height()) + end) + + end) + describe('spawn_at', function () setup(function () @@ -215,16 +279,20 @@ describe('player_char', function () setup(function () spy.on(player_char, "spawn_at") + stub(player_char, "get_center_height", function () + return 11 + end) end) teardown(function () player_char.spawn_at:revert() + player_char.get_center_height:revert() end) it('should call spawn_at with the position offset by -(character center height)', function () pc:spawn_bottom_at(vector(56, 12)) assert.spy(player_char.spawn_at).was_called(1) - assert.spy(player_char.spawn_at).was_called_with(match.ref(pc), vector(56, 12 - pc_data.center_height_standing)) + assert.spy(player_char.spawn_at).was_called_with(match.ref(pc), vector(56, 12 - 11)) end) end) @@ -264,14 +332,14 @@ describe('player_char', function () check_escape_from_ground_mock:revert() end) - it('should call _check_escape_from_ground and _enter_motion_state(motion_states.airborne)', function () + it('should call _check_escape_from_ground and _enter_motion_state(motion_states.falling)', function () pc:spawn_at(vector(56, 12)) -- implementation assert.spy(check_escape_from_ground_mock).was_called(1) assert.spy(check_escape_from_ground_mock).was_called_with(match.ref(pc)) 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(enter_motion_state_stub).was_called_with(match.ref(pc), motion_states.falling) end) end) @@ -308,32 +376,60 @@ describe('player_char', function () setup(function () spy.on(player_char, "warp_to") + stub(player_char, "get_center_height", function () + return 11 + end) end) teardown(function () player_char.warp_to:revert() + player_char.get_center_height:revert() end) it('should call warp_to with the position offset by -(character center height)', function () pc:warp_bottom_to(vector(56, 12)) assert.spy(player_char.warp_to).was_called(1) - assert.spy(player_char.warp_to).was_called_with(match.ref(pc), vector(56, 12 - pc_data.center_height_standing)) + assert.spy(player_char.warp_to).was_called_with(match.ref(pc), vector(56, 12 - 11)) end) end) describe('get_bottom_center', function () + + setup(function () + stub(player_char, "get_center_height", function () + return 11 + end) + end) + + teardown(function () + player_char.get_center_height:revert() + end) + it('(10 0 3) => at (10 6)', function () pc.position = vector(10, 0) - assert.are_equal(vector(10, 0 + pc_data.center_height_standing), pc:get_bottom_center()) + assert.are_equal(vector(10, 0 + 11), pc:get_bottom_center()) end) + end) - describe('+ set_bottom_center', function () + describe('set_bottom_center', function () + + setup(function () + stub(player_char, "get_center_height", function () + return 11 + end) + end) + + teardown(function () + player_char.get_center_height:revert() + end) + it('set_bottom_center (10 6) => at (10 0)', function () - pc:set_bottom_center(vector(10, 0 + pc_data.center_height_standing)) + pc:set_bottom_center(vector(10, 0 + 11)) assert.are_equal(vector(10, 0), pc.position) end) + end) describe('move_by', function () @@ -390,7 +486,7 @@ describe('player_char', function () end) after_each(function () - input.players_btn_states[0] = generate_initial_btn_states() + input:init() player_char._toggle_debug_motion:clear() end) @@ -521,7 +617,7 @@ describe('player_char', function () end) after_each(function () - input.players_btn_states[0] = generate_initial_btn_states() + input:init() player_char.spawn_at:clear() end) @@ -725,13 +821,13 @@ describe('player_char', function () end) it('should return left when character is moving airborne toward left', function () - pc.motion_state = motion_states.airborne + pc.motion_state = motion_states.falling -- or any airborne state pc.velocity.x = -4 assert.are_equal(horizontal_dirs.left, pc:_get_prioritized_dir()) end) it('should return right when character is moving airborne toward right', function () - pc.motion_state = motion_states.airborne + pc.motion_state = motion_states.falling -- or any airborne state pc.velocity.x = 4 assert.are_equal(horizontal_dirs.right, pc:_get_prioritized_dir()) end) @@ -750,20 +846,30 @@ describe('player_char', function () describe('_get_ground_sensor_position_from', function () - it('* should return the position down-left of the character center when horizontal dir is left', function () - assert.are_equal(vector(7, 10 + pc_data.center_height_standing), pc:_get_ground_sensor_position_from(vector(10, 10), horizontal_dirs.left)) + setup(function () + stub(player_char, "get_center_height", function () + return 11 + end) + end) + + teardown(function () + player_char.get_center_height:revert() + end) + + it('should return the position down-left of the character center when horizontal dir is left', function () + assert.are_equal(vector(7, 10 + 11), pc:_get_ground_sensor_position_from(vector(10, 10), horizontal_dirs.left)) end) it('should return the position down-left of the x-floored character center when horizontal dir is left', function () - assert.are_equal(vector(7, 10 + pc_data.center_height_standing), pc:_get_ground_sensor_position_from(vector(10.9, 10), horizontal_dirs.left)) + assert.are_equal(vector(7, 10 + 11), pc:_get_ground_sensor_position_from(vector(10.9, 10), horizontal_dirs.left)) end) - it('* should return the position down-left of the character center when horizontal dir is right', function () - assert.are_equal(vector(12, 10 + pc_data.center_height_standing), pc:_get_ground_sensor_position_from(vector(10, 10), horizontal_dirs.right)) + it('should return the position down-left of the character center when horizontal dir is right', function () + assert.are_equal(vector(12, 10 + 11), pc:_get_ground_sensor_position_from(vector(10, 10), horizontal_dirs.right)) end) it('should return the position down-left of the x-floored character center when horizontal dir is right', function () - assert.are_equal(vector(12, 10 + pc_data.center_height_standing), pc:_get_ground_sensor_position_from(vector(10.9, 10), horizontal_dirs.right)) + assert.are_equal(vector(12, 10 + 11), pc:_get_ground_sensor_position_from(vector(10.9, 10), horizontal_dirs.right)) end) end) @@ -1249,12 +1355,29 @@ describe('player_char', function () animated_sprite.play:clear() end) - it('should enter passed state: airborne and reset ground-specific state vars', function () + it('should enter passed state: falling and reset ground-specific state vars', function () -- character starts grounded - pc:_enter_motion_state(motion_states.airborne) + pc:_enter_motion_state(motion_states.falling) assert.are_same({ - motion_states.airborne, + motion_states.falling, + 0, + false + }, + { + pc.motion_state, + pc.ground_speed, + pc.should_jump + }) + assert.spy(animated_sprite.play).was_not_called() + end) + + it('should enter passed state: air_spin and reset ground-specific state vars', function () + -- character starts grounded + pc:_enter_motion_state(motion_states.air_spin) + + assert.are_same({ + motion_states.air_spin, 0, false }, @@ -1269,7 +1392,7 @@ describe('player_char', 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.motion_state = motion_states.falling pc:_enter_motion_state(motion_states.grounded) @@ -1289,6 +1412,25 @@ describe('player_char', function () assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "idle") end) + it('should adjust center position down when becoming compact', function () + pc.position = vector(10, 20) + + -- character starts grounded + pc:_enter_motion_state(motion_states.air_spin) + + assert.are_equal(20 + pc_data.center_height_standing - pc_data.center_height_compact, pc.position.y) + end) + + it('should adjust center position up when standing up', function () + pc.motion_state = motion_states.air_spin + pc.position = vector(10, 20) + + -- character starts grounded + pc:_enter_motion_state(motion_states.grounded) + + assert.are_equal(20 - pc_data.center_height_standing + pc_data.center_height_compact, pc.position.y) + end) + end) describe('_update_platformer_motion', function () @@ -1317,23 +1459,23 @@ describe('player_char', function () end) it('(when motion state is airborne) should call _check_jump', function () - pc.motion_state = motion_states.airborne + pc.motion_state = motion_states.falling -- or any airborne state pc:_update_platformer_motion() assert.spy(check_jump_stub).was_not_called() end) end) - describe('(_update_platformer_motion_grounded sets motion state to airborne)', function () + describe('(_update_platformer_motion_grounded sets motion state to air_spin)', function () local update_platformer_motion_grounded_mock local update_platformer_motion_airborne_stub setup(function () -- mock the worst case possible for _update_platformer_motion_grounded, - -- changing the state to airborne to make sure the airborne branch is not entered afterward + -- changing the state to air_spin to make sure the airborne branch is not entered afterward (else instead of 2 if blocks) update_platformer_motion_grounded_mock = stub(player_char, "_update_platformer_motion_grounded", function (self) - self.motion_state = motion_states.airborne + self.motion_state = motion_states.air_spin end) update_platformer_motion_airborne_stub = stub(player_char, "_update_platformer_motion_airborne") end) @@ -1366,7 +1508,7 @@ describe('player_char', function () describe('(when character is grounded)', function () - it('^ should call _update_platformer_motion_grounded', function () + it('should call _update_platformer_motion_grounded', function () pc.motion_state = motion_states.grounded pc:_update_platformer_motion() @@ -1378,10 +1520,10 @@ describe('player_char', function () end) - describe('(when character is airborne)', function () + describe('(when character is in air_spin)', function () - it('^ should call _update_platformer_motion_airborne', function () - pc.motion_state = motion_states.airborne + it('should call _update_platformer_motion_airborne', function () + pc.motion_state = motion_states.air_spin pc:_update_platformer_motion() @@ -1394,13 +1536,13 @@ describe('player_char', function () end) - describe('(_check_jump enters airborne motion state)', function () + describe('(_check_jump enters air_spin motion state)', function () local check_jump_mock setup(function () check_jump_mock = stub(player_char, "_check_jump", function () - pc.motion_state = motion_states.airborne + pc.motion_state = motion_states.air_spin end) end) @@ -1414,7 +1556,7 @@ describe('player_char', function () describe('(when character is grounded)', function () - it('^ should call _update_platformer_motion_airborne since _check_jump will enter airborne first', function () + it('should call _update_platformer_motion_airborne since _check_jump will enter air_spin first', function () pc.motion_state = motion_states.grounded pc:_update_platformer_motion() @@ -1426,19 +1568,8 @@ describe('player_char', function () end) - describe('(when character is airborne)', function () - - it('^ should call _update_platformer_motion_airborne', function () - pc.motion_state = motion_states.airborne - - pc:_update_platformer_motion() - - assert.spy(update_platformer_motion_airborne_stub).was_called(1) - assert.spy(update_platformer_motion_airborne_stub).was_called_with(match.ref(pc)) - assert.spy(update_platformer_motion_grounded_mock).was_not_called() - end) - - end) + -- we need to test (when character is airborne) since in this context _check_jump + -- always trigger a jump, which is impossible from the air (as double jump is not implemented) end) @@ -1547,7 +1678,7 @@ describe('player_char', function () assert.spy(enter_motion_state_stub).was_not_called() end) - it('#solo should play the run animation at playback speed = abs(ground speed), if not 0', function () + it('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() @@ -1652,12 +1783,12 @@ describe('player_char', function () 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 () + it('should call _enter_motion_state with falling 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(enter_motion_state_stub).was_called_with(match.ref(pc), motion_states.falling) assert.spy(check_jump_intention_stub).was_not_called() assert.spy(animated_sprite.play).was_not_called() end) @@ -1702,12 +1833,12 @@ 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 nor anim_spr:play (falling)', function () + it('should call _enter_motion_state with falling 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(enter_motion_state_stub).was_called_with(match.ref(pc), motion_states.falling) assert.spy(check_jump_intention_stub).was_not_called() assert.spy(animated_sprite.play).was_not_called() end) @@ -2504,6 +2635,10 @@ describe('player_char', function () mock_mset(0, 1, 64) -- full tile end) + -- in the tests below, we can use pc_data.center_height_standing directly instead + -- of pc:get_center_height() + -- because the character is not compact (e.g. no air spin) + it('when stepping left with the right sensor still on the ground, decrement x', function () local motion_result = motion.ground_motion_result( vector(-1, 8 - pc_data.center_height_standing), @@ -2879,7 +3014,7 @@ describe('player_char', function () return i == horizontal_dirs.left and vector(-1, center_position.y) or vector(1, center_position.y) end) - is_column_blocked_by_ceiling_at_mock = stub(player_char, "_is_column_blocked_by_ceiling_at", function (sensor_position) + is_column_blocked_by_ceiling_at_mock = stub(player_char, "_is_column_blocked_by_ceiling_at", function (self, sensor_position) -- simulate ceiling detection by encoding information in x and y if sensor_position.y == 1 then return sensor_position.x < 0 and false or false @@ -2918,10 +3053,20 @@ describe('player_char', function () describe('_is_column_blocked_by_ceiling_at', function () + setup(function () + stub(player_char, "get_full_height", function () + return 11 + end) + end) + + teardown(function () + player_char.get_full_height:revert() + end) + describe('(no tiles)', function () it('should return false anywhere', function () - assert.is_false(pc._is_column_blocked_by_ceiling_at(vector(4, 5))) + assert.is_false(pc:_is_column_blocked_by_ceiling_at(vector(4, 5))) end) end) @@ -2935,11 +3080,11 @@ describe('player_char', function () it('should return false for sensor position just above the bottom of the tile', function () -- here, the current tile is the full tile, and we only check tiles above, so we detect nothing - assert.is_false(pc._is_column_blocked_by_ceiling_at(vector(8, 7.9))) + assert.is_false(pc:_is_column_blocked_by_ceiling_at(vector(8, 7.9))) end) it('should return false for sensor position on the left of the tile', function () - assert.is_false(pc._is_column_blocked_by_ceiling_at(vector(7, 8))) + assert.is_false(pc:_is_column_blocked_by_ceiling_at(vector(7, 8))) end) -- bugfix history: @@ -2948,19 +3093,19 @@ describe('player_char', function () -- 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))) + assert.is_true(pc:_is_column_blocked_by_ceiling_at(vector(8, 8))) end) it('should return true for sensor position on the bottom-right of the tile', function () - assert.is_true(pc._is_column_blocked_by_ceiling_at(vector(15, 8))) + assert.is_true(pc:_is_column_blocked_by_ceiling_at(vector(15, 8))) end) it('should return false for sensor position on the right of the tile', function () - assert.is_false(pc._is_column_blocked_by_ceiling_at(vector(16, 8))) + assert.is_false(pc:_is_column_blocked_by_ceiling_at(vector(16, 8))) end) it('should return true for sensor position below the tile, at character height - 1px', function () - assert.is_true(pc._is_column_blocked_by_ceiling_at(vector(12, 8 + pc_data.full_height_standing - 1))) + assert.is_true(pc:_is_column_blocked_by_ceiling_at(vector(12, 8 + 11 - 1))) end) -- bugfix history: @@ -2969,7 +3114,7 @@ describe('player_char', function () -- 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))) + assert.is_false(pc:_is_column_blocked_by_ceiling_at(vector(12, 8 + 11))) end) end) @@ -2984,14 +3129,14 @@ describe('player_char', function () it('should return false for sensor position on the left of the tile', function () -- normally the character should step up and pass this position during the next-step pass -- and this returns false so the character won't be blocked - assert.is_false(pc._is_column_blocked_by_ceiling_at(vector(0, 7))) + assert.is_false(pc:_is_column_blocked_by_ceiling_at(vector(0, 7))) end) it('should return true for sensor position at the bottom-left of the tile', function () -- technically this is still a step up, but we consider it is the next-step method's fault -- if it didn't step up correctly so we afford to return true and block the character, -- as it makes more simple code - assert.is_true(pc._is_column_blocked_by_ceiling_at(vector(0, 8))) + assert.is_true(pc:_is_column_blocked_by_ceiling_at(vector(0, 8))) end) end) @@ -3029,7 +3174,7 @@ describe('player_char', function () local result = pc:_check_jump() -- interface - assert.are_same({true, vector(4.1, -4.25), motion_states.airborne, true}, {result, pc.velocity, pc.motion_state, pc.has_jumped_this_frame}) + assert.are_same({true, vector(4.1, -4.25), motion_states.air_spin, true}, {result, pc.velocity, pc.motion_state, pc.has_jumped_this_frame}) end) end) @@ -3045,8 +3190,8 @@ describe('player_char', function () end) before_each(function () - -- optional, just to enter airborne state and be in a meaningful state - pc:_enter_motion_state(motion_states.airborne) + -- optional, just to enter an airborne state and be in a meaningful state in this context + pc:_enter_motion_state(motion_states.falling) -- clear spy just after this instead of after_each to avoid messing the call count player_char._enter_motion_state:clear() end) @@ -3324,8 +3469,8 @@ describe('player_char', function () describe('_check_hold_jump', function () before_each(function () - -- optional, just to enter airborne state and be in a meaningful state - pc:_enter_motion_state(motion_states.airborne) + -- optional, just to enter air_spin state and be in a meaningful state in this context + pc:_enter_motion_state(motion_states.air_spin) end) it('should interrupt the jump when still possible and hold_jump_intention is false', function () @@ -3452,7 +3597,7 @@ describe('player_char', 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] + -- was using the integer max_pixel_distance instead of the float velocity:get() -- 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 = motion.air_motion_result( @@ -3702,10 +3847,19 @@ describe('player_char', function () describe('(with flat ground)', function () before_each(function () + -- X mock_mset(0, 0, 64) -- full tile end) + -- in the tests below, we can use pc_data.full/center_height_standing directly instead + -- of pc:get_full/center_height() + -- because the character is not compact (e.g. no air spin) + it('direction up into ceiling should not move, and flag is_blocked_by_ceiling', function () + -- we need an upward velocity for ceiling check if not faster on x than y + pc.velocity.x = 0 + pc.velocity.y = -3 + local motion_result = motion.air_motion_result( vector(4, 8 + pc_data.full_height_standing - pc_data.center_height_standing), false, @@ -3728,6 +3882,9 @@ describe('player_char', function () end) it('direction down into ground should not move, and flag is_landing with slope_angle', function () + pc.velocity.x = 0 + pc.velocity.y = 3 + local motion_result = motion.air_motion_result( vector(4, 0 - pc_data.center_height_standing), false, @@ -3771,7 +3928,85 @@ describe('player_char', function () ) end) - it('direction right into wall via ceiling should not move, and flag is_blocked_by_wall', function () + it('direction right into wall via ground should not move, and flag is_blocked_by_wall', function () + local motion_result = motion.air_motion_result( + vector(-3, 1 - pc_data.center_height_standing), + false, + false, + false, + nil + ) + + pc:_next_air_step(directions.right, motion_result) + + assert.are_equal(motion.air_motion_result( + vector(-3, 1 - pc_data.center_height_standing), + true, + false, + false, + nil + ), + motion_result + ) + end) + + it('direction left into wall via ceiling downward and faster on x than y should not move, and flag is_blocked_by_wall', function () + -- important + pc.velocity.x = -3 + pc.velocity.y = 2 + + local motion_result = motion.air_motion_result( + vector(11, 7 + pc_data.full_height_standing - pc_data.center_height_standing), + false, + false, + false, + nil + ) + + pc:_next_air_step(directions.left, motion_result) + + assert.are_equal(motion.air_motion_result( + vector(11, 7 + pc_data.full_height_standing - pc_data.center_height_standing), + true, + false, + false, + nil + ), + motion_result + ) + end) + + it('direction left into wall via ceiling downward and slower on x than y should 1px left without being blocked', function () + -- important + pc.velocity.x = -2 + pc.velocity.y = 3 + + local motion_result = motion.air_motion_result( + vector(11, 7 + pc_data.full_height_standing - pc_data.center_height_standing), + false, + false, + false, + nil + ) + + pc:_next_air_step(directions.left, motion_result) + + assert.are_equal(motion.air_motion_result( + vector(10, 7 + pc_data.full_height_standing - pc_data.center_height_standing), + false, + false, + false, + nil + ), + motion_result + ) + end) + + it('direction right into wall via ceiling downward and faster on x than y should not move, and flag is_blocked_by_wall', function () + -- important + pc.velocity.x = 3 + pc.velocity.y = 2 + local motion_result = motion.air_motion_result( vector(-3, 7 + pc_data.full_height_standing - pc_data.center_height_standing), false, @@ -3793,7 +4028,36 @@ describe('player_char', function () ) end) + it('direction right into wall via ceiling downward and slower on x than y should 1px right without being blocked', function () + -- important + pc.velocity.x = 2 + pc.velocity.y = 3 + + local motion_result = motion.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(motion.air_motion_result( + vector(-2, 7 + pc_data.full_height_standing - pc_data.center_height_standing), + false, + false, + false, + nil + ), + motion_result + ) + end) + it('(after landing in previous step) direction right onto new ground should move and update slope_angle', function () + pc.velocity.x = 1 + pc.velocity.y = 0 + local motion_result = motion.air_motion_result( vector(-3, 0 - pc_data.center_height_standing), false, @@ -3816,6 +4080,9 @@ describe('player_char', function () end) it('(after landing in previous step) direction left into the air should move and unset is_landing', function () + pc.velocity.x = -1 + pc.velocity.y = 0 + local motion_result = motion.air_motion_result( vector(-2, 0 - pc_data.center_height_standing), false, @@ -3864,7 +4131,7 @@ describe('player_char', function () pc:_update_debug() assert.spy(update_velocity_debug_mock).was_called(1) assert.spy(update_velocity_debug_mock).was_called_with(match.ref(pc)) - assert.are_equal(vector(1, 2) + vector(4, -3) * delta_time, pc.position) + assert.are_equal(vector(1, 2) + vector(4, -3) * delta_time60, pc.position) end) end) @@ -3896,11 +4163,11 @@ describe('player_char', function () pc.move_intention = vector(-1, 1) pc:_update_velocity_component_debug("x") assert.is_true(almost_eq_with_message( - vector(- pc.debug_move_accel * delta_time, 0), + vector(- pc.debug_move_accel * delta_time60, 0), pc.debug_velocity)) pc:_update_velocity_component_debug("y") assert.is_true(almost_eq_with_message( - vector(- pc.debug_move_accel * delta_time, pc.debug_move_accel * delta_time), + vector(- pc.debug_move_accel * delta_time60, pc.debug_move_accel * delta_time60), pc.debug_velocity)) end) @@ -3920,7 +4187,7 @@ describe('player_char', function () it('when move intention is (-1, 1), update 1 frame => at (3.867 -3.867)', function () pc.move_intention = vector(-1, 1) pc:_update_velocity_debug() - pc:move_by(pc.debug_velocity * delta_time) + pc:move_by(pc.debug_velocity * delta_time60) assert.is_true(almost_eq_with_message(vector(3.8667, -3.8667), pc.position)) end) @@ -3928,7 +4195,7 @@ describe('player_char', function () pc.move_intention = vector(-1, 1) for i=1,10 do pc:_update_velocity_debug() - pc:move_by(pc.debug_velocity * delta_time) + pc:move_by(pc.debug_velocity * delta_time60) end assert.is_true(almost_eq_with_message(vector(-2.73, 2.73), pc.position)) assert.is_true(almost_eq_with_message(vector(-60, 60), pc.debug_velocity)) -- at max speed @@ -3938,12 +4205,12 @@ describe('player_char', function () pc.move_intention = vector(-1, 1) for i=1,10 do pc:_update_velocity_debug() - pc:move_by(pc.debug_velocity * delta_time) + pc:move_by(pc.debug_velocity * delta_time60) end pc.move_intention = vector.zero() for i=1,5 do pc:_update_velocity_debug() - pc:move_by(pc.debug_velocity * delta_time) + pc:move_by(pc.debug_velocity * delta_time60) end assert.is_true(almost_eq_with_message(vector(-20, 20), pc.debug_velocity, 0.01)) end) @@ -3952,12 +4219,12 @@ describe('player_char', function () pc.move_intention = vector(-1, 1) for i=1,10 do pc:_update_velocity_debug() - pc:move_by(pc.debug_velocity * delta_time) + pc:move_by(pc.debug_velocity * delta_time60) end pc.move_intention = vector.zero() for i=1,8 do pc:_update_velocity_debug() - pc:move_by(pc.debug_velocity * delta_time) + pc:move_by(pc.debug_velocity * delta_time60) end assert.is_true(almost_eq_with_message(vector.zero(), pc.debug_velocity)) end) diff --git a/src/game/ingame/stage.lua b/src/ingame/stage_state.lua similarity index 51% rename from src/game/ingame/stage.lua rename to src/ingame/stage_state.lua index 22e27961..a63f095e 100644 --- a/src/game/ingame/stage.lua +++ b/src/ingame/stage_state.lua @@ -3,31 +3,25 @@ require("engine/core/coroutine") require("engine/core/math") require("engine/render/color") local flow = require("engine/application/flow") +local gamestate = require("engine/application/gamestate") 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 = { +local player_char = require("ingame/playercharacter") +local stage_data = require("data/stage_data") +local audio = require("resources/audio") - -- enums - substates = { - play = "play", -- playing and moving around - result = "result" -- result screen - }, +local stage_state = derived_class(gamestate) - state = nil -} - --- game state -local state = singleton(function (self) - self.type = gamestate.types.stage +stage_state.type = ':stage' - -- state vars +-- enums +stage_state.substates = { + play = "play", -- playing and moving around + result = "result" -- result screen +} - -- current coroutines - self.coroutine_curries = {} +function stage_state:_init() + gamestate._init(self) -- stage id self.curr_stage_id = 1 @@ -36,7 +30,7 @@ local state = singleton(function (self) self.curr_stage_data = stage_data.for_stage[self.curr_stage_id] -- substate - self.current_substate = stage.substates.play + self.current_substate = stage_state.substates.play -- player character self.player_char = nil @@ -47,22 +41,22 @@ local state = singleton(function (self) -- title overlay self.title_overlay = ui.overlay(0) -end) -stage.state = state +end -function state:on_enter() - self.current_substate = stage.substates.play +function stage_state:on_enter() + self.current_substate = stage_state.substates.play self:spawn_player_char() self.has_reached_goal = false self.camera_pos = vector.zero() - self:start_coroutine_method(self.show_stage_title_async) + self.app:start_coroutine(self.show_stage_title_async, self) self:play_bgm() end -function state:on_exit() - -- clear all coroutines - clear_table(self.coroutine_curries) +function stage_state:on_exit() + -- clear all coroutines (we normally let app handle them, but in this context + -- we know that all coroutines belong to the stage state, so no risk clearing them from here) + self.app:stop_all_coroutines() -- clear object state vars self.player_char = nil @@ -75,10 +69,8 @@ function state:on_exit() self:stop_bgm() end -function state:update() - self:update_coroutines() - - if self.current_substate == stage.substates.play then +function stage_state:update() + if self.current_substate == stage_state.substates.play then self.player_char:update() self:check_reached_goal() self:update_camera() @@ -87,7 +79,7 @@ function state:update() end end -function state:render() +function stage_state:render() camera() self:render_background() @@ -96,50 +88,10 @@ function state:render() end --- coroutines - --- create and register coroutine with optional arguments -function state:start_coroutine(async_function, ...) - coroutine = cocreate(async_function) - add(self.coroutine_curries, coroutine_curry(coroutine, ...)) -end - --- variant for methods that apply self argument automatically -function state:start_coroutine_method(async_function, ...) - self:start_coroutine(async_function, self, ...) -end - --- update emit coroutine if active, remove if dead -function state:update_coroutines() - local coroutine_curries_to_del = {} - for i, coroutine_curry in pairs(self.coroutine_curries) do - local status = costatus(coroutine_curry.coroutine) - if status == "suspended" then - -- resume the coroutine and assert if failed - -- (assertions don't work from inside coroutines, but will return false) - -- pass the curry arguments now (most of the time they are only useful - -- on the 1st coresume call, since other times they are just yield() return values) - local result = coresume(coroutine_curry.coroutine, unpack(coroutine_curry.args)) - assert(result, "Assertion failed in coroutine update for: "..coroutine_curry) - elseif status == "dead" then - -- register the coroutine for removal from the sequence (don't delete it now since we are iterating over it) - -- note that this block is only entered on the frame after the last coresume - add(coroutine_curries_to_del, coroutine_curry) - else -- status == "running" - warn("stage.state:update_coroutines: coroutine should not be running outside its body: "..coroutine_curry, "flow") - end - end - -- delete dead coroutines - for coroutine_curry in all(coroutine_curries_to_del) do - del(self.coroutine_curries, coroutine_curry) - end -end - - -- setup -- spawn the player character at the stage spawn location -function state:spawn_player_char() +function stage_state:spawn_player_char() local spawn_position = self.curr_stage_data.spawn_location:to_center_position() self.player_char = player_char() self.player_char:spawn_at(spawn_position) @@ -148,42 +100,42 @@ end -- gameplay events -function state:check_reached_goal() +function stage_state:check_reached_goal() if not self.has_reached_goal and self.player_char.position.x >= self.curr_stage_data.goal_x then self.has_reached_goal = true - self:start_coroutine_method(self.on_reached_goal_async) + self.app:start_coroutine(self.on_reached_goal_async, self) end end -function state:on_reached_goal_async() +function stage_state:on_reached_goal_async() self:feedback_reached_goal() - self.current_substate = stage.substates.result + self.current_substate = stage_state.substates.result self:stop_bgm(stage_data.bgm_fade_out_duration) - yield_delay(stage_data.back_to_titlemenu_delay) + self.app:yield_delay_s(stage_data.back_to_titlemenu_delay) self:back_to_titlemenu() end -function state:feedback_reached_goal() +function stage_state:feedback_reached_goal() sfx(audio.sfx_ids.goal_reached) end -function state:back_to_titlemenu() - flow:query_gamestate_type(gamestate.types.titlemenu) +function stage_state:back_to_titlemenu() + flow:query_gamestate_type(':titlemenu') end -- camera -- update camera position based on player character position -function state:update_camera() +function stage_state:update_camera() -- stiff motion self.camera_pos.x = self.player_char.position.x self.camera_pos.y = self.player_char.position.y end -- set the camera offset for stage elements -function state:set_camera_offset_stage() +function stage_state:set_camera_offset_stage() -- the camera position is used to render the stage. it represents the screen center -- whereas pico-8 defines a top-left camera position, so we subtract a half screen to center the view camera(self.camera_pos.x - screen_width / 2, self.camera_pos.y - screen_height / 2) @@ -192,9 +144,9 @@ end -- ui -function state:show_stage_title_async() +function stage_state:show_stage_title_async() self.title_overlay:add_label("title", self.curr_stage_data.title, vector(50, 30), colors.white) - yield_delay(stage_data.show_stage_title_delay) + self.app:yield_delay_s(stage_data.show_stage_title_delay) self.title_overlay:remove_label("title") end @@ -202,7 +154,7 @@ end -- render -- render the stage background -function state:render_background() +function stage_state:render_background() camera() rectfill(0, 0, 127, 127, self.curr_stage_data.background_color) end @@ -210,14 +162,14 @@ end -- render the stage elements with the main camera: -- - environment -- - player character -function state:render_stage_elements() +function stage_state:render_stage_elements() self:set_camera_offset_stage() self:render_environment() self:render_player_char() end -- render the stage environment (tiles) -function state:render_environment() +function stage_state:render_environment() -- optimize: don't draw the whole stage offset by camera, -- instead just draw the portion of the level of interest -- (and either keep camera offset or offset manually and subtract from camera offset) @@ -229,12 +181,12 @@ function state:render_environment() end -- render the player character at its current position -function state:render_player_char() +function stage_state:render_player_char() self.player_char:render() end -- render the title overlay with a fixed ui camera -function state:render_title_overlay() +function stage_state:render_title_overlay() camera(0, 0) self.title_overlay:draw_labels() end @@ -242,11 +194,11 @@ end -- audio -function state:play_bgm() +function stage_state:play_bgm() music(self.curr_stage_data.bgm_id, 0) end -function state:stop_bgm(fade_duration) +function stage_state:stop_bgm(fade_duration) -- convert duration from seconds to milliseconds if fade_duration then fade_duration_ms = 1000 * fade_duration @@ -259,4 +211,4 @@ end -- export -return stage +return stage_state diff --git a/src/ingame/stage_state_utest.lua b/src/ingame/stage_state_utest.lua new file mode 100644 index 00000000..f22f6896 --- /dev/null +++ b/src/ingame/stage_state_utest.lua @@ -0,0 +1,655 @@ +require("engine/test/bustedhelper") +local stage_state = require("ingame/stage_state") + +local flow = require("engine/application/flow") +local gamestate = require("engine/application/gamestate") +local ui = require("engine/ui/ui") + +local picosonic_app = require("application/picosonic_app") +local stage_data = require("data/stage_data") +local player_char = require("ingame/playercharacter") +local titlemenu = require("menu/titlemenu") +local audio = require("resources/audio") + +describe('stage_state', function () + + describe('static members', function () + + it('type is ":stage"', function () + assert.are_equal(':stage', stage_state.type) + end) + + end) + + describe('(with instance)', function () + + local state + + before_each(function () + local app = picosonic_app() + state = stage_state() + -- no need to register gamestate properly, just add app member to pass tests + state.app = app + end) + + describe('state', function () + + -- bugfix history: . + it('init', function () + assert.are_same({ + ':stage', + 1, + stage_state.substates.play, + nil, + false, + vector.zero(), + ui.overlay(0) + }, + { + state.type, + state.curr_stage_id, + state.current_substate, + state.player_char, + state.has_reached_goal, + state.camera_pos, + state.title_overlay + }) + end) + + describe('on_enter', function () + + setup(function () + stub(stage_state, "spawn_player_char") + stub(picosonic_app, "start_coroutine") + stub(stage_state, "play_bgm") + end) + + teardown(function () + stage_state.spawn_player_char:revert() + picosonic_app.start_coroutine:revert() + stage_state.play_bgm:revert() + end) + + after_each(function () + stage_state.spawn_player_char:clear() + picosonic_app.start_coroutine:clear() + stage_state.play_bgm:clear() + end) + + before_each(function () + state:on_enter() + end) + + it('should enter the play substates', function () + assert.are_equal(stage_state.substates.play, state.current_substate) + end) + + it('should call spawn_player_char', function () + local s = assert.spy(stage_state.spawn_player_char) + s.was_called(1) + s.was_called_with(match.ref(state)) + end) + + it('should set has_reached_goal to false', function () + assert.is_false(state.has_reached_goal) + end) + + it('should initialize camera at origin', function () + assert.are_equal(vector:zero(), state.camera_pos) + end) + + it('should call start_coroutine_method on show_stage_title_async', function () + local s = assert.spy(picosonic_app.start_coroutine) + s.was_called(1) + s.was_called_with(match.ref(state.app), stage_state.show_stage_title_async, match.ref(state)) + end) + + it('should call start_coroutine_method on show_stage_title_async', function () + assert.spy(state.play_bgm).was_called(1) + assert.spy(state.play_bgm).was_called_with(match.ref(state)) + end) + + end) + + describe('on_exit', function () + + setup(function () + stub(ui.overlay, "clear_labels") + stub(picosonic_app, "stop_all_coroutines") + stub(stage_state, "stop_bgm") + end) + + teardown(function () + ui.overlay.clear_labels:revert() + picosonic_app.stop_all_coroutines:revert() + stage_state.stop_bgm:revert() + end) + + after_each(function () + ui.overlay.clear_labels:clear() + picosonic_app.stop_all_coroutines:clear() + stage_state.stop_bgm:clear() + end) + + before_each(function () + -- enter first, so we can check if on_exit cleans state correctly + state:on_enter() + state:on_exit() + end) + + it('should stop all the coroutines', function () + local s = assert.spy(picosonic_app.stop_all_coroutines) + s.was_called(1) + s.was_called_with(match.ref(state.app)) + end) + + it('should clear the player character', function () + assert.is_nil(state.player_char) + end) + + it('should call title_overlay:clear_labels', function () + local s = assert.spy(ui.overlay.clear_labels) + s.was_called(1) + s.was_called_with(match.ref(state.title_overlay)) + end) + + it('should reset pico8 camera', function () + assert.are_same({0, 0}, {pico8.camera_x, pico8.camera_y}) + end) + + it('should call stop_bgm', function () + assert.spy(stage_state.stop_bgm).was_called(1) + assert.spy(stage_state.stop_bgm).was_called_with(match.ref(state)) + end) + + end) + + describe('spawn_player_char', function () + + setup(function () + spy.on(player_char, "spawn_at") + end) + + teardown(function () + player_char.spawn_at:revert() + end) + + it('should spawn the player character at the stage spawn location', function () + state:spawn_player_char() + local player_char = state.player_char + assert.is_not_nil(player_char) + local spawn_position = state.curr_stage_data.spawn_location:to_center_position() + + -- interface + assert.are_equal(spawn_position, player_char.position) + -- we haven't initialized any map in busted, so the character is falling in the air and spawn_at detected this + assert.are_equal(motion_states.falling, player_char.motion_state) + + -- implementation + assert.spy(player_char.spawn_at).was_called(1) + assert.spy(player_char.spawn_at).was_called_with(match.ref(state.player_char), spawn_position) + end) + + end) + + describe('(stage states added)', function () + + before_each(function () + flow:add_gamestate(state) + flow:add_gamestate(titlemenu) -- for transition on reached goal + end) + + after_each(function () + flow:init() + end) + + describe('(stage state entered)', function () + + before_each(function () + flow:_change_state(state) + -- entering stage currently starts coroutine show_stage_title_async + -- which will cause side effects when updating coroutines to test other + -- async functions, so clear that now + state.app:stop_all_coroutines() + end) + + describe('update_camera', function () + + before_each(function () + state.player_char.position = vector(12, 24) + end) + + it('should move the camera to player position', function () + state:update_camera() + assert.are_equal(vector(12, 24), state.camera_pos) + end) + + end) + + describe('update', function () + + setup(function () + stub(player_char, "update") + stub(stage_state, "check_reached_goal") + stub(stage_state, "update_camera") + end) + + teardown(function () + player_char.update:revert() + stage_state.check_reached_goal:revert() + stage_state.update_camera:revert() + end) + + after_each(function () + player_char.update:clear() + stage_state.check_reached_goal:clear() + stage_state.update_camera:clear() + end) + + describe('(current substate is play)', function () + + it('should call player_char:update, check_reached_goal and update_camera', function () + state.current_substate = stage_state.substates.play + state:update() + assert.spy(player_char.update).was_called(1) + assert.spy(player_char.update).was_called_with(match.ref(state.player_char)) + assert.spy(stage_state.check_reached_goal).was_called(1) + assert.spy(stage_state.check_reached_goal).was_called_with(match.ref(state)) + assert.spy(stage_state.update_camera).was_called(1) + assert.spy(stage_state.update_camera).was_called_with(match.ref(state)) end) + end) + + describe('(current substate is result)', function () + + it('should call player_char:update, check_reached_goal and update_camera', function () + state.current_substate = stage_state.substates.result + state:update() + assert.spy(player_char.update).was_not_called() + assert.spy(stage_state.check_reached_goal).was_not_called() + assert.spy(stage_state.update_camera).was_not_called() + end) + + end) + + end) -- update + + describe('render', function () + + setup(function () + stub(stage_state, "render_background") + stub(stage_state, "render_stage_elements") + stub(stage_state, "render_title_overlay") + end) + + teardown(function () + stage_state.render_background:revert() + stage_state.render_stage_elements:revert() + stage_state.render_title_overlay:revert() + end) + + after_each(function () + stage_state.render_background:clear() + stage_state.render_stage_elements:clear() + stage_state.render_title_overlay:clear() + end) + + it('should reset camera, call render_background, render_stage_elements, render_title_overlay', function () + state:render() + assert.are_same({0, 0}, {pico8.camera_x, pico8.camera_y}) + assert.spy(stage_state.render_background).was_called(1) + assert.spy(stage_state.render_background).was_called_with(match.ref(state)) + assert.spy(stage_state.render_stage_elements).was_called(1) + assert.spy(stage_state.render_stage_elements).was_called_with(match.ref(state)) + assert.spy(stage_state.render_title_overlay).was_called(1) + assert.spy(stage_state.render_title_overlay).was_called_with(match.ref(state)) + end) + + end) -- state.render + + describe('check_reached_goal', function () + + setup(function () + stub(picosonic_app, "start_coroutine") + end) + + teardown(function () + picosonic_app.start_coroutine:revert() + end) + + -- start_coroutine is also called on stage enter (with show_stage_title_async) + -- so we must clear call count *before* the first test + before_each(function () + picosonic_app.start_coroutine:clear() + end) + + describe('(before the goal)', function () + + -- should be each + before_each(function () + state.player_char.position = vector(state.curr_stage_data.goal_x - 1, 0) + state:check_reached_goal() + end) + + it('should not set has_reached_goal to true', function () + assert.is_false(state.has_reached_goal) + end) + + it('should not start on_reached_goal_async', function () + local s = assert.spy(picosonic_app.start_coroutine) + s.was_not_called() + end) + + end) + + describe('(just on the goal)', function () + + before_each(function () + state.player_char.position = vector(state.curr_stage_data.goal_x, 0) + state:check_reached_goal() + end) + + it('should set has_reached_goal to true', function () + assert.is_true(state.has_reached_goal) + end) + + it('should start on_reached_goal_async', function () + local s = assert.spy(picosonic_app.start_coroutine) + s.was_called(1) + s.was_called_with(match.ref(state.app), stage_state.on_reached_goal_async, match.ref(state)) + end) + + end) + + describe('(after the goal)', function () + + before_each(function () + state.player_char.position = vector(state.curr_stage_data.goal_x + 1, 0) + state:check_reached_goal() + end) + + it('should set has_reached_goal to true', function () + assert.is_true(state.has_reached_goal) + end) + + it('should start on_reached_goal_async', function () + local s = assert.spy(picosonic_app.start_coroutine) + s.was_called(1) + s.was_called_with(match.ref(state.app), stage_state.on_reached_goal_async, match.ref(state)) + end) + + end) + + end) + + describe('state.on_reached_goal_async', function () + + local on_reached_goal_async_coroutine + + setup(function () + stub(stage_state, "back_to_titlemenu") + end) + + teardown(function () + stage_state.back_to_titlemenu:revert() + end) + + after_each(function () + stage_state.back_to_titlemenu:clear() + end) + + before_each(function () + on_reached_goal_async_coroutine = cocreate(state.on_reached_goal_async) + end) + + it('should set substate to result after 1 update', function () + -- update coroutines once to advance on_reached_goal_async + coresume(on_reached_goal_async_coroutine, state) + assert.are_equal(stage_state.substates.result, state.current_substate) + end) + + -- this test is a bit extra, as it checks yield_delay_s's own validity + -- however, it's useful to check that yield is done correctly (e.g. pass frames vs sec) + -- and luassert spies are not good are identifying exact call order, so checking + -- yield call itself is not too useful + it('should query gamestate ":titlemenu" not earlier than after 1.0s', function () + for i = 1, stage_data.back_to_titlemenu_delay * state.app.fps - 1 do + coresume(on_reached_goal_async_coroutine, state) + end + + assert.spy(stage_state.back_to_titlemenu).was_not_called() + end) + + it('should query gamestate ":titlemenu" after 1.0s', function () + -- hold back 1 frame to make sure function will be called exactly next frame + for i = 1, stage_data.back_to_titlemenu_delay * state.app.fps - 1 do + coresume(on_reached_goal_async_coroutine, state) + end + + -- not called yet + assert.spy(stage_state.back_to_titlemenu).was_not_called() + + coresume(on_reached_goal_async_coroutine, state) + + -- just called + assert.spy(stage_state.back_to_titlemenu).was_called(1) + assert.spy(stage_state.back_to_titlemenu).was_called_with(match.ref(state)) + end) + + end) + + describe('state.feedback_reached_goal', function () + local sfx_stub + + setup(function () + sfx_stub = stub(_G, "sfx") + end) + + teardown(function () + sfx_stub:revert() + end) + + after_each(function () + sfx_stub:clear() + end) + + it('should play goal_reached sfx', function () + state:feedback_reached_goal() + assert.spy(sfx_stub).was_called(1) + assert.spy(sfx_stub).was_called_with(audio.sfx_ids.goal_reached) + end) + + end) + + describe('back_to_titlemenu', function () + + it('should change gamestate to titlemenu on next update', function () + state:back_to_titlemenu() + flow:update() + assert.are_equal(':titlemenu', flow.curr_state.type) + end) + + end) + + describe('(no overlay labels)', function () + + local on_show_stage_title_async + + before_each(function () + on_show_stage_title_async = cocreate(state.show_stage_title_async) + end) + + after_each(function () + -- we don't stub overlay.add_label here, so we must clear any side effects + clear_table(state.title_overlay.labels) + end) + + it('show_stage_title_async should add a title label and remove it after stage_data.show_stage_title_delay seconds', function () + -- hold back last frame to check that label was added and didn't disappear yet + for i = 1, stage_data.show_stage_title_delay * state.app.fps - 1 do + coresume(on_show_stage_title_async, state) + end + assert.are_equal(ui.label(state.curr_stage_data.title, vector(50, 30), colors.white), state.title_overlay.labels["title"]) + + -- reach last frame now to check if label just disappeared + coresume(on_show_stage_title_async, state) + + assert.is_nil(state.title_overlay.labels["title"]) + end) + + end) + + describe('state render methods', function () + + local map_stub + local player_char_render_stub + + setup(function () + rectfill_stub = stub(_G, "rectfill") + map_stub = stub(_G, "map") + spy.on(stage_state, "render_environment") + player_char_render_stub = stub(player_char, "render") + title_overlay_draw_labels_stub = stub(ui.overlay, "draw_labels") + end) + + teardown(function () + rectfill_stub:revert() + map_stub:revert() + state.render_environment:revert() + player_char_render_stub:revert() + title_overlay_draw_labels_stub:revert() + end) + + after_each(function () + rectfill_stub:clear() + map_stub:clear() + state.render_environment:clear() + player_char_render_stub:clear() + title_overlay_draw_labels_stub:clear() + end) + + it('render_title_overlay should call title_overlay:draw_labels', function () + state:render_title_overlay() + assert.are_equal(vector.zero(), vector(pico8.camera_x, pico8.camera_y)) + assert.spy(title_overlay_draw_labels_stub).was_called(1) + assert.spy(title_overlay_draw_labels_stub).was_called_with(state.title_overlay) + end) + + it('render_background should reset camera position, call rectfill on the whole screen with stage background color', function () + state.camera_pos = vector(24, 13) + state:render_background() + assert.are_equal(vector(0, 0), vector(pico8.camera_x, pico8.camera_y)) + assert.spy(rectfill_stub).was_called(1) + assert.spy(rectfill_stub).was_called_with(0, 0, 127, 127, state.curr_stage_data.background_color) + end) + + it('render_stage_elements should set camera position, call map for environment and player_char:render', function () + state.camera_pos = vector(24, 13) + state:render_stage_elements() + assert.are_equal(vector(24 - 128 / 2, 13 - 128 / 2), vector(pico8.camera_x, pico8.camera_y)) + assert.spy(state.render_environment).was_called(1) + assert.spy(state.render_environment).was_called_with(match.ref(state)) + assert.spy(player_char_render_stub).was_called(1) + assert.spy(player_char_render_stub).was_called_with(match.ref(state.player_char)) + end) + + it('set_camera_offset_stage should set the pico8 camera so that it is centered on the camera position', function () + state.camera_pos = vector(24, 13) + state:set_camera_offset_stage() + assert.are_equal(vector(24 - 128 / 2, 13 - 128 / 2), vector(pico8.camera_x, pico8.camera_y)) + end) + + describe('(after set_camera_offset_stage)', function () + + before_each(function () + state:set_camera_offset_stage() + end) + + it('render_environment should call map', function () + state:render_environment() + assert.spy(map_stub).was_called(1) + assert.spy(map_stub).was_called_with(0, 0, 0, 0, state.curr_stage_data.width, state.curr_stage_data.height) + end) + + it('render_player_char should call player_char:render', function () + state:render_player_char() + assert.spy(player_char_render_stub).was_called(1) + assert.spy(player_char_render_stub).was_called_with(match.ref(state.player_char)) + end) + + end) + + end) -- state render methods + + describe('state audio methods', function () + + after_each(function () + pico8.current_music = nil + end) + + it('play_bgm should start level bgm', function () + state:play_bgm() + assert.are_same({music=audio.music_pattern_ids.green_hill, fadems=0, channel_mask=0}, pico8.current_music) + end) + + it('stop_bgm should stop level bgm if started, else do nothing', function () + state:stop_bgm() + assert.is_nil(pico8.current_music) + state:play_bgm() + state:stop_bgm() + assert.is_nil(pico8.current_music) + state:play_bgm() + state:stop_bgm(2.0) + assert.is_nil(pico8.current_music) + end) + + end) -- state audio methods + + describe('on exit stage state to enter titlemenu state', function () + + before_each(function () + flow:_change_state(titlemenu) + end) + + it('player character should be nil', function () + assert.is_nil(state.player_char) + end) + + it('title overlay should be empty', function () + assert.is_not_nil(state.title_overlay) + assert.is_not_nil(state.title_overlay.labels) + assert.is_true(is_empty(state.title_overlay.labels)) + end) + + describe('reenter stage state', function () + + -- should be each + before_each(function () + flow:_change_state(state) + end) + + it('current substate should be play', function () + assert.are_equal(stage_state.substates.play, state.current_substate) + end) + + it('player character should not be nil and respawned at the spawn location', function () + assert.is_not_nil(state.player_char) + assert.are_equal(state.curr_stage_data.spawn_location:to_center_position(), state.player_char.position) + end) + + it('should not have reached goal', function () + assert.is_false(state.has_reached_goal) + end) + + end) + + end) -- on exit stage state to enter titlemenu state + + end) -- (enter stage state) + + end) -- (stage states added) + + end) -- (with instance) + + end) -- (stage state) + +end) diff --git a/src/game/itests/itest_dsl.lua b/src/itest/itest_dsl.lua similarity index 89% rename from src/game/itests/itest_dsl.lua rename to src/itest/itest_dsl.lua index b30d37d5..1917e999 100644 --- a/src/game/itests/itest_dsl.lua +++ b/src/itest/itest_dsl.lua @@ -32,15 +32,27 @@ local integrationtest = require("engine/test/integrationtest") local itest_manager, integration_test = get_members(integrationtest, "itest_manager", "integration_test") -local tile_data = require("game/data/tile_data") +local tile_data = require("data/tile_data") local tilemap = require("engine/data/tilemap") -- dsl interpretation requirements local flow = require("engine/application/flow") 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") +local player_char = require("ingame/playercharacter") +local pc_data = require("data/playercharacter_data") + + +-- helper function to access stage_stage quickly if current state +-- is stage, as it is not a singleton anymore +local function get_current_state_as_stage() + if flow.curr_state then + if flow.curr_state.type == ':stage' then + return flow.curr_state + end + assert(false, "current state is "..flow.curr_state.type..", expected ':stage'") + end + assert(false, "current state is nil, expected stage_state") +end -- module @@ -76,7 +88,8 @@ local function generate_function_table(module, enum_types, prefix) end return t end ---#if utest +--#if busted +-- allow access to function for utest itest_dsl.generate_function_table = generate_function_table --#endif @@ -239,7 +252,8 @@ itest_dsl.value_parsers = value_parsers -- so they can update its state if needed function itest_dsl.execute_warp(args) - stage.state.player_char:warp_bottom_to(args[1]) + local current_stage_state = get_current_state_as_stage() + current_stage_state.player_char:warp_bottom_to(args[1]) end function itest_dsl.execute_set(args) @@ -251,28 +265,34 @@ function itest_dsl.execute_set(args) end function itest_dsl.execute_set_control_mode(args) - stage.state.player_char.control_mode = args[1] + local current_stage_state = get_current_state_as_stage() + current_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] + local current_stage_state = get_current_state_as_stage() + current_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]] + local current_stage_state = get_current_state_as_stage() + current_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() + local current_stage_state = get_current_state_as_stage() + current_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 + local current_stage_state = get_current_state_as_stage() + current_stage_state.player_char.jump_intention = true -- will be consumed + current_stage_state.player_char.hold_jump_intention = true end function itest_dsl.execute_stop_jump(args) - stage.state.player_char.hold_jump_intention = false + local current_stage_state = get_current_state_as_stage() + current_stage_state.player_char.hold_jump_intention = false end function itest_dsl.execute_press(args) @@ -295,23 +315,28 @@ itest_dsl.executors = executors -- gameplay value evaluation functions function itest_dsl.eval_pc_bottom_pos() - return stage.state.player_char:get_bottom_center() + local current_stage_state = get_current_state_as_stage() + return current_stage_state.player_char:get_bottom_center() end function itest_dsl.eval_pc_velocity() - return stage.state.player_char.velocity + local current_stage_state = get_current_state_as_stage() + return current_stage_state.player_char.velocity end function itest_dsl.eval_pc_ground_spd() - return stage.state.player_char.ground_speed + local current_stage_state = get_current_state_as_stage() + return current_stage_state.player_char.ground_speed end function itest_dsl.eval_pc_motion_state() - return stage.state.player_char.motion_state + local current_stage_state = get_current_state_as_stage() + return current_stage_state.player_char.motion_state end function itest_dsl.eval_pc_slope() - return stage.state.player_char.slope_angle + local current_stage_state = get_current_state_as_stage() + return current_stage_state.player_char.slope_angle end -- table of functions used to evaluate and returns the gameplay value in current game state @@ -322,11 +347,13 @@ 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 + local current_stage_state = get_current_state_as_stage() + current_stage_state.player_char.velocity = value end function itest_dsl.set_pc_ground_spd(value) - stage.state.player_char.ground_speed = value + local current_stage_state = get_current_state_as_stage() + current_stage_state.player_char.ground_speed = value end @@ -362,8 +389,8 @@ end -- 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 --- tilemap tilemap|nil tilemap data if gamestate type is 'stage', nil else +-- 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 {command} sequence of commands to apply local dsl_itest = new_struct() itest_dsl.dsl_itest = dsl_itest @@ -398,7 +425,7 @@ end -- 2. action sequence and expectations -- ex: -- [[ --- @stage # < gamestate 'stage' with tag '#' for custom +-- @stage # < gamestate ':stage' with tag '#' for custom -- ... < for custom stage, provide the tilemap in ascii -- ### < . for empty tile, # for full tile, etc. -- < blank after tilemap to mark the end @@ -424,7 +451,7 @@ end -- return gamestate type, stage_name, tilemap data and index of next line to parse so we can chain parsing -- the format of the gamestate definition is: --- @[gamestate] (stage_name|#)? < 2nd part only if gamestate == 'stage', '#' for custom tilemap +-- @[gamestate] (stage_name|#)? < 2nd part only if gamestate == ':stage', '#' for custom tilemap -- [tilemap row 1] < only for custom tilemap -- ... -- [tilemap row n] @@ -435,9 +462,9 @@ function itest_dsl_parser.parse_gamestate_definition(lines) local gamestate_header = lines[1] assert(sub(gamestate_header, 1, 1) == '@', "gamestate_header '"..gamestate_header.."' doesn't start with @") local header_parts = strspl(gamestate_header, ' ', true) - local gamestate_type = sub(header_parts[1], 2) + local gamestate_type = ':'..sub(header_parts[1], 2) local stage_name = nil - if gamestate_type == 'stage' then + if gamestate_type == ':stage' then assert(#header_parts == 2) stage_name = header_parts[2] end @@ -529,11 +556,12 @@ end function itest_dsl_parser.create_itest(name, dsli) itest_dsl_parser._itest = integration_test(name, {dsli.gamestate_type}) - itest_dsl_parser._itest.setup = function () + itest_dsl_parser._itest.setup = function (app) flow:change_gamestate_by_type(dsli.gamestate_type) - if dsli.gamestate_type == "stage" then + if dsli.gamestate_type == ':stage' then -- puppet control - stage.state.player_char.control_mode = control_modes.puppet + local current_stage_state = get_current_state_as_stage() + current_stage_state.player_char.control_mode = control_modes.puppet if dsli.stage_name == '#' then -- load tilemap data and build it from ascii setup_map_data() @@ -547,7 +575,7 @@ function itest_dsl_parser.create_itest(name, dsli) itest_dsl_parser._itest.teardown = function () -- 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.gamestate_type == ':stage' then if dsli.stage_name == '#' then -- clear tilemap and unload tilemap data tilemap.clear_map() @@ -647,7 +675,7 @@ function itest_dsl_parser:_define_final_assertion() -- 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 +--#if busted -- 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 diff --git a/src/game/tests/utestitest_dsl.lua b/src/itest/itest_dsl_utest.lua similarity index 86% rename from src/game/tests/utestitest_dsl.lua rename to src/itest/itest_dsl_utest.lua index 64b590ca..82199490 100644 --- a/src/game/tests/utestitest_dsl.lua +++ b/src/itest/itest_dsl_utest.lua @@ -1,7 +1,14 @@ require("engine/test/bustedhelper") require("engine/core/helper") require("engine/core/math") -local itest_dsl = require("game/itests/itest_dsl") +local flow = require("engine/application/flow") +local tilemap = require("engine/data/tilemap") +local input = require("engine/input/input") +local integrationtest = require("engine/test/integrationtest") +local itest_manager, time_trigger, integration_test = get_members(integrationtest, + "itest_manager", "time_trigger", "integration_test") + +local itest_dsl = require("itest/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, @@ -10,21 +17,26 @@ 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 flow = require("engine/application/flow") -local input = require("engine/input/input") -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") -local pc_data = require("game/data/playercharacter_data") +local stage_state = require("ingame/stage_state") + +local picosonic_app = require("application/picosonic_app") +local player_char = require("ingame/playercharacter") +local pc_data = require("data/playercharacter_data") describe('itest_dsl', function () + local state + + before_each(function () + local app = picosonic_app() + state = stage_state() + state.app = app + + -- some executions require the player character + state.player_char = player_char() + end) + describe('gameplay_value_data', function () describe('_init', function () @@ -160,7 +172,7 @@ describe('itest_dsl', function () end) it('should return the single argument as motion state', function () - assert.are_equal(motion_states.airborne, itest_dsl.parse_motion_state({"airborne"})) + assert.are_equal(motion_states.falling, itest_dsl.parse_motion_state({"falling"})) end) end) @@ -185,13 +197,12 @@ describe('itest_dsl', function () describe('execute_', function () before_each(function () - -- some executions require the player character - stage.state.player_char = player_char() + flow:add_gamestate(state) + flow:change_gamestate_by_type(':stage') end) after_each(function () - -- clean up dummy player character - stage.state:init() + flow:init() end) describe('execute_warp', function () @@ -208,19 +219,19 @@ describe('itest_dsl', function () 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)) + assert.spy(player_char.warp_bottom_to).was_called_with(match.ref(state.player_char), vector(1, 3)) end) end) describe('"execute_set', function () - it('#solo should set pc velocity to (1, -3)', function () + it('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) + assert.are_equal(vector(1, -3), state.player_char.velocity) end) - it('#solo should fail with unsupported gp_value_type for setting', function () + it('should fail with unsupported gp_value_type for setting', function () assert.has_error(function () itest_dsl.execute_set({"pc_slope", -2}) end, "itest_dsl.set_pc_slope is not defined") @@ -232,7 +243,7 @@ describe('itest_dsl', 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) + assert.are_equal(control_modes.puppet, state.player_char.control_mode) end) end) @@ -241,7 +252,7 @@ describe('itest_dsl', 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) + assert.are_equal(motion_modes.debug, state.player_char.motion_mode) end) end) @@ -250,7 +261,7 @@ describe('itest_dsl', function () it('should set the move intention of the current player character to the directional unit vector matching his horizontal direction', function () itest_dsl.execute_move({horizontal_dirs.right}) - assert.are_equal(vector(1, 0), stage.state.player_char.move_intention) + assert.are_equal(vector(1, 0), state.player_char.move_intention) end) end) @@ -258,9 +269,9 @@ 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) + state.player_char.move_intention = vector(99, -99) itest_dsl.execute_stop({}) - assert.are_equal(vector.zero(), stage.state.player_char.move_intention) + assert.are_equal(vector.zero(), state.player_char.move_intention) end) end) @@ -270,7 +281,7 @@ describe('itest_dsl', 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}) + {state.player_char.jump_intention, state.player_char.hold_jump_intention}) end) end) @@ -278,9 +289,9 @@ describe('itest_dsl', function () describe('execute_stop_jump', function () it('should set the hold jump intention to false', function () - stage.state.player_char.hold_jump_intention = true + state.player_char.hold_jump_intention = true itest_dsl.execute_stop_jump({}) - assert.is_false(stage.state.player_char.hold_jump_intention) + assert.is_false(state.player_char.hold_jump_intention) end) end) @@ -310,19 +321,14 @@ describe('itest_dsl', function () 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() + flow:add_gamestate(state) + flow:change_gamestate_by_type(':stage') end) describe('eval_pc_bottom_pos', function () it('should return the bottom position of the current player character', function () - stage.state.player_char:set_bottom_center(vector(12, 47)) + state.player_char:set_bottom_center(vector(12, 47)) assert.are_equal(vector(12, 47), eval_pc_bottom_pos()) end) @@ -330,8 +336,8 @@ describe('itest_dsl', function () describe('eval_pc_velocity', function () - it('should return the velocity the current player character', function () - stage.state.player_char.velocity = vector(1, -4) + it('should return the velocity of the current player character', function () + state.player_char.velocity = vector(1, -4) assert.are_equal(vector(1, -4), eval_pc_velocity()) end) @@ -339,8 +345,8 @@ describe('itest_dsl', function () describe('eval_pc_ground_spd', function () - it('should return the ground speed current player character', function () - stage.state.player_char.ground_speed = 3.5 + it('should return the ground speed of the current player character', function () + state.player_char.ground_speed = 3.5 assert.are_equal(3.5, eval_pc_ground_spd()) end) @@ -348,17 +354,17 @@ describe('itest_dsl', function () 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()) + it('should return the motion state of the current player character', function () + state.player_char.motion_state = motion_states.air_spin + assert.are_equal(motion_states.air_spin, eval_pc_motion_state()) end) end) describe('eval_pc_slope', function () - it('should return the ground speed current player character', function () - stage.state.player_char.slope_angle = -0.125 + it('should return the slope angle of the current player character', function () + state.player_char.slope_angle = -0.125 assert.are_equal(-0.125, eval_pc_slope()) end) @@ -370,20 +376,15 @@ describe('itest_dsl', function () 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() + flow:add_gamestate(state) + flow:change_gamestate_by_type(':stage') 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) + assert.are_equal(vector(1, -4), state.player_char.velocity) end) end) @@ -392,7 +393,7 @@ describe('itest_dsl', 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) + assert.are_equal(3.5, state.player_char.ground_speed) end) end) @@ -454,7 +455,6 @@ describe('itest_dsl', function () after_each(function () itest_dsl_parser:init() flow:init() - stage.state:init() pico8:clear_map() setup_map_data:clear() teardown_map_data:clear() @@ -533,7 +533,7 @@ describe('itest_dsl', function () -- + 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 +@stage # 64 @@ -547,7 +547,8 @@ expect assert.is_not_nil(dsli) assert.are_same( { - 'stage', + -- no ':stage' here, it's still interpret as plain text at this point + '@stage', '#', tilemap({ { 0, 64}, @@ -576,7 +577,7 @@ expect local gamestate_type, stage_name, tm, next_line_index = itest_dsl_parser.parse_gamestate_definition(dsli_lines) assert.are_same( { - 'titlemenu', + ':titlemenu', nil, nil, 3 @@ -599,7 +600,7 @@ expect local gamestate_type, stage_name, tm, next_line_index = itest_dsl_parser.parse_gamestate_definition(dsli_lines) assert.are_same( { - 'stage', + ':stage', "test1", nil, 3 @@ -639,7 +640,7 @@ expect -- interface assert.are_same( { - 'stage', + ':stage', '#', tilemap({ {70, 64}, @@ -793,7 +794,7 @@ expect it('should create an itest with a name and a dsl itest', function () local dsli = dsl_itest() - dsli.gamestate_type = 'stage' + dsli.gamestate_type = ':stage' dsli.stage_name = "test1" dsli.tilemap = nil dsli.commands = { @@ -813,7 +814,7 @@ expect assert.are_equal(4, #test.action_sequence) assert.are_same({ "test 1", - {'stage'}, + {':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 @@ -836,27 +837,25 @@ 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 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 + -- we only need stage state for stage itests + -- however, if character reaches goal we may go to another state + -- and since we removed dummies, we may have issues + -- if it happens, just create a dummy next state with the right type + -- and add it here + flow:add_gamestate(state) -- verify setup callback behavior test.setup() - assert.are_equal(gamestate.types.stage, flow.curr_state.type) + assert.are_equal(':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) + assert.is_not_nil(state.player_char) + assert.are_equal(vector(12, 45 - pc_data.center_height_standing), 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) + assert.are_equal(vector(-1, 0), state.player_char.move_intention) -- we have not passed time so the character cannot have reached expected position -- note we are testing as busted, so we get the almost_eq messages @@ -867,8 +866,8 @@ expect 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) + state.player_char:set_bottom_center(vector(10, 45)) + state.player_char.velocity = vector(2, -3.5) assert.are_same({true, ""}, {test.final_assertion()}) -- verify that parser state is cleaned up, ready for next parsing @@ -903,26 +902,20 @@ expect 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.gamestate_type = ':stage' dsli.stage_name = "#" dsli.tilemap = tilemap({}) dsli.commands = {} local test = itest_dsl_parser.create_itest("test 1", dsli) - -- 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 + -- see comment in previous test + flow:add_gamestate(state) test.setup() -- interface - assert.are_equal(control_modes.puppet, stage.state.player_char.control_mode) + assert.are_equal(control_modes.puppet, state.player_char.control_mode) -- implementation local s_data = assert.spy(setup_map_data) @@ -935,14 +928,16 @@ expect it('teardown should call clear_map and teardown_map_data if custom stage definition', function () local dsli = dsl_itest() - dsli.gamestate_type = 'stage' + dsli.gamestate_type = ':stage' dsli.stage_name = "#" dsli.tilemap = tilemap({}) dsli.commands = {} local test = itest_dsl_parser.create_itest("test 1", dsli) - gamestate_proxy:require_gamestates(test.active_gamestates) + -- see comment in previous test + flow:add_gamestate(state) + test.teardown() -- implementation @@ -1026,7 +1021,7 @@ expect itest_dsl_parser._itest = integration_test('test', {}) end) - it('#solo should set the final assertion as returning true, message when the gameplay value is expected', function () + it('should set the final assertion as returning true, message when the gameplay value is expected', function () itest_dsl_parser._final_expectations = { expectation("pc_bottom_pos", vector(27, 30)), expectation("pc_velocity", vector(-3, 2.5)) @@ -1035,7 +1030,7 @@ expect assert.are_same({true, ""}, {itest_dsl_parser._itest.final_assertion()}) end) - it('#solo should set the final assertion as returning false, message when the gameplay value is not expected', function () + it('should set the final assertion as returning false, message when the gameplay value is not expected', function () itest_dsl_parser._final_expectations = { expectation("pc_bottom_pos", vector(27, 30)), -- ok expectation("pc_velocity", vector(-3, 7.5)) -- different from actual diff --git a/src/itest_main.lua b/src/itest_main.lua new file mode 100644 index 00000000..778a1f3c --- /dev/null +++ b/src/itest_main.lua @@ -0,0 +1,92 @@ +-- main source file for all itests, used to run itests in pico8 + +-- must require at main top, to be used in any required modules from here +require("engine/pico8/api") + +require("engine/test/integrationtest") + +--#if log +local logging = require("engine/debug/logging") +--#endif + +local picosonic_app = require("application/picosonic_app") + +-- set app immediately so during itest registration by require, +-- time_trigger can access app fps +local app = picosonic_app() +itest_runner.app = app + +-- tag to add require for itest files here +--[[add_require]] + +function _init() +--#if log + -- register log streams to output logs to both the console and the file log + logging.logger:register_stream(logging.console_log_stream) + logging.logger:register_stream(logging.file_log_stream) + logging.file_log_stream.file_prefix = "picosonic" + + -- clear log file on new itest session + logging.file_log_stream:clear() + + logging.logger.active_categories = { + -- engine + ['default'] = true, + -- ['codetuner'] = nil, + -- ['flow'] = nil, + ['itest'] = true, + -- ['log'] = nil, + -- ['ui'] = nil, + -- ['frame'] = nil, + + -- game + -- ['...'] = true, + } +--#endif + + picosonic_app.initial_gamestate = ':titlemenu' + + -- start first itest + init_game_and_start_next_itest() +end + +function _update60() + handle_input() + itest_runner:update_game_and_test() +end + +function _draw() + itest_runner:draw_game_and_test() +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 + itest_manager: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 + itest_manager:init_game_and_start_itest_by_relative_index(-10) + return + elseif btnp(button_ids.down) then + -- skip many itests + itest_manager:init_game_and_start_itest_by_relative_index(10) + return + end + + 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() + end + end +end diff --git a/src/game/itests/itestplayercharacter.lua b/src/itests/itestplayercharacter.lua similarity index 81% rename from src/game/itests/itestplayercharacter.lua rename to src/itests/itestplayercharacter.lua index bdd5e154..8c777ada 100644 --- a/src/game/itests/itestplayercharacter.lua +++ b/src/itests/itestplayercharacter.lua @@ -1,16 +1,15 @@ -- gamestates: stage local integrationtest = require("engine/test/integrationtest") -local itest_dsl = require("game/itests/itest_dsl") +local itest_dsl = require("itest/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") local input = require("engine/input/input") local flow = require("engine/application/flow") -local stage = require("game/ingame/stage") -- required -local pc_data = require("game/data/playercharacter_data") -local tile_data = require("game/data/tile_data") ---#ifn pico8 -local tile_test_data = require("game/test_data/tile_test_data") +local pc_data = require("data/playercharacter_data") +local tile_data = require("data/tile_data") +--#if busted +local tile_test_data = require("test_data/tile_test_data") --#endif local itest @@ -154,25 +153,6 @@ 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( @@ -285,7 +265,7 @@ stop wait 24 expect pc_bottom_pos 39.859375 40.8125 -expect pc_motion_state airborne +expect pc_motion_state falling expect pc_ground_spd 0 expect pc_velocity 0.84375 2.625 ]]) @@ -294,10 +274,11 @@ expect pc_velocity 0.84375 2.625 -- 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 +-- at frame 36: pos (19.609375, 8), velocity (0.84375, 0), falling (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 +-- at frame 60: pos (39.859375, 8 + 32.8125), velocity (0.84375, 2.625), falling + itest_dsl_parser.register( 'platformer hop flat', [[ @@ -311,7 +292,7 @@ stop_jump wait 20 expect pc_bottom_pos 4 -11.296875 -expect pc_motion_state airborne +expect pc_motion_state air_spin expect pc_ground_spd 0 expect pc_velocity 0 -0.03125 ]]) @@ -320,12 +301,12 @@ expect pc_velocity 0 -0.03125 -- wait for apogee (frame 20) and stop -- 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 2: bpos (4, 8 - 2), velocity (0, -2), air_spin (hop confirmed, no gravity applied this frame) +-- at frame 3: bpos (4, 8 - 3.890625), velocity (0, -1.890625), air_spin +-- at frame 19: pos (4, 8 - 19.265625), velocity (0, -0.140625), air_spin -> before apogee +-- at frame 20: pos (4, 8 - 19.296875), velocity (0, -0.03125), air_spin -> reached apogee +-- at frame 21: pos (4, 8 - 19.21875), velocity (0, 0.078125), air_spin -> starts going down +-- at frame 38: pos (4, 8 - 1.15625), velocity (0, 1.9375), air_spin -> about to land -- at frame 39: pos (4, 8), velocity (0, 0), grounded -> has landed -- => apogee at y = 8 - 19.296875 = -11.296875 @@ -342,7 +323,7 @@ jump wait 2 expect pc_bottom_pos 4 4.75 -expect pc_motion_state airborne +expect pc_motion_state air_spin expect pc_ground_spd 0 expect pc_velocity 0 -3.25 ]]) @@ -361,7 +342,7 @@ stop_jump wait 1 expect pc_bottom_pos 4 -3.421875 -expect pc_motion_state airborne +expect pc_motion_state air_spin expect pc_ground_spd 0 expect pc_velocity 0 -2 ]]) @@ -370,10 +351,10 @@ expect pc_velocity 0 -2 -- interrupt variable jump at the end of frame 2 -- 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) +-- at frame 2: bpos (4, 8 - 3.25), velocity (0, -3.25), air_spin (jump confirmed) +-- at frame 3: bpos (4, 8 - 6.390625), velocity (0, -3.140625), air_spin +-- at frame 4: bpos (4, 8 - 9.421875), velocity (0, -3.03125), air_spin +-- at frame 5: bpos (4, 8 - 11.421875), velocity (0, -2), air_spin (interrupt jump, no extra gravity) itest_dsl_parser.register( @@ -389,7 +370,7 @@ stop_jump wait 6 expect pc_bottom_pos 4 -11.78125 -expect pc_motion_state airborne +expect pc_motion_state air_spin expect pc_ground_spd 0 expect pc_velocity 0 -1.453125 ]]) @@ -399,11 +380,11 @@ expect pc_velocity 0 -1.453125 -- 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.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 +-- at frame 6: bpos (4, 8 - 13.3125), velocity (0, -1.890625), air_spin +-- at frame 7: bpos (4, 8 - 15.09375), velocity (0, -1.78125), air_spin +-- at frame 8: bpos (4, 8 - 16.765625), velocity (0, -1.671875), air_spin +-- at frame 9: bpos (4, 8 - 18.328125), velocity (0, -1.5625), air_spin +-- at frame 10: bpos (4, 8 - 19.78125), velocity (0, -1.453125), air_spin itest_dsl_parser.register( @@ -417,7 +398,7 @@ jump wait 31 expect pc_bottom_pos 4 -41.921875 -expect pc_motion_state airborne +expect pc_motion_state air_spin expect pc_ground_spd 0 expect pc_velocity 0 -0.078125 ]]) @@ -426,11 +407,11 @@ expect pc_velocity 0 -0.078125 -- wait for the apogee (frame 31) and stop -- 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 2: bpos (4, 8 - 3.25), velocity (0, -3.25), air_spin (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), air_spin -> before apogee +-- at frame 31: bpos (4, 8 - 49.921875), velocity (0, -0.078125), air_spin -> reached apogee (100px in 16-bit, matches SPG on Jumping) +-- at frame 32: bpos (4, 8 - 49.890625), velocity (0, 0.03125), air_spin -> starts going down +-- at frame 61: bpos (4, 8 - 1.40625), velocity (0, 3.203125), air_spin -> about to land -- at frame 62: bpos (4, 8), velocity (0, 0), grounded -> has landed @@ -468,12 +449,12 @@ expect pc_velocity 0 0 -- calculation notes: -- wait for apogee (frame 20) and stop -- 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 2: bpos (4, 8 - 2), velocity (0, -2), air_spin (hop confirmed) +-- at frame 3: bpos (4, 8 - 3.890625), velocity (0, -1.890625), air_spin (hop confirmed) +-- at frame 19: bpos (4, 8 - 19.265625), velocity (0, -0.140625), air_spin -> before apogee +-- at frame 20: bpos (4, 8 - 19.296875), velocity (0, -0.03125), air_spin -> reached apogee +-- at frame 21: bpos (4, 8 - 19.21875), velocity (0, 0.078125), air_spin -> starts going down +-- at frame 38: bpos (4, 8 - 1.15625), velocity (0, 1.9375), air_spin -> 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, @@ -493,7 +474,7 @@ move right wait 29 expect pc_bottom_pos 24.390625 -41.921875 -expect pc_motion_state airborne +expect pc_motion_state air_spin expect pc_ground_spd 0 expect pc_velocity 1.359375 -0.078125 ]]) @@ -514,7 +495,7 @@ move right wait 9 expect pc_bottom_pos 5 1.9375 -expect pc_motion_state airborne +expect pc_motion_state air_spin expect pc_ground_spd 0 expect pc_velocity 0 -1.125 ]]) @@ -523,17 +504,17 @@ expect pc_velocity 0 -1.125 -- 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 +-- at frame 2: bpos (4 + .046875, 16 - 2), velocity (3/64, -2), air_spin (hop) +-- at frame 3: bpos (4.140625, 16 - 3.890625), velocity (6/64, -1 - 57/64), air_spin +-- at frame 4: bpos (4.28125, 16 - 5.671875), velocity (9/64, -1 - 50/64), air_spin +-- at frame 5: bpos (4.46875, 16 - 7.34375), velocity (12/64, -1 - 43/64), air_spin +-- at frame 6: bpos (4.703125, 16 - 8.90625), velocity (15/64, -1 - 36/64), air_spin +-- at frame 7: bpos (4.984375, 16 - 10.359375), velocity (18/64, -1 - 29/64), air_spin -- 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) +-- at frame 8: bpos (5, 16 - 11.703125), velocity (18/64, -1 - 22/64), air_spin (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) +-- at frame 9: bpos (5, 16 - 12.9375), velocity (0, -1 - 15/64), air_spin (hit wall) +-- at frame 10: bpos (5, 16 - 14.0625), velocity (0, -1 - 8/64), air_spin (hit wall) -- /64 format is nice, but I need to make a helper -- that converts floats to this format if I want a meaningful @@ -555,7 +536,7 @@ move left wait 9 expect pc_bottom_pos 11 1.9375 -expect pc_motion_state airborne +expect pc_motion_state air_spin expect pc_ground_spd 0 expect pc_velocity 0 -1.125 ]]) @@ -571,31 +552,30 @@ itest_dsl_parser.register( warp 4 32 jump -wait 4 +wait 7 -expect pc_bottom_pos 4 24 -expect pc_motion_state airborne +expect pc_bottom_pos 4 16 +expect pc_motion_state air_spin 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 +-- we are now using sonic compact height = 8 during jump +-- so he will hit ceiling at bottom pos = (1 tile '#') * 8 + 8 = 16 = 32 - 16 +-- where 32 is the initial bottom pos, so we need to jump over 16px -- 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 +-- 2 (4, 32 - 3 - 16/64) (0, -3 - 16/64) air_spin confirm jump (no gravity on first frame) +-- 3 (4, 32 - 6 - 25/64) (0, -3 - 9/64) air_spin +-- 4 (4, 32 - 8) (0, 0) air_spin +-- 4 (4, 32 - 9 - 27/64) (0, -3 - 2/64) air_spin +-- 5 (4, 32 - 12 - 22/64) (0, -2 - 59/64) air_spin +-- 6 (4, 32 - 15 - 10/64) (0, -2 - 52/64) air_spin +-- 7 (4, 32 - 16) (0, 0) air_spin hit ceiling -- human tests: let human check rendering (until I find a way to automate this) diff --git a/src/game/itests/itesttitlemenu.lua b/src/itests/itesttitlemenu.lua similarity index 76% rename from src/game/itests/itesttitlemenu.lua rename to src/itests/itesttitlemenu.lua index 2f165276..21c27dbc 100644 --- a/src/game/itests/itesttitlemenu.lua +++ b/src/itests/itesttitlemenu.lua @@ -3,14 +3,13 @@ local integrationtest = require("engine/test/integrationtest") 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") -local gamestate = require("game/application/gamestate") itest_manager:register_itest('player select credits, confirm', - {gamestate.types.titlemenu}, function () + {':titlemenu'}, function () -- enter title menu - setup_callback(function () - flow:change_gamestate_by_type(gamestate.types.titlemenu) + setup_callback(function (app) + flow:change_gamestate_by_type(':titlemenu') end) wait(1.0) @@ -43,7 +42,7 @@ itest_manager:register_itest('player select credits, confirm', -- check that we entered the credits state final_assert(function () - return flow.curr_state.type == gamestate.types.credits, "current game state is not '"..gamestate.types.credits.."', has instead type: "..flow.curr_state.type + return flow.curr_state.type == ':credits', "current game state is not ':credits', has instead type: "..flow.curr_state.type end) end) diff --git a/src/main.lua b/src/main.lua new file mode 100644 index 00000000..251e92ee --- /dev/null +++ b/src/main.lua @@ -0,0 +1,80 @@ +-- main entry file for the game + +-- we must require engine/pico8/api at the top of our main.lua, so API bridges apply to all modules +require("engine/pico8/api") +-- we also require codetuner so any file can used tuned() +-- if tuner symbol is defined, then we also initialize it in _init +local codetuner = require("engine/debug/codetuner") + +--#if log +local logging = require("engine/debug/logging") +--#endif + +--#if visual_logger +local vlogger = require("engine/debug/visual_logger") +--#endif + +--#if profiler +local profiler = require("engine/debug/profiler") +--#endif + +local picosonic_app = require("application/picosonic_app") + +local app = picosonic_app() + +function _init() +--#if log + -- start logging before app in case we need to read logs about app start itself + logging.logger:register_stream(logging.console_log_stream) + logging.logger:register_stream(logging.file_log_stream) +--#if visual_logger + logging.logger:register_stream(vlogger.vlog_stream) +--#endif + + logging.file_log_stream.file_prefix = "picosonic" + + -- clear log file on new game session (or to preserve the previous log, + -- you could add a newline and some "[SESSION START]" tag instead) + logging.file_log_stream:clear() + + logging.logger.active_categories = { + -- engine + ['default'] = true, + ['codetuner'] = true, + ['flow'] = true, + ['itest'] = true, + ['log'] = true, + ['ui'] = true, + -- ['frame'] = nil, + + -- game + -- ['...'] = true, + } +--#endif + +--#if visual_logger + -- uncomment to enable visual logger + -- vlogger.window:show() +--#endif + +--#if profiler + -- uncomment to enable profiler + -- profiler.window:show(colors.orange) +--#endif + +--#if tuner + codetuner:show() + codetuner.active = true +--#endif + + app.initial_gamestate = ':titlemenu' + app:start() +end + +function _update60() + app:update() +end + +function _draw() + app:draw() +end diff --git a/src/menu/credits.lua b/src/menu/credits.lua new file mode 100644 index 00000000..db443730 --- /dev/null +++ b/src/menu/credits.lua @@ -0,0 +1,27 @@ +require("engine/application/flow") +require("engine/core/class") +require("engine/render/color") +local gamestate = require("engine/application/gamestate") + + +local credits = derived_class(gamestate) + +credits.type = ':credits' + +-- function credits:on_enter() +-- end + +-- function credits:on_exit() +-- end + +-- function credits:update() +-- end + +function credits:render() + color(colors.white) + api.print("credits state", 4*11, 6*12) +end + +-- export + +return credits diff --git a/src/menu/credits_utest.lua b/src/menu/credits_utest.lua new file mode 100644 index 00000000..a66f9d50 --- /dev/null +++ b/src/menu/credits_utest.lua @@ -0,0 +1,67 @@ +require("engine/test/bustedhelper") + +local picosonic_app = require("application/picosonic_app") +local credits = require("menu/credits") + +describe('credits', function () + + describe('static members', function () + + it('type is :stage', function () + assert.are_equal(':credits', credits.type) + end) + + end) + + describe('(with instance)', function () + + local state + + before_each(function () + local app = picosonic_app() + state = credits() + -- no need to register gamestate properly, just add app member to pass tests + state.app = app + end) + + it('type is ":stage"', function () + assert.are_equal(':credits', credits.type) + end) + + -- describe('on_enter', function () + -- end) + + -- describe('on_exit', function () + -- end) + + -- describe('update', function () + -- end) + + describe('render', function () + + local api_print_stub + + setup(function () + api_print_stub = stub(api, "print") + end) + + teardown(function () + api_print_stub:revert() + end) + + after_each(function () + api_print_stub:clear() + end) + + it('should print "credits state" in white', function () + credits:render() + assert.are_equal(colors.white, pico8.color) + assert.spy(api_print_stub).was_called(1) + assert.spy(api_print_stub).was_called_with("credits state", 4*11, 6*12) + end) + + end) + + end) -- (with instance) + +end) diff --git a/src/game/menu/titlemenu.lua b/src/menu/titlemenu.lua similarity index 65% rename from src/game/menu/titlemenu.lua rename to src/menu/titlemenu.lua index 6ef5c341..145fad2f 100644 --- a/src/game/menu/titlemenu.lua +++ b/src/menu/titlemenu.lua @@ -2,15 +2,14 @@ require("engine/core/class") require("engine/render/color") local input = require("engine/input/input") local flow = require("engine/application/flow") +local gamestate = require("engine/application/gamestate") local ui = require("engine/ui/ui") -local gamestate = require("game/application/gamestate") -local titlemenu = {} +local titlemenu = derived_class(gamestate) --- game state -local titlemenustate = singleton(function (self) - self.type = gamestate.types.titlemenu +titlemenu.type = ':titlemenu' +function titlemenu:_init() -- parameters -- number of items in the menu @@ -20,16 +19,16 @@ local titlemenustate = singleton(function (self) -- current cursor index (0: start, 1: credits) self.current_cursor_index = 0 -end) +end -function titlemenustate:on_enter() +function titlemenu:on_enter() self.current_cursor_index = 0 end -function titlemenustate:on_exit() +function titlemenu:on_exit() end -function titlemenustate:update() +function titlemenu:update() if input:is_just_pressed(button_ids.up) then self:move_cursor_up() elseif input:is_just_pressed(button_ids.down) then @@ -39,31 +38,29 @@ function titlemenustate:update() end end -function titlemenustate:render() +function titlemenu:render() color(colors.white) api.print("start", 4*11, 6*12) api.print("credits", 4*11, 6*13) api.print(">", 4*10, 6*(12+self.current_cursor_index)) end -function titlemenustate:move_cursor_up() +function titlemenu:move_cursor_up() -- move cursor up, clamped self.current_cursor_index = max(self.current_cursor_index - 1, 0) end -function titlemenustate:move_cursor_down() +function titlemenu:move_cursor_down() -- move cursor down, clamped self.current_cursor_index = min(self.current_cursor_index + 1, self.items_count - 1) end -function titlemenustate:confirm_current_selection() +function titlemenu:confirm_current_selection() if self.current_cursor_index == 0 then - flow:query_gamestate_type(gamestate.types.stage) + flow:query_gamestate_type(':stage') else -- current_cursor_index == 1 - flow:query_gamestate_type(gamestate.types.credits) + flow:query_gamestate_type(':credits') end end --- export -titlemenu.state = titlemenustate return titlemenu diff --git a/src/menu/titlemenu_utest.lua b/src/menu/titlemenu_utest.lua new file mode 100644 index 00000000..7fae05a9 --- /dev/null +++ b/src/menu/titlemenu_utest.lua @@ -0,0 +1,275 @@ +require("engine/test/bustedhelper") +local input = require("engine/input/input") +local flow = require("engine/application/flow") +local gamestate = require("engine/application/gamestate") + +local picosonic_app = require("application/picosonic_app") +local titlemenu = require("menu/titlemenu") + +local dummy_stage_state = derived_class(gamestate) +dummy_stage_state.type = ':stage' + +local dummy_credits_state = derived_class(gamestate) +dummy_credits_state.type = ':credits' + +describe('titlemenu', function () + + describe('static members', function () + + it('type is :stage', function () + assert.are_equal(':titlemenu', titlemenu.type) + end) + + end) + + describe('(stage states added)', function () + + before_each(function () + flow:add_gamestate(titlemenu) + flow:add_gamestate(dummy_credits_state) + flow:add_gamestate(dummy_stage_state) + end) + + after_each(function () + flow:init() + end) + + describe('(with instance)', function () + + local state + + before_each(function () + local app = picosonic_app() + state = titlemenu() + -- no need to register gamestate properly, just add app member to pass tests + state.app = app + end) + + describe('state:on_enter', function () + + before_each(function () + state:on_enter() + end) + + it('should initialize cursor at index 0', function () + state:on_enter() + assert.are_equal(0, state.current_cursor_index) + end) + + end) + + describe('state:on_exit', function () + end) + + describe('(titlemenu state entered)', function () + + before_each(function () + flow:_change_state(state) + end) + + describe('state.current_cursor_index', function () + it('should be set to 0', function () + assert.are_equal(0, state.current_cursor_index) + end) + end) + + describe('state:update', function () + + setup(function () + stub(titlemenu, "move_cursor_up") + stub(titlemenu, "move_cursor_down") + stub(titlemenu, "confirm_current_selection") + end) + + teardown(function () + titlemenu.move_cursor_up:revert() + titlemenu.move_cursor_down:revert() + titlemenu.confirm_current_selection:revert() + end) + + after_each(function () + input:init() + + titlemenu.move_cursor_up:clear() + titlemenu.move_cursor_down:clear() + titlemenu.confirm_current_selection:clear() + end) + + it('(when input up in down) it should be move cursor up', function () + input.players_btn_states[0][button_ids.up] = btn_states.just_pressed + state:update() + assert.spy(titlemenu.move_cursor_up).was_called(1) + assert.spy(titlemenu.move_cursor_up).was_called_with(match.ref(state)) + end) + + it('(when input down in down) it should be move cursor down', function () + input.players_btn_states[0][button_ids.down] = btn_states.just_pressed + state:update() + assert.spy(titlemenu.move_cursor_down).was_called(1) + assert.spy(titlemenu.move_cursor_down).was_called_with(match.ref(state)) + end) + + it('(when input x in down) it should be move cursor x', function () + input.players_btn_states[0][button_ids.x] = btn_states.just_pressed + state:update() + assert.spy(titlemenu.confirm_current_selection).was_called(1) + assert.spy(titlemenu.confirm_current_selection).was_called_with(match.ref(state)) + end) + + end) + + describe('(cursor start at index 0)', function () + + before_each(function () + state.current_cursor_index = 0 + end) + + after_each(function () + state.current_cursor_index = 0 + end) + + describe('state:move_cursor_up', function () + + it('should not change current_cursor_index due to clamping', function () + state:move_cursor_up() + assert.are_equal(0, state.current_cursor_index) + end) + + end) + + describe('state:move_cursor_down', function () + + it('should increase current_cursor_index', function () + state:move_cursor_down() + assert.are_equal(1, state.current_cursor_index) + end) + + end) + + describe('render', function () + + local api_print_stub + + setup(function () + api_print_stub = stub(api, "print") + end) + + teardown(function () + api_print_stub:revert() + end) + + after_each(function () + api_print_stub:clear() + end) + + it('should print "starts", "credits" and cursor ">" in front of start in white', function () + state:render() + assert.are_equal(colors.white, pico8.color) + assert.spy(api_print_stub).was_called(3) + assert.spy(api_print_stub).was_called_with("start", 4*11, 6*12) + assert.spy(api_print_stub).was_called_with("credits", 4*11, 6*13) + assert.spy(api_print_stub).was_called_with(">", 4*10, 6*12) + end) + + end) + + end) + + describe('(cursor start at index 1)', function () + + before_each(function () + state.current_cursor_index = 1 + end) + + after_each(function () + state.current_cursor_index = 0 + end) + + describe('state:move_cursor_up', function () + + it('should decrease current_cursor_index', function () + state:move_cursor_up() + assert.are_equal(0, state.current_cursor_index) + end) + + end) + + describe('state:move_cursor_down', function () + + it('should not change current_cursor_index due to clamping', function () + state:move_cursor_down() + assert.are_equal(1, state.current_cursor_index) + end) + + end) + + + describe('render', function () + + local api_print_stub + + setup(function () + api_print_stub = stub(api, "print") + end) + + teardown(function () + api_print_stub:revert() + end) + + after_each(function () + api_print_stub:clear() + end) + + it('should print "starts", "credits" and cursor ">" in front of credits in white', function () + state:render() + assert.are_equal(colors.white, pico8.color) + assert.spy(api_print_stub).was_called(3) + assert.spy(api_print_stub).was_called_with("start", 4*11, 6*12) + assert.spy(api_print_stub).was_called_with("credits", 4*11, 6*13) + assert.spy(api_print_stub).was_called_with(">", 4*10, 6*13) + end) + + end) + + end) + + end) -- (titlemenu state entered) + + describe('(enter titlemenu state each time)', function () + + before_each(function () + flow:_change_state(state) + end) + + after_each(function () + flow.curr_state:on_exit() -- whatever the current gamestate is + flow.curr_state = nil + end) + + describe('state:confirm_current_selection', function () + + it('should have queried stage state', function () + state.current_cursor_index = 0 + state:confirm_current_selection() + assert.are_equal(':stage', flow.next_state.type) + end) + + end) + + describe('state:confirm_current_selection', function () + + it('should have queried credits state', function () + state.current_cursor_index = 1 + state:confirm_current_selection() + assert.are_equal(':credits', flow.next_state.type) + end) + + end) + + end) -- (enter titlemenu state each time) + + end) -- (with instance) + + end) -- (stage states added) + +end) diff --git a/src/game/platformer/motion.lua b/src/platformer/motion.lua similarity index 100% rename from src/game/platformer/motion.lua rename to src/platformer/motion.lua diff --git a/src/game/tests/utestmotion.lua b/src/platformer/motion_utest.lua similarity index 98% rename from src/game/tests/utestmotion.lua rename to src/platformer/motion_utest.lua index de5096d8..6ff21900 100644 --- a/src/game/tests/utestmotion.lua +++ b/src/platformer/motion_utest.lua @@ -1,5 +1,5 @@ require("engine/test/bustedhelper") -local motion = require("game/platformer/motion") +local motion = require("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") diff --git a/src/game/platformer/tile.lua b/src/platformer/tile.lua similarity index 100% rename from src/game/platformer/tile.lua rename to src/platformer/tile.lua diff --git a/src/game/tests/utesttile.lua b/src/platformer/tile_utest.lua similarity index 98% rename from src/game/tests/utesttile.lua rename to src/platformer/tile_utest.lua index fa49b792..5899808a 100644 --- a/src/game/tests/utesttile.lua +++ b/src/platformer/tile_utest.lua @@ -1,6 +1,6 @@ require("engine/test/bustedhelper") require("engine/core/math") -local tile = require("game/platformer/tile") +local tile = require("platformer/tile") local tile_data = tile.tile_data local height_array = tile.height_array diff --git a/src/game/platformer/world.lua b/src/platformer/world.lua similarity index 95% rename from src/game/platformer/world.lua rename to src/platformer/world.lua index d51a1358..e2782c24 100644 --- a/src/game/platformer/world.lua +++ b/src/platformer/world.lua @@ -1,6 +1,5 @@ -local tile = require("game/platformer/tile") --- engine > game reference to clean -local collision_data = require("game/data/collision_data") +local tile = require("platformer/tile") +local collision_data = require("data/collision_data") local world = {} diff --git a/src/game/tests/utestworld.lua b/src/platformer/world_utest.lua similarity index 97% rename from src/game/tests/utestworld.lua rename to src/platformer/world_utest.lua index 10072e9b..5f15bad7 100644 --- a/src/game/tests/utestworld.lua +++ b/src/platformer/world_utest.lua @@ -1,5 +1,7 @@ -local world = require("game/platformer/world") -local tile_test_data = require("game/test_data/tile_test_data") +require("engine/test/bustedhelper") +local world = require("platformer/world") + +local tile_test_data = require("test_data/tile_test_data") describe('world (with mock tiles data setup)', function () @@ -189,6 +191,7 @@ describe('world (with mock tiles data setup)', function () end) end) - end) - end) + end) + +end) diff --git a/src/game/resources/audio.lua b/src/resources/audio.lua similarity index 100% rename from src/game/resources/audio.lua rename to src/resources/audio.lua diff --git a/src/game/resources/visual.lua b/src/resources/visual.lua similarity index 100% rename from src/game/resources/visual.lua rename to src/resources/visual.lua diff --git a/src/game/sandbox.lua b/src/sandbox.lua similarity index 100% rename from src/game/sandbox.lua rename to src/sandbox.lua diff --git a/src/game/tests/utesttile_data.lua b/src/test_data/tile_data_utest.lua similarity index 90% rename from src/game/tests/utesttile_data.lua rename to src/test_data/tile_data_utest.lua index 53237c17..93147b0e 100644 --- a/src/game/tests/utesttile_data.lua +++ b/src/test_data/tile_data_utest.lua @@ -1,6 +1,6 @@ require("engine/test/bustedhelper") -require("game/data/tile_data") -local tile_test_data = require("game/test_data/tile_test_data") +require("data/tile_data") +local tile_test_data = require("test_data/tile_test_data") describe('tiledata', function () diff --git a/src/game/test_data/tile_test_data.lua b/src/test_data/tile_test_data.lua similarity index 96% rename from src/game/test_data/tile_test_data.lua rename to src/test_data/tile_test_data.lua index f9a495e3..0f16596a 100644 --- a/src/game/test_data/tile_test_data.lua +++ b/src/test_data/tile_test_data.lua @@ -1,8 +1,8 @@ ---#ifn pico8 +--#if busted require("engine/test/pico8api") -local tile = require("game/platformer/tile") -local collision_data = require("game/data/collision_data") +local tile = require("platformer/tile") +local collision_data = require("data/collision_data") local stub = require("luassert.stub") local tile_test_data = {} diff --git a/src/tests/headless_itests_utest.lua b/src/tests/headless_itests_utest.lua new file mode 100644 index 00000000..cefd5701 --- /dev/null +++ b/src/tests/headless_itests_utest.lua @@ -0,0 +1,54 @@ +-- todo: use busted --helper=.../bustedhelper instead of all the bustedhelper requires! +require("engine/test/bustedhelper") +require("engine/test/headless_itest") +require("engine/test/integrationtest") +local logging = require("engine/debug/logging") + +local picosonic_app = require("application/picosonic_app") + +local app = picosonic_app() +app.initial_gamestate = ':titlemenu' + +logging.logger:register_stream(logging.console_log_stream) +logging.logger:register_stream(logging.file_log_stream) +logging.file_log_stream.file_prefix = "picosonic_headless_itests" + +-- clear log file on new itest session +logging.file_log_stream:clear() + +logging.logger.active_categories = { + -- engine + ['default'] = true, + -- ['codetuner'] = nil, + -- ['flow'] = nil, + ['itest'] = true, + -- ['log'] = nil, + -- ['ui'] = nil, + -- ['frame'] = nil, + + -- game + -- ['...'] = true, +} + +-- set app immediately so during itest registration by require, +-- time_trigger can access app fps +itest_runner.app = app + +-- require *_itest.lua files to automatically register them in the integration test manager +require_all_scripts_in('src', 'itests') + +local should_render = check_env_should_render() +if should_render then + print("[headless itest] enabling rendering") +end + +-- uncomment below to randomize seed (doesn't matter too much in picosonic) +-- (busted needs that to give different results each time, +-- while PICO-8 will automatically randomize the seed on start) +-- ! since itests won't give the same results every time, if you want a specific result, +-- you need to force setup some variables (like the next opponent) in your specific itest +-- local random_seed = os.time() +-- print("[headless itest] setting random seed to: "..random_seed) +-- srand(random_seed) + +create_describe_headless_itests_callback(app, should_render, describe, setup, teardown, before_each, after_each, it, assert) diff --git a/src/game/utest_main.lua b/src/utest_main.lua similarity index 59% rename from src/game/utest_main.lua rename to src/utest_main.lua index 1decdfc8..cf6f80f9 100644 --- a/src/game/utest_main.lua +++ b/src/utest_main.lua @@ -1,12 +1,12 @@ -- main source file to run a unit test in pico8 -- this is really useful for data tests and pico8 fixed-point math tests, -- otherwise busted tests should be enough --- each utest should be put inside the game/utests folder with the name utest{module}.lua +-- each utest should be put inside the src/utests folder with the name utest{something}.lua -require("game/utests/utest$utest") local unittest = require("engine/test/unittest") local utest_manager, unit_test = unittest.utest_manager, unittest.unit_test -local gamestate_proxy = require("game/application/gamestate_proxy") +-- tag to add require for pico8 utests files (should be in utests/) +--[[add_require]] --#if log local logging = require("engine/debug/logging") @@ -14,8 +14,5 @@ logging.logger:register_stream(logging.console_log_stream) --#endif function _init() - -- require all gamestate modules needed for the utest (currently only dummy states) - gamestate_proxy:require_gamestates() - utest_manager:run_all_tests() end diff --git a/src/game/utests/utestdata.lua b/src/utests/utestdata.lua similarity index 63% rename from src/game/utests/utestdata.lua rename to src/utests/utestdata.lua index cc701436..c8975120 100644 --- a/src/game/utests/utestdata.lua +++ b/src/utests/utestdata.lua @@ -1,10 +1,16 @@ require("engine/test/unittest") require("engine/core/math") -local tile = require("game/platformer/tile") -local collision_data = require("game/data/collision_data") +local tile = require("platformer/tile") local height_array = tile.height_array local tile_data = tile.tile_data +-- data to test +local collision_data = require("data/collision_data") +-- this one is not checked although we could verify that sprites are not empty or something +-- but it's useful to check definition sanity (e.g. animation with 0 sprites, in particular +-- after minification if keys are not protected with ["key"] syntax) +local playercharacter_data = require("data/playercharacter_data") + check('sprite_id_location(0, 4) should have collision flag set', function () local sprite_id = sprite_id_location(0, 4):to_sprite_id() assert(fget(sprite_id, sprite_flags.collision), "sprite_id_location(0, 4) has collision flag unset") @@ -27,3 +33,11 @@ check('= height_array._fill_array on sprite_id_location(0, 5) the array with til height_array._fill_array(array, sprite_id_location(1, 5)) assert(are_same_with_message({1, 2, 3, 4, 5, 6, 7, 8}, array)) end) + +check('sonic_sprite_data_table preserved key "idle"', function () + assert(playercharacter_data.sonic_sprite_data_table["idle"] ~= nil) +end) + +check('sonic_animated_sprite_data_table preserved key "idle"', function () + assert(playercharacter_data.sonic_animated_sprite_data_table["idle"] ~= nil) +end) diff --git a/test.sh b/test.sh index 8f989326..ffc897ed 100755 --- a/test.sh +++ b/test.sh @@ -1,93 +1,73 @@ #!/bin/bash -# $1: test name (module name) -if [[ $# -lt 1 ]] ; then - 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 - -r or --render to enable rendering in the itest loop (used for $1=headless_itests only)" - exit 1 -fi +# Copied from pico-boots-demo/test.sh -# https://stackoverflow.com/questions/192249/how-do-i-parse-command-line-arguments-in-bash +# Configuration +game_src_path="$(dirname "$0")/src" +game_config_path="$(dirname "$0")/config" +picoboots_scripts_path="$(dirname "$0")/pico-boots/scripts" -if [[ ${1::5} = "utest" ]] ; then - MODULE=${1:5} -else - MODULE=$1 -fi +help() { + echo "Test game modules with busted -# shift arguments 1 position so we start reading options at $1 -shift +This is essentially a proxy script for pico-boots/scripts/test_scripts.sh that avoids +passing src/FOLDER every time we want to test a group of scripts in the game. -TEST_FILTER_MODE="standard" +It doesn't prepend the engine path though, so if you want to test engine folders easily, +use pico-boots/test.sh instead. -# 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 +Dependencies: +- busted (must be in PATH) +- luacov (must be in PATH) +" + usage +} -RENDER=false +usage() { + echo "Usage: test.sh [FOLDER-1 [FOLDER-2 [...]]] -while [[ "$1" != "" ]]; do - case $1 in - -r | --render ) shift - RENDER=true - ;; - * ) echo "unknown option: $1" - exit 1 - esac - shift -done +ARGUMENTS + FOLDER Path to game folder to test. + Path is relative to src. Sub-folders are supported. + (optional) -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, 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" - # 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 + -h, --help Show this help message +" +} -if [[ $TEST_FILTER_MODE = "all" ]] ; then - FILTER="" - FILTER_OUT="" - USE_COVERAGE=true -elif [[ $TEST_FILTER_MODE = "solo" ]]; then - FILTER="--filter \"#solo\"" - FILTER_OUT="" - # coverage on a file is not relevant when testing one or two functions - USE_COVERAGE=false -else - FILTER="" - FILTER_OUT="--filter-out \"#mute\"" - USE_COVERAGE=true -fi - -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 - PRE_TEST="" - POST_TEST="" -fi +# Default parameters +folders=() +other_options=() -EXTRA_ARGS="" +# Read arguments +# https://stackoverflow.com/questions/192249/how-do-i-parse-command-line-arguments-in-bash +roots=() +while [[ $# -gt 0 ]]; do + case $1 in + -h | --help ) + help + exit 0 + ;; + -* ) # we started adding options + # since we don't support "--" for final positional arguments, just pass all the rest to test_scripts.sh + break + ;; + * ) # positional argument: folder + folders+=("$1") + shift # past argument + ;; + esac +done -if [[ $RENDER = true ]]; then - EXTRA_ARGS+="--render" +if [[ ${#folders[@]} -ne 0 ]]; then + # Paths are relative to game src, so prepend it before passing to actual test script + for folder in "${folders[@]}"; do + roots+=("\"$game_src_path/$folder\"") + done +else + # No folder passed, test the whole game folder + roots=("\"$game_src_path\"") fi -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..." -echo "> $TEST_COMMAND" -# Generate luacov report and display all uncovered lines and cover percentages -bash -c "$TEST_COMMAND" +# Add extra lua root 'src' to enable require for game scripts +"$picoboots_scripts_path/test_scripts.sh" ${roots[@]} --lua-root src -c "$game_config_path/.luacov_game" $@