Saving Files and Testing Games¶
Two modules of the acron framework deal with things outside the game
loop: acron.persist keeps files between sessions – saves, settings,
progress – and acron.testing drives the real engine from a test or a
tool script.
Saving files¶
A game must start whatever is on disk, and a save must never be left
half-written. acron.persist reads JSON defensively, writes it
atomically, and brings old files forward to the current format. Where the
files live and what they are called is up to the game.
from pathlib import Path
from acron import persist
SAVE = Path.home() / ".mygame" / "save.json"
VERSION = 3
def upgrade_1(d):
d["coins"] = d.pop("gold", 0)
return d
def upgrade_2(d):
d.setdefault("unlocked", [])
return d
def load():
d = persist.read_object(SAVE) or {}
return persist.migrate(d, VERSION, {1: upgrade_1, 2: upgrade_2})
def save(state):
state["v"] = VERSION
if not persist.write_json(SAVE, state):
show_message("The game could not be saved.")
|
the JSON in the file, or |
|
the same, but anything other than a JSON object reads as |
|
writes through a temporary file, an |
|
removes a file; |
|
walks |
migrate() reads the file’s version from its v key
(schema_version()); a file without one, or with one that
is not a positive integer, is version 1. The walk stops at a version the
table has no step for, and a file newer than version is returned as it
is, for the game to take what it recognises.
Testing games¶
acronmanager test (the launcher’s Test button) runs the project’s tests
with pytest, from the project’s environment; acronmanager test -- -k
shadows -x passes arguments on to pytest, and [tool.acron.test] command
in pyproject.toml replaces pytest with a command of the project’s own.
acron.testing has the pieces for tests and tool scripts that run the
game’s real engine rather than a mock.
acron.testing needs numpy, which the web runtime does not have; it is
for tests and tools, not for a shipped game.
Running the app offscreen¶
from acron import testing
testing.offscreen("960x540") # before ShowBase
from direct.showbase.ShowBase import ShowBase
base = ShowBase()
testing.fixed_clock(base) # 1/60 s per frame
start_game(base)
testing.step(base, 120) # two seconds of game time
base.win.save_screenshot("after-two-seconds.png")
~acron.testing.offscreen(size=None)asks for an offscreen buffer instead of a window (sizelike"960x540"). It must run beforeShowBaseis created.~acron.testing.fixed_clock(base, dt=1/60)puts the clock in non-real-time mode with a fixed frame time. Offscreen, frames render as fast as they can, and a real-time clock would leave a fixed-step game loop with too few steps.~acron.testing.step(base, frames)runs that many frames, each of which renders.~acron.testing.after_call(obj, name, extra)wraps the methodobj.nameso thatextra()runs after it, every call. It is how a test drives something that rewrites its own state every frame, such as an input poll: setting the fields directly does not stick, overriding them after the update does.memory_footprint()is the process’s physical memory footprint in bytes, for a test that checks that a released scene really frees its memory.
Comparing frames¶
Some rendering bugs – flicker from state order, wrong winding, a uniform that is never set – leave every value a test can read correct and show only on screen. These functions compare rendered frames:
~acron.testing.frame_rgb(win)returns the window’s last frame as a texture and anint16RGB array;~acron.testing.pair_stats(a, b)returns the mean absolute RGB difference of two such arrays and the percentage of pixels that differ;~acron.testing.flicker_scan(base, frames=40)steps that many frames and returns the stats of each consecutive pair. Stop everything that animates on purpose first; on a still scene, a mean difference above 1 is visible flashing;~acron.testing.gl_spam_frame(base, path)renders one frame with the OpenGL renderer’s call stream (program and texture binds, changed uniforms) written topath, and returns that frame asframe_rgb()does, so the log of a frame that differs can be compared with one that does not.
stats = testing.flicker_scan(base, frames=40)
assert max(mean for mean, pct in stats) <= 1.0
Clicking in a real window¶
acron.testing.ClickDriver(base, *, settle_frames=2) moves the pointer and presses the mouse button
through the window’s own input device, so the click goes through the same
path as a player’s. It runs a few frames between each stage, so that RmlUi
has seen the hover before the press.
press_at(x, y, hold=False)presses at a window pixel, and releases unlesshold;release()releases.click_element(element)clicks the centre of an RmlUi element. Element offsets are already in window pixels; do not scale them by the dp ratio.
acron.testing.Checklist prints PASS/FAIL lines and counts the
failures, for a tool script that proves several things in a row:
checks = testing.Checklist()
clicks = testing.ClickDriver(base)
clicks.click_element(doc.get_element_by_id("btn_start"))
checks.check("the start button starts the game", game.running)
raise SystemExit(checks.exit_code)