This template serves as a foundation for developing games using the Picotron fantasy computer environment. It offers a well-organized setup for managing Lua code, logging, and unit testing, ensuring your game's stability and scalability during development.
- Introduction
- Project Structure
- Installation
- Usage
- Lua
require()
and Module System - Logging System
- Unit Testing
- Assertion Library
- License
- Additional Resources
- Contributing
Picotron is a fantasy computer developed by Lexaloffle, offering a creative platform for developing games, demos, and tools using pixel graphics and Lua programming. This template streamlines game development by providing an organized project structure, essential utilities (such as logging and assertion tools), and a unit testing framework.
The project is structured as follows:
PROJECT_ROOT/
├── lib/
│ ├── assert.lua # Assertion utilities for testing
│ ├── log.lua # Logging utilities for runtime events
├── src/ # Source code for your game (expandable)
├── test/ # Directory for unit tests
├── .gitattributes # Git configuration for handling line endings
├── configuration.lua # Global configuration settings
├── globals.lua # Global utility functions
├── LICENSE # License file for the project (GNU GPLv3)
├── logview.lua # Log viewer for inspecting logs in a GUI window
├── main.lua # Entry point for the game
├── README.md # Project documentation
├── require.lua # Module loading system (custom `require()`)
├── run_tests.lua # Script to run unit tests
└── test_configuration.lua # Configuration settings for tests
-
Clone this repository to your local machine:
git clone https://github.com/yourusername/picotron-game-template.git
-
Install the Picotron Fantasy Computer following the official instructions.
-
Copy your project folder into the Picotron workspace to begin developing.
To start your game, open Picotron and navigate to the main.lua
file. This file serves as the entry point for your game. Customize it as needed to incorporate your game's logic.
Global settings, such as enabling/disabling features like logging or defining game-specific variables, can be modified in the configuration.lua
file. This file centralizes all critical configurations for the game.
This template includes a custom implementation of the Lua require()
function, compatible with Lua 5.4, to simplify module loading and caching, making your game easier to manage and test.
-
Modular Design: Organize your code by distributing functionality across different modules (files). Modules can contain game logic, utilities, or configuration settings. Use
require()
to load them. -
Module Caching: Once a module is loaded, it is cached to avoid multiple reloads, improving performance by reusing loaded modules.
-
Using
require()
to Load Modules: Load a module using its filename. Therequire()
function returns the module's content, typically a table of functions or data.Example:
local player = require("player") player.move(10, 20) print("Player health: " .. player.health) player.take_damage(5) print("Player health after damage: " .. player.health)
-
Adding Custom Search Paths: Define additional search paths to organize your modules in different directories.
Example:
add_module_path("/additional_module_directory/")
-
Clearing the Module Cache: Clear the module cache to reload specific modules, which is useful during testing.
Example:
clear_module_cache({ "log" }) -- Clears all cached modules except the log module
-
Mock Modules for Testing: Load mock versions of modules during testing by using aliases, allowing you to simulate behavior without affecting the actual game.
Example:
local log = require("mock_log", "log") -- Mock log module under the real log alias log.info("Testing started")
This template includes a comprehensive logging system to track game events, debug information, and errors. The system allows you to log messages to either the console or an external process, such as the included log viewer, providing real-time visibility into your game’s behavior.
-
Log Levels: Different log levels control the verbosity of the output. Set the appropriate log level before initializing the logging system. Available log levels are:
TRACE
: Very detailed logging, useful for tracing function calls.DEBUG
: Detailed information to help with debugging.INFO
: General game execution information (e.g., "Game started").WARN
: Warnings about potential issues that do not stop the game.ERROR
: Critical errors requiring immediate attention.
-
Log Targets: Logs can be directed to either:
- Console: Logs printed directly in the console.
- External Process: Logs sent to an external process such as the log viewer (
logview.lua
).
-
External Logging: When logging to an external process, the system sends messages to another program (e.g.,
logview.lua
), allowing real-time monitoring in a separate window. -
Timestamped Entries: Each log entry is automatically timestamped, which helps with debugging and tracking event order.
-
Dynamic Control Over Logging: Adjust the log level to control which messages are logged. For example, setting the log level to
ERROR
will only log critical errors.
-
Setting Log Level and Target: Set the log level and target before calling
init()
.Example:
log.set_level(log.levels.DEBUG) -- Set log level to DEBUG log.set_target(log.targets.CONSOLE) -- Set log target to console log.init() -- Initialize logging
-
Logging Messages: Log messages at different levels (
TRACE
,DEBUG
,INFO
,WARN
,ERROR
).log.info("Game initialized") -- Logs an info message log.error("Failed to load asset") -- Logs an error message
-
Reinitializing the Log System: If you change the log level or target after the initial setup, reinitialize the logging system by calling
init()
again.log.set_level(log.levels.TRACE) -- Change log level to TRACE log.set_target(log.targets.EXTERNAL_PROCESS) -- Change target to external process log.init() -- Reinitialize logging system
-
Tracing Function Calls: Use
trace_function()
to log function entry and exit points, making it easier to trace the flow of function calls.local result = log.trace_function("move_player", move_player, player, dx, dy)
log.set_level(log.levels.INFO)
log.set_target(log.targets.EXTERNAL_PROCESS)
log.init() -- Initialize logging
log.info("Game started")
log.warn("Low player health detected")
if not player then
log.error("Failed to load player data")
end
The logview.lua
script provides a graphical interface for viewing log messages in real-time. It displays up to 500 log entries, removing the oldest entries as new ones are added.
- Always set the log level and target before calling
log.init()
. - Reinitialize the logging system if the log level or target is changed after initialization.
This template includes a unit testing system to ensure that your game functions reliably and as expected. It allows for organized testing with modular test files, setup/teardown functions, and integration with the logging system to maintain code quality and catch bugs early.
-
Modular Test Structure: Tests are organized into Lua files (fixtures) stored in the
test/
directory. Each fixture contains multiple test cases, focusing on specific parts of the game. -
Test Lifecycle Management: The framework supports lifecycle hooks to manage the test environment:
before_all()
: Runs once before any tests in the fixture, useful for initializing resources.before_each()
: Runs before each test case to prepare or reset the environment.- Test Functions: Each test case is defined as a separate function in the fixture.
after_each()
: Cleans up after each test case to ensure a fresh state.after_all()
: Runs after all tests in the fixture, typically used for final cleanup of shared resources.
Example test fixture structure:
-- Importing required modules local assert = require("assert") -- Used for assertions in test cases local log = require("log") -- Used for logging during test execution -- Define a fixture table to hold the test setup, teardown, and test functions local fixture = { } -- Called once before any tests are run (fixture initialization) function fixture.before_all() -- Setup logic that runs before all tests in the fixture end -- Called before each individual test is executed (test case initialization) function fixture.before_each() -- Setup logic that runs before each test case in the fixture end -- First test case: Write your test logic here function fixture.test_something() -- Test something end -- Second test case: Another example test function fixture.test_something2() -- Test something else end -- Called after each individual test is executed (test case cleanup) function fixture.after_each() -- Cleanup logic that runs after each test case in the fixture end -- Called once after all tests are run (fixture cleanup) function fixture.after_all() -- Cleanup logic that runs after all tests in the fixture end -- Return the fixture table to be used by the test framework return fixture
-
Logging Integration: The system logs test results in real-time, providing detailed error messages if tests fail.
-
Automatic Test Discovery: The
run_tests.lua
script automatically detects and runs all test files in thetest/
directory. -
Detailed Error Reporting: The system logs detailed error messages, including the line number, making it easy to identify the source of problems.
To run all tests, execute the run_tests.lua
script:
run_tests.lua
To run specific tests:
run_tests.lua test_player.lua test_inventory.lua
Each test file (fixture) contains multiple test cases using lifecycle hooks to manage setup and teardown. Below is an example test fixture for the player.lua
module.
local assert = require("assert")
local log = require("log")
local player = require("player")
local fixture = {}
function fixture.before_each()
player.x, player.y = 0, 0
player.health = 100
player.inventory = {}
log.info("Player state reset for a new test")
end
function fixture.test_player_moves_correctly()
local initial_x, initial_y = player.x, player.y
local dx, dy = 5, 3
player.move(dx, dy)
assert.are_equal(player.x, initial_x + dx, "Player x-coordinate should update correctly")
assert.are_equal(player.y, initial_y + dy, "Player y-coordinate should update correctly")
end
function fixture.test_player_takes_damage_correctly()
local damage = 40
local initial_health = player.health
player.take_damage(damage)
assert.are_equal(player.health, initial_health - damage, "Player health should decrease by the damage amount")
end
function fixture.test_player_healing_limits_to_maximum()
player.take_damage(50)
local heal_amount = 60
player.heal(heal_amount)
assert.are_equal(player.health, 100, "Player health should not exceed 100")
end
return fixture
-
before_each()
: Resets the player state before each test case. -
Test Functions: Each test case checks specific behavior, such as player movement, health management, or inventory handling.
-
after_each()
: Cleans up after each test. -
Error Handling: If a test fails, the system captures and logs the error, continuing to the next test.
Run the tests using run_tests.lua
. The system will execute each test case and log the results.
run_tests.lua
[INFO] Test 'test_player_moves_correctly' passed.
[INFO] Test 'test_player_takes_damage_correctly' passed.
[INFO] Test 'test_player_healing_limits_to_maximum' passed.
The assert.lua
module provides custom assertion functions to assist with testing. Assertions are conditions that must be true during program execution, and if they fail, the program halts and reports an error.
-
Comparison Assertions:
assert.are_equal(actual, expected, message)
: Verifies that two values are equal.assert.are_not_equal(actual, expected, message)
: Verifies that two values are not equal.assert.are_equal_tables(actual, expected, message)
: Verifies that two tables are deeply equal.assert.are_equal_tables_ignore_nil(actual, expected, message)
: Ignoresnil
values during table comparison.
-
Type and Value Assertions:
assert.is_nil(value, message)
: Ensures that the value isnil
.assert.is_not_nil(value, message)
: Ensures the value is notnil
.assert.is_type(value, expected_type, message)
: Ensures that the value is of a specific type.assert.is_true(value, message)
: Ensures the value istrue
.assert.is_false(value, message)
: Ensures the value isfalse
.
-
Number Comparisons:
assert.is_greater_than(actual, threshold, message)
: Ensures a number is greater than a given threshold.assert.is_less_than(actual, threshold, message)
: Ensures a number is less than a given threshold.
-
Table Assertions:
assert.contains(table, value, message)
: Ensures a table contains a specific value.assert.has_key(table, key, message)
: Ensures a table contains a specific key.assert.has_length(table, expected_length, message)
: Ensures a table or string has the expected length.
-
Pattern Matching:
assert.matches_pattern(value, pattern, message)
: Ensures that a string matches a given Lua pattern.
In unit testing, assertions verify that functions behave as expected. Here’s a simple example:
local assert = require("assert")
local player = { health = 100 }
assert.are_equal(player.health, 100, "Player health should start at 100")
player.health = player.health - 20
assert.is_greater_than(player.health, 0, "Player should have health after damage")
player.health = player.health - 90
assert.is_greater_than(player.health, 0, "Player health should not go below zero")
If an assertion fails, an error message is thrown and the program halts.
Example error message for a failed are_equal
assertion:
Assertion failed: expected '100', got '90'
We welcome contributions to the Picotron Game Template! Whether you want to improve existing features, add new ones, or fix bugs, your help is greatly appreciated. To contribute, please follow these steps:
-
Fork the Repository: Fork this repository to your GitHub account by clicking the "Fork" button at the top of the page.
-
Clone Your Fork: Clone the forked repository to your local machine:
git clone https://github.com/yourusername/picotron-game-template.git
-
Create a New Branch: Create a new branch for your feature or bug fix:
git checkout -b my-new-feature
-
Make Your Changes: Implement your feature, bug fix, or improvement. Be sure to write clear, concise, and well-documented code. Ensure that your changes do not break existing functionality by running the unit tests.
-
Test Your Changes: Before submitting your changes, run all tests to ensure that everything is working as expected:
run_tests.lua
-
Commit and Push: Once you're satisfied with your changes, commit your work with a descriptive message and push it to your fork:
git add . git commit -m "Add my feature or fix a bug" git push origin my-new-feature
-
Submit a Pull Request: Open a pull request from your fork back to the main repository. Provide a clear explanation of your changes and why they should be merged.
- Follow the Existing Code Style: Ensure that your code matches the style and conventions used in the rest of the project.
- Write Tests: If applicable, add unit tests for new features or bug fixes.
- Keep It Modular: Ensure your changes are well-structured and maintainable, adhering to the modular nature of the project.
- Document Your Changes: Update relevant sections of the README and add comments to the code where necessary to help others understand your contribution.
If you find a bug or have suggestions for improvements, feel free to open an issue on GitHub. Please provide detailed information to help us understand the issue or your proposal.
We look forward to your contributions!
This project is licensed under the GNU General Public License v3.0. See the LICENSE
file for more details.