From 6569e8d0ee188942f706f90b6c12137a364b53f0 Mon Sep 17 00:00:00 2001 From: DavidCooperWCU Date: Sat, 11 Jul 2026 00:37:56 -0400 Subject: [PATCH 1/3] added gdscript highlighting and packaging 1. gdscript-prism.js does gdscript 2.0 highlighting for code in programs 2. pretext-html.xsl and pretext-runestone.xsl were modified for the highlighting 3. gdscript_pck function in pretext.py uses extract-gdscript.xsl, and godot_helper.py to build godot packs in zip format as generated files. --- js/prism/gdscript-prism.js | 33 ++++++ pretext/lib/godot/project.godot | 25 ++++ pretext/lib/godot_helper.py | 194 ++++++++++++++++++++++++++++++++ pretext/lib/pretext.py | 179 +++++++++++++++++++++++++++++ pretext/pretext | 6 + pretext/pretext.cfg | 1 + schema/publication-schema.xml | 2 +- xsl/extract-gdscript.xsl | 61 ++++++++++ xsl/pretext-common.xsl | 1 + xsl/pretext-html.xsl | 1 + xsl/pretext-runestone.xsl | 37 ++++++ xsl/publisher-variables.xsl | 1 + 12 files changed, 540 insertions(+), 1 deletion(-) create mode 100644 js/prism/gdscript-prism.js create mode 100644 pretext/lib/godot/project.godot create mode 100644 pretext/lib/godot_helper.py create mode 100644 xsl/extract-gdscript.xsl diff --git a/js/prism/gdscript-prism.js b/js/prism/gdscript-prism.js new file mode 100644 index 000000000..a5b41ad49 --- /dev/null +++ b/js/prism/gdscript-prism.js @@ -0,0 +1,33 @@ +(function() { + if (!window.Prism) return; + + // Define grammar using safe string-based RegExp constructors + var gdscript2Grammar = { + 'comment': new RegExp('#.*'), + 'string': { + pattern: new RegExp('(?:r|f|b)?(?:"(?:\\\\.|[^"\\\\])*"|\'(?:\\\\.|[^\'\\\\])*\')', 'i'), + greedy: true + }, + 'annotation': { + pattern: new RegExp('@\\w+'), + alias: 'builtin' + }, + 'keyword': new RegExp('\\b(?:as|assert|await|break|breakpoint|class|class_name|const|continue|enum|export|extends|for|func|if|elif|else|in|is|match|onready|pass|preload|return|self|setget|signal|static|super|tool|var|void|while|yield)\\b'), + 'function': new RegExp('\\b[a-z_]\\w*(?=\\s*\\()', 'i'), + 'number': new RegExp('\\b(?:0b+|0x[\\da-fA-F]+|\\d+(?:\\.\\d+)?(?:e[+-]?\\d+)?)\\b'), + 'boolean': new RegExp('\\b(?:true|false)\\b'), + 'operator': new RegExp('->|:=|&&|\\|\\||[-+*/%&|^!=<>]=?|~'), + 'punctuation': new RegExp('[{}[\\];(),.:]') + }; + + // Hook into Prism's token initialization pipeline + Prism.hooks.add('before-tokenize', function(env) { + if (env.language === 'gdscript') { + Prism.languages.gdscript = gdscript2Grammar; + } + }); + + // Pre-register it globally + Prism.languages.gdscript = gdscript2Grammar; +})(); + \ No newline at end of file diff --git a/pretext/lib/godot/project.godot b/pretext/lib/godot/project.godot new file mode 100644 index 000000000..0760cdfa2 --- /dev/null +++ b/pretext/lib/godot/project.godot @@ -0,0 +1,25 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="GDPractice-for-IGDWG" +config/features=PackedStringArray("4.6", "GL Compatibility") +config/icon="res://icon.svg" + +[physics] + +3d/physics_engine="Jolt Physics" + +[rendering] + +rendering_device/driver.windows="d3d12" +renderer/rendering_method="gl_compatibility" +renderer/rendering_method.mobile="gl_compatibility" diff --git a/pretext/lib/godot_helper.py b/pretext/lib/godot_helper.py new file mode 100644 index 000000000..4ca0ac0f1 --- /dev/null +++ b/pretext/lib/godot_helper.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +"""Resolves (downloading if necessary) a pinned Godot version and its export templates, +then runs addons/gdpractice/export_packs.gd against the project. + +Godot binary resolution order: + 1. `godot` on PATH -- used as-is if its version matches GODOT_VERSION. + If it's a *different* version, a warning is printed and it is NOT used; + we fall through to steps 2/3 instead, so a mismatched PATH install can't + silently reintroduce a version-mismatch bug. + 2. A previously downloaded copy in this script's cache directory + (~/.godot_versions//). + 3. Downloaded fresh from the official godotengine/godot GitHub release for + GODOT_VERSION, into that same cache directory. + +Export templates are checked against (and installed into) based on system-standard +per-OS directories. + +Dependencies: + pip install certifi + + +""" + +import argparse +import platform +import shutil +import ssl +import stat +import subprocess +import sys +import urllib.request +import zipfile +from pathlib import Path + +import certifi + +# Set up logging package: +import logging +log = logging.getLogger('ptxlogger') + + + +GODOT_VERSION = "4.6.3" +GODOT_VERSION_TAG = "{}-stable".format(GODOT_VERSION) +# Matches the "major.minor.patch.status" format Godot itself uses for the +# export-templates directory name (e.g. "4.6.3.stable"). +GODOT_TEMPLATE_VERSION_STRING = "{}.stable".format(GODOT_VERSION) + +GITHUB_RELEASE_BASE = "https://github.com/godotengine/godot/releases/download/{}".format(GODOT_VERSION_TAG) + +CACHE_DIR = Path.home() / ".godot_versions" / GODOT_VERSION + + +# Built from certifi's CA bundle rather than relying on the system/Python installation's +# own certificate store, since python.org-installed Python on macOS in particular often +# has no certificates configured out of the box, causing CERTIFICATE_VERIFY_FAILED. +_SSL_CONTEXT = ssl.create_default_context(cafile=certifi.where()) + + +def _asset_name_for_platform() -> str: + system = platform.system() + machine = platform.machine().lower() + + if system == "Darwin": + return "Godot_v{}_macos.universal.zip".format(GODOT_VERSION_TAG) + elif system == "Linux": + arch = "arm64" if machine in ("aarch64", "arm64") else "x86_64" + return "Godot_v{}_linux.{}.zip".format(GODOT_VERSION_TAG,arch) + elif system == "Windows": + return "Godot_v{}_win64.exe.zip".format(GODOT_VERSION_TAG) + else: + raise RuntimeError("Unsupported platform: {}".format(system)) + + +def _export_templates_asset_name() -> str: + return "Godot_v{}_export_templates.tpz".format(GODOT_VERSION_TAG) + + +def _template_install_dir() -> Path: + system = platform.system() + if system == "Linux": + return Path.home() / ".local/share/godot/export_templates" / GODOT_TEMPLATE_VERSION_STRING + elif system == "Darwin": + return Path.home() / "Library/Application Support/Godot/export_templates" / GODOT_TEMPLATE_VERSION_STRING + elif system == "Windows": + import os + return Path(os.environ["APPDATA"]) / "Godot/export_templates" / GODOT_TEMPLATE_VERSION_STRING + else: + raise RuntimeError("Unsupported platform: {}".format(system)) + + +def _download(url: str, destination: Path) -> None: + log.info("Downloading {} ...".format(url)) + destination.parent.mkdir(parents=True, exist_ok=True) + with urllib.request.urlopen(url, context=_SSL_CONTEXT) as response, open(destination, "wb") as out_file: + shutil.copyfileobj(response, out_file) + + +def _get_godot_version(godot_path: str) -> str | None: + try: + result = subprocess.run([godot_path, "--version"], capture_output=True, text=True, timeout=15) + except (OSError, subprocess.TimeoutExpired): + return None + if result.returncode != 0: + return None + # Godot's --version output looks like "4.6.3.stable.official.7d41c59c4" + return result.stdout.strip() + + +def _cached_binary_path() -> Path | None: + system = platform.system() + if system == "Darwin": + candidate = CACHE_DIR / "Godot.app/Contents/MacOS/Godot" + elif system == "Windows": + candidate = CACHE_DIR / "Godot_v{}_win64.exe".format(GODOT_VERSION_TAG) + else: + candidate = CACHE_DIR / "Godot_v{}_linux.x86_64".format(GODOT_VERSION_TAG) + return candidate if candidate.exists() else None + + +def _download_and_extract_godot() -> Path: + asset_name = _asset_name_for_platform() + zip_path = CACHE_DIR / asset_name + _download("{}/{}".format(GITHUB_RELEASE_BASE,asset_name), zip_path) + + log.info("Extracting {} ...".format(zip_path)) + with zipfile.ZipFile(zip_path) as zf: + zf.extractall(CACHE_DIR) + zip_path.unlink() + + binary_path = _cached_binary_path() + if binary_path is None: + raise RuntimeError( + "Extracted {} but couldn't locate the Godot executable under {}. ".format(asset_name,CACHE_DIR) + + "The archive's internal layout may not match what this script expects -- check its contents manually." + ) + + if platform.system() != "Windows": + binary_path.chmod(binary_path.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH) + + return binary_path + + +def resolve_godot(gd_cmd : str) -> str: + path_godot = shutil.which(gd_cmd) + if path_godot is not None: + version_string = _get_godot_version(path_godot) + if version_string is not None and version_string.startswith(GODOT_VERSION): + log.info("Using Godot on PATH ({}, version {}).".format(path_godot,version_string)) + return path_godot + else: + log.info( + "WARNING: 'godot' on PATH is version '{}', ".format(version_string) + + "not the pinned version '{}'. Ignoring it and using a ".format(GODOT_VERSION) + + "separately managed copy of {} instead.".format(GODOT_VERSION) + ) + + cached = _cached_binary_path() + if cached is not None: + log.info("Using cached Godot {} at {}.".format(GODOT_VERSION,cached)) + return str(cached) + + log.info("Godot {} not found on PATH or in cache; downloading...".format(GODOT_VERSION)) + return str(_download_and_extract_godot()) + + +def ensure_export_templates() -> None: + template_dir = _template_install_dir() + if template_dir.exists() and any(template_dir.iterdir()): + log.info(f"Export templates for {GODOT_VERSION} already installed at {template_dir}.") + return + + asset_name = _export_templates_asset_name() + tpz_path = CACHE_DIR / asset_name + _download("{}/{}".format(GITHUB_RELEASE_BASE,asset_name), tpz_path) + + log.info("Installing export templates to {} ...".format(template_dir)) + template_dir.mkdir(parents=True, exist_ok=True) + # .tpz files are zip archives with a top-level "templates/" folder; Godot expects + # the version directory's contents to be the files *inside* that folder, not the + # folder itself. + with zipfile.ZipFile(tpz_path) as zf: + for member in zf.namelist(): + if not member.startswith("templates/") or member == "templates/": + continue + relative_path = member[len("templates/"):] + target_path = template_dir / relative_path + if member.endswith("/"): + target_path.mkdir(parents=True, exist_ok=True) + continue + target_path.parent.mkdir(parents=True, exist_ok=True) + with zf.open(member) as source, open(target_path, "wb") as out_file: + shutil.copyfileobj(source, out_file) + tpz_path.unlink() diff --git a/pretext/lib/pretext.py b/pretext/lib/pretext.py index 4b60aea2b..f6f3c9d99 100644 --- a/pretext/lib/pretext.py +++ b/pretext/lib/pretext.py @@ -93,6 +93,9 @@ # contextmanager tools import contextlib +# gdscript needs +import pathlib + import time # cleanup multiline strings used as source code @@ -131,6 +134,7 @@ __module_warning = common.__module_warning from . import webwork from . import stack +from . import godot_helper # Not much can be done without the "lxml" module which mimics # the "xsltproc" executable (they share the same libraries) @@ -1580,6 +1584,171 @@ def warn(citation_item): raise ValueError(msg.format(f) + root_cause) +############################## +# +# GDScript interactive packing +# +############################## + +def gd_pack(source_dir, dest_zip_file, pack_name): + source_path = pathlib.Path(source_dir).resolve() + try: + godot_cmd = common.get_executable_cmd("godot") + except Exception as e: + log.error("{}".format(e)) + godot_cmd = "godot" + + godot_cmd = godot_helper.resolve_godot(godot_cmd) + godot_helper.ensure_export_templates() + + PROJECT_FILENAME = "project.godot" + EXPORTS_FILENAME = "export_presets.cfg" + EXPORT_FILENAME = "export_preset.cfg" + + source_config_path = os.path.join(source_path,"practice_solutions",pack_name,EXPORT_FILENAME) + + # Define the destination path for the config file in the project folder + target_config_path = os.path.join(source_path, EXPORTS_FILENAME) + # Define the destination path for the project file in the project folder + target_project_path = os.path.join(source_path, PROJECT_FILENAME) + + tmp_dir = common.get_temporary_directory() + + # Check if a config already exists there (so we don't accidentally overwrite/delete a permanent one) + config_already_existed = os.path.exists(target_config_path) + if config_already_existed: + shutil.copy2(target_config_path, tmp_dir) + + # Check if a project file already exists there (so we don't accidentally overwrite/delete a permanent one) + project_already_existed = os.path.exists(target_project_path) + if project_already_existed: + shutil.copy2(target_project_path, tmp_dir) + + godot_template_path = os.path.join(get_ptx_path(), "pretext","lib","godot") + template_project_path = os.path.join(godot_template_path,PROJECT_FILENAME) + try: + shutil.copy2(source_config_path,target_config_path) + shutil.copy2(template_project_path,source_path) + + # dry run + command = [ + godot_cmd, + "--headless", + "--path", source_path, + "--editor", + "--quit" + ] + + # Execute the command + result = subprocess.run( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + # Check for success or failure of dry run + if result.returncode == 0: + log.info("Trial successful! File saved to: {}".format(dest_zip_file)) + log.info(result.stdout) + log.error(result.stderr) + else: + log.error("Trial failed. Godot Output:") + log.error(result.stdout) + log.error(result.stderr) + raise OSError(result.stderr) + + command = [ + godot_cmd, + "--headless", + "--path", source_path, + "--export-pack", pack_name, + dest_zip_file + ] + + # Execute the command + result = subprocess.run( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + # Check for success or failure + if result.returncode == 0: + log.info("Export successful! File saved to: {}".format(dest_zip_file)) + log.info(result.stdout) + log.error(result.stderr) + else: + log.error("Export failed. Godot Output:") + log.error(result.stdout) + log.error(result.stderr) + raise OSError(result.stderr) + + except Exception as e: + msg = "An error occurred: {}".format(e) + raise OSError(msg) + finally: + tmp_config_file = os.path.join(tmp_dir,EXPORTS_FILENAME) + tmp_project_file = os.path.join(tmp_dir,PROJECT_FILENAME) + if os.path.exists(target_config_path): + if not config_already_existed or os.path.exists(tmp_config_file): + os.remove(target_config_path) + if os.path.exists(target_project_path): + if not config_already_existed or os.path.exists(tmp_project_file): + os.remove(target_project_path) + if config_already_existed: + shutil.copy2(tmp_config_file,source_path) + if project_already_existed: + shutil.copy2(tmp_project_file,source_path) + +def gdscript_pck(xml_source, pub_file, stringparams, xmlid_root, dest_dir): + # to ensure provided stringparams aren't mutated unintentionally + stringparams = stringparams.copy() + source_dir = common.get_source_path(xml_source) + #_, external_dir = common.get_managed_directories(xml_source, pub_file) + log.info( + "exporting GDScript interactives from {} for placement in {}".format( + source_dir, dest_dir + ) + ) + ptx_xsl_dir = common.get_ptx_xsl_path() + extraction_xslt = os.path.join(ptx_xsl_dir, "extract-gdscript.xsl") + # support publisher file, subtree argument + if pub_file: + stringparams["publisher"] = pub_file + if xmlid_root: + stringparams["subtree"] = xmlid_root + # Build list of id's into a scratch directory/file + tmp_dir = common.get_temporary_directory() + id_filename = os.path.join(tmp_dir, "gdscript-ids.txt") + log.debug("GDScript id list temporarily in {}".format(id_filename)) + # this builds the gdscript names + common.xsltproc(extraction_xslt, xml_source, id_filename, None, stringparams) + # "run" an assignment for the list of triples of strings + with open(id_filename, "r") as id_file: + # read lines, but only lines that are comma delimited + triples = [p.strip() for p in id_file.readlines() if "," in p] + for triple in triples: + try: + # first item is destination name, second item is source, + # third item is scene to derive export name + triple_a = triple.split(",") + src = os.path.join(source_dir,triple_a[1]) + # this is the path to the destination file + path = os.path.join(dest_dir, triple_a[0] + ".zip") + # split third item on path separator + parts = triple_a[2].split("/") + # the second to last part should be the problem name + name = parts[-2] + log.info("exporting {} as {} using {} ...".format(src, path, name)) + gd_pack(src, path,name) + except Exception as e: + log.error("Something went wrong with gdscript conversion for {}:\n {}".format(triple,e)) + + log.info("GDScript pck exporting complete") + + ############################## # # You Tube thumbnail scraping @@ -2000,6 +2169,7 @@ def all_images(xml, pub_file, stringparams, xmlid_root): # explore source for various PreTeXt elements needing assistance # no element => empty list => boolean is False + has_gdscript = bool(src_tree.xpath("/pretext/*[not(docinfo)]//program[@pck and @interactive='activecode']")) has_latex_image = bool(src_tree.xpath("/pretext/*[not(docinfo)]//latex-image")) has_asymptote = bool(src_tree.xpath("/pretext/*[not(docinfo)]//asymptote")) has_sageplot = bool(src_tree.xpath("/pretext/*[not(docinfo)]//sageplot")) @@ -2072,6 +2242,15 @@ def all_images(xml, pub_file, stringparams, xmlid_root): sage_conversion(xml, pub_file, stringparams, xmlid_root, dest_dir, "pdf", None) sage_conversion(xml, pub_file, stringparams, xmlid_root, dest_dir, "svg", None) + # gdscript pck + # + if has_gdscript: + dest_dir = os.path.join(generated_dir, "gdscript", "") + if not (os.path.isdir(dest_dir)): + os.mkdir(dest_dir) + # no format, they are what they are (*.jpg) + gdscript_pck(xml, pub_file, stringparams, xmlid_root, dest_dir) + # YouTube previews # if has_youtube: diff --git a/pretext/pretext b/pretext/pretext index 4117ea653..80c8fc207 100755 --- a/pretext/pretext +++ b/pretext/pretext @@ -139,6 +139,7 @@ def get_destination_directory(directory, xml_source, pub_file, component): "latex-package": "latex-packages", "references" : "references", "webwork": "webwork", + "gdscript" : "gdscript" } # if specified, check, sanitize, return absolute path if directory: @@ -370,6 +371,7 @@ def get_cli_arguments(): ("references", "References and citations via Citation Stylesheet Language"), ("pg-macros", "Server-side macros for WeBWorK problem generation"), ("dynamic", "Expression subsitutions for dynamically generated exercises"), + ("gdscript", "use godot to package zip files for GDScript interactives"), ("youtube", "Thumbnails for YouTube videos (JPEG only)"), ("play-button", "generate generic static video image"), ("qrcode", "QR codes for static versions of interactive content"), @@ -768,6 +770,10 @@ def main(): ptx.youtube_thumbnail( xml_source, publication_file, stringparams, args.xmlid, dest_dir ) + elif args.component == "gdscript": + ptx.gdscript_pck( + xml_source, publication_file, stringparams, args.xmlid, dest_dir + ) elif args.component == "play-button": ptx.play_button(dest_dir) elif args.component == "qrcode": diff --git a/pretext/pretext.cfg b/pretext/pretext.cfg index 172c5814e..a56c4f28d 100644 --- a/pretext/pretext.cfg +++ b/pretext/pretext.cfg @@ -70,3 +70,4 @@ liblouis = file2brl perl = perl fop = fop jing = jing +godot = godot diff --git a/schema/publication-schema.xml b/schema/publication-schema.xml index 848d318fd..bddcba3f9 100644 --- a/schema/publication-schema.xml +++ b/schema/publication-schema.xml @@ -438,7 +438,7 @@ Calculator = element calculator { attribute model { "geogebra-classic" | "geogebra-graphing" | "geogebra-geometry" | "geogebra-3d" | "none "}?, - attribute activecode { "python" | "javascript" | "html" | "sql" | "c" | "cpp" | "java" | "kotlin" | "python3" | "octave" | "none"}? + attribute activecode { "python" | "javascript" | "html" | "sql" | "c" | "cpp" | "gdscript" | "java" | "kotlin" | "python3" | "octave" | "none"}? } Crossreferences = diff --git a/xsl/extract-gdscript.xsl b/xsl/extract-gdscript.xsl new file mode 100644 index 000000000..da2a46656 --- /dev/null +++ b/xsl/extract-gdscript.xsl @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + , + + , + + + + diff --git a/xsl/pretext-common.xsl b/xsl/pretext-common.xsl index 2db3b303d..55ec2612c 100644 --- a/xsl/pretext-common.xsl +++ b/xsl/pretext-common.xsl @@ -7352,6 +7352,7 @@ Book (with parts), "section" at level 3 + diff --git a/xsl/pretext-html.xsl b/xsl/pretext-html.xsl index 5dc32b24b..cc73e06be 100644 --- a/xsl/pretext-html.xsl +++ b/xsl/pretext-html.xsl @@ -14219,6 +14219,7 @@ TODO: + diff --git a/xsl/pretext-runestone.xsl b/xsl/pretext-runestone.xsl index 2f7a2d5ca..11b132562 100644 --- a/xsl/pretext-runestone.xsl +++ b/xsl/pretext-runestone.xsl @@ -59,6 +59,13 @@ along with PreTeXt. If not, see . + + + + + + + @@ -2568,6 +2575,35 @@ along with PreTeXt. If not, see . + + + + + + + + + + + + + + + + + gdscript/ + + + .zip + + + + + + + + + @@ -2739,6 +2775,7 @@ along with PreTeXt. If not, see . browser browser browser + browser jobeserver jobeserver jobeserver diff --git a/xsl/publisher-variables.xsl b/xsl/publisher-variables.xsl index 3388fac28..011578d6f 100644 --- a/xsl/publisher-variables.xsl +++ b/xsl/publisher-variables.xsl @@ -2067,6 +2067,7 @@ along with PreTeXt. If not, see . From 0040338830a8b95474f45c2d9657944ce1e52f35 Mon Sep 17 00:00:00 2001 From: DavidCooperWCU Date: Sat, 11 Jul 2026 00:44:10 -0400 Subject: [PATCH 2/3] documented gdscript addition --- doc/guide/author/topics.xml | 10 +++++++++- doc/guide/publisher/publication-file.xml | 1 + doc/guide/publisher/runestone.xml | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/doc/guide/author/topics.xml b/doc/guide/author/topics.xml index 25480e2cd..678d91c0e 100644 --- a/doc/guide/author/topics.xml +++ b/doc/guide/author/topics.xml @@ -4048,7 +4048,7 @@ Tests for Interactive Programs -

There are currently two ways to make automated tests for ActiveCode programs. The first way is to simply provide code in the tests element of the program as described in . It will be assumed this code is unit tests and it will be run automatically with the user's submission.

+

There are currently two main ways to make automated tests for ActiveCode programs. The first way is to simply provide code in the tests element of the program as described in . It will be assumed this code is unit tests and it will be run automatically with the user's submission.

Below is an example of unit tested Python. For more examples, see the Coding Exercises section of the Sample Book

@@ -4096,6 +4096,8 @@

Structured IO tests are added as iotests to the tests element. Each iotest element must have a single input and output element. The input element contains the input that will be fed to the program, and the match element contains the expected output of the program. Indentation for both will be normalized in the same way that programs are (so multiple-line sample output can be indented in your source document).

When evaluated, the program output and test output will have leading and trailing spaces removed. Other than that spacing, the two must match exactly to be considered a "passed" test.

+ +

GDScript has it's own way to test based on the GDPractice addon library. The sample book has three examples of testing with gdscript.

@@ -4133,6 +4135,12 @@ AC + IO + + GDScript + gdscript + AC + AC + Java java diff --git a/doc/guide/publisher/publication-file.xml b/doc/guide/publisher/publication-file.xml index 55cfdf320..f583bffd2 100644 --- a/doc/guide/publisher/publication-file.xml +++ b/doc/guide/publisher/publication-file.xml @@ -612,6 +612,7 @@
  • javascript (JavaScript)
  • html (HTML)
  • sql (SQL)
  • +
  • gdscript (GDScript)
  • c (C, Runestone server only)
  • cpp (C++, Runestone server only)
  • java (Java, Runestone server only)
  • diff --git a/doc/guide/publisher/runestone.xml b/doc/guide/publisher/runestone.xml index f68b2edd6..4c03b53d5 100644 --- a/doc/guide/publisher/runestone.xml +++ b/doc/guide/publisher/runestone.xml @@ -29,7 +29,7 @@
  • As much as possible, non-interactive versions of these problems will render in less-capable formats, like PDF, EPUB, and braille.
  • -
  • A program element with the attribute interactive set to activecode (even outside of a an exercise) will be realized as a Runestone ActiveCode interactive program, where programs can be edited, compiled, and run. In some cases a CodeLens interactive trace utility is also available. The language must be set. Supported values for the language when hosted at Runestone are: python, python3, c, cpp (C++), javascript, java, kotlin, octave (Matlab), sql, and html. When hosted on your own server, python, javascript, sql, and html, are supported with in-browser routines. So you do not need to configure anything server-side for this capability. See subsections of for details.
  • +
  • A program element with the attribute interactive set to activecode (even outside of a an exercise) will be realized as a Runestone ActiveCode interactive program, where programs can be edited, compiled, and run. In some cases a CodeLens interactive trace utility is also available. The language must be set. Supported values for the language when hosted at Runestone are: python, python3, c, cpp (C++), gdscript, javascript, java, kotlin, octave (Matlab), sql, and html. When hosted on your own server, python, javascript, sql, and html, are supported with in-browser routines. So you do not need to configure anything server-side for this capability. See subsections of for details.
  • Similarly, a program element with the attribute interactive set to codelens (even outside of a an exercise) will be realized as a Runestone CodeLens interactive program. This allows a reader to step through the program, much like in a debugger, but with more informative displays of the intermediate state of the program (and nothing like breakpoints or changing variable's values). This ability varies by language, and by hosting location. See subsections of for details.
  • From 0413007b7fade74ef14435186f3286a00d9640bc Mon Sep 17 00:00:00 2001 From: DavidCooperWCU Date: Sat, 11 Jul 2026 00:48:17 -0400 Subject: [PATCH 3/3] added gdscript examples 1 noninteractive example in the programs section 3 activecode examples in the activecode section, and 2 noninteractive programs in one of the activities as part of an activecode example. --- examples/sample-book/gdpractice/.gitignore | 3 + .../L1.P1.double_value/export_preset.cfg | 50 ++++ .../L1.P1.double_value/practice.gd | 9 + .../L1.P1.double_value/practice.tscn | 6 + .../L1.P1.double_value/.file_checksums.json | 4 + .../practices/L1.P1.double_value/practice.gd | 9 + .../L1.P1.double_value/practice.tscn | 6 + .../gdpractice/L2.P9.visual_2d/icon.svg | 1 + .../export_preset.cfg | 50 ++++ .../L2.P9.visual_2d_input_motion/practice.gd | 8 + .../practice.tscn | 15 ++ .../L2.P9.visual_2d_input_motion/practice.gd | 0 .../practice.tscn | 12 + .../gdpractice/first_script/icon.svg | 1 + .../first_script/export_preset.cfg | 50 ++++ .../first_script/practice.gd | 11 + .../first_script/practice.tscn | 9 + .../practices/first_script/practice.tscn | 7 + .../gdpractice/first_script/project.binary | Bin 0 -> 460 bytes examples/sample-book/rune.xml | 224 ++++++++++++++++++ 20 files changed, 475 insertions(+) create mode 100644 examples/sample-book/gdpractice/.gitignore create mode 100644 examples/sample-book/gdpractice/L1.P1.double_value/practice_solutions/L1.P1.double_value/export_preset.cfg create mode 100644 examples/sample-book/gdpractice/L1.P1.double_value/practice_solutions/L1.P1.double_value/practice.gd create mode 100644 examples/sample-book/gdpractice/L1.P1.double_value/practice_solutions/L1.P1.double_value/practice.tscn create mode 100644 examples/sample-book/gdpractice/L1.P1.double_value/practices/L1.P1.double_value/.file_checksums.json create mode 100644 examples/sample-book/gdpractice/L1.P1.double_value/practices/L1.P1.double_value/practice.gd create mode 100644 examples/sample-book/gdpractice/L1.P1.double_value/practices/L1.P1.double_value/practice.tscn create mode 100644 examples/sample-book/gdpractice/L2.P9.visual_2d/icon.svg create mode 100644 examples/sample-book/gdpractice/L2.P9.visual_2d/practice_solutions/L2.P9.visual_2d_input_motion/export_preset.cfg create mode 100644 examples/sample-book/gdpractice/L2.P9.visual_2d/practice_solutions/L2.P9.visual_2d_input_motion/practice.gd create mode 100644 examples/sample-book/gdpractice/L2.P9.visual_2d/practice_solutions/L2.P9.visual_2d_input_motion/practice.tscn create mode 100644 examples/sample-book/gdpractice/L2.P9.visual_2d/practices/L2.P9.visual_2d_input_motion/practice.gd create mode 100644 examples/sample-book/gdpractice/L2.P9.visual_2d/practices/L2.P9.visual_2d_input_motion/practice.tscn create mode 100644 examples/sample-book/gdpractice/first_script/icon.svg create mode 100644 examples/sample-book/gdpractice/first_script/practice_solutions/first_script/export_preset.cfg create mode 100644 examples/sample-book/gdpractice/first_script/practice_solutions/first_script/practice.gd create mode 100644 examples/sample-book/gdpractice/first_script/practice_solutions/first_script/practice.tscn create mode 100644 examples/sample-book/gdpractice/first_script/practices/first_script/practice.tscn create mode 100644 examples/sample-book/gdpractice/first_script/project.binary diff --git a/examples/sample-book/gdpractice/.gitignore b/examples/sample-book/gdpractice/.gitignore new file mode 100644 index 000000000..3a2c8a6a0 --- /dev/null +++ b/examples/sample-book/gdpractice/.gitignore @@ -0,0 +1,3 @@ +*.uid +*.import +.godot diff --git a/examples/sample-book/gdpractice/L1.P1.double_value/practice_solutions/L1.P1.double_value/export_preset.cfg b/examples/sample-book/gdpractice/L1.P1.double_value/practice_solutions/L1.P1.double_value/export_preset.cfg new file mode 100644 index 000000000..c8967289c --- /dev/null +++ b/examples/sample-book/gdpractice/L1.P1.double_value/practice_solutions/L1.P1.double_value/export_preset.cfg @@ -0,0 +1,50 @@ +[preset.0] + +name="L1.P1.double_value" +platform="Web" +runnable=true +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="" +patches=PackedStringArray() +patch_delta_encoding=false +patch_delta_compression_level_zstd=19 +patch_delta_min_reduction=0.1 +patch_delta_include_filters="*" +patch_delta_exclude_filters="" +encryption_include_filters="" +encryption_exclude_filters="" +seed=0 +encrypt_pck=false +encrypt_directory=false +script_export_mode=2 +export_files=PackedStringArray() + +[preset.0.options] + +custom_template/debug="" +custom_template/release="" +variant/extensions_support=false +variant/thread_support=false +vram_texture_compression/for_desktop=true +vram_texture_compression/for_mobile=false +html/export_icon=true +html/custom_html_shell="" +html/head_include="" +html/canvas_resize_policy=2 +html/focus_canvas_on_start=true +html/experimental_virtual_keyboard=false +progressive_web_app/enabled=false +progressive_web_app/ensure_cross_origin_isolation_headers=true +progressive_web_app/offline_page="" +progressive_web_app/display=1 +progressive_web_app/orientation=0 +progressive_web_app/icon_144x144="" +progressive_web_app/icon_180x180="" +progressive_web_app/icon_512x512="" +progressive_web_app/background_color=Color(0, 0, 0, 1) +threads/emscripten_pool_size=8 +threads/godot_pool_size=4 diff --git a/examples/sample-book/gdpractice/L1.P1.double_value/practice_solutions/L1.P1.double_value/practice.gd b/examples/sample-book/gdpractice/L1.P1.double_value/practice_solutions/L1.P1.double_value/practice.gd new file mode 100644 index 000000000..71c37a997 --- /dev/null +++ b/examples/sample-book/gdpractice/L1.P1.double_value/practice_solutions/L1.P1.double_value/practice.gd @@ -0,0 +1,9 @@ +# L1.P1 — Double the value +# +# Complete get_double() so that it returns twice its input. +extends Node + + +# Returns double `value`. +func get_double(value: int) -> int: + return value * 2 # return value diff --git a/examples/sample-book/gdpractice/L1.P1.double_value/practice_solutions/L1.P1.double_value/practice.tscn b/examples/sample-book/gdpractice/L1.P1.double_value/practice_solutions/L1.P1.double_value/practice.tscn new file mode 100644 index 000000000..e4eeb1d48 --- /dev/null +++ b/examples/sample-book/gdpractice/L1.P1.double_value/practice_solutions/L1.P1.double_value/practice.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://practice_solutions/L1.P1.double_value/practice.gd" id="1_practice"] + +[node name="DoubleValue" type="Node"] +script = ExtResource("1_practice") diff --git a/examples/sample-book/gdpractice/L1.P1.double_value/practices/L1.P1.double_value/.file_checksums.json b/examples/sample-book/gdpractice/L1.P1.double_value/practices/L1.P1.double_value/.file_checksums.json new file mode 100644 index 000000000..84c0f2e13 --- /dev/null +++ b/examples/sample-book/gdpractice/L1.P1.double_value/practices/L1.P1.double_value/.file_checksums.json @@ -0,0 +1,4 @@ +{ + "practice.gd": "dd1c49fd44f1767b1b8417209dfe21f3", + "practice.tscn": "06200c4c9124d206646c1778fb846492" +} \ No newline at end of file diff --git a/examples/sample-book/gdpractice/L1.P1.double_value/practices/L1.P1.double_value/practice.gd b/examples/sample-book/gdpractice/L1.P1.double_value/practices/L1.P1.double_value/practice.gd new file mode 100644 index 000000000..fa80a9e33 --- /dev/null +++ b/examples/sample-book/gdpractice/L1.P1.double_value/practices/L1.P1.double_value/practice.gd @@ -0,0 +1,9 @@ +# L1.P1 — Double the value +# +# Complete get_double() so that it returns twice its input. +extends Node + + +# Returns double `value`. +func get_double(value: int) -> int: + return value diff --git a/examples/sample-book/gdpractice/L1.P1.double_value/practices/L1.P1.double_value/practice.tscn b/examples/sample-book/gdpractice/L1.P1.double_value/practices/L1.P1.double_value/practice.tscn new file mode 100644 index 000000000..d7e129376 --- /dev/null +++ b/examples/sample-book/gdpractice/L1.P1.double_value/practices/L1.P1.double_value/practice.tscn @@ -0,0 +1,6 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="res://practices/L1.P1.double_value/practice.gd" id="1_bmi4d"] + +[node name="DoubleValue" type="Node" unique_id=2012261070] +script = ExtResource("1_bmi4d") diff --git a/examples/sample-book/gdpractice/L2.P9.visual_2d/icon.svg b/examples/sample-book/gdpractice/L2.P9.visual_2d/icon.svg new file mode 100644 index 000000000..b370ceb72 --- /dev/null +++ b/examples/sample-book/gdpractice/L2.P9.visual_2d/icon.svg @@ -0,0 +1 @@ + diff --git a/examples/sample-book/gdpractice/L2.P9.visual_2d/practice_solutions/L2.P9.visual_2d_input_motion/export_preset.cfg b/examples/sample-book/gdpractice/L2.P9.visual_2d/practice_solutions/L2.P9.visual_2d_input_motion/export_preset.cfg new file mode 100644 index 000000000..214e52916 --- /dev/null +++ b/examples/sample-book/gdpractice/L2.P9.visual_2d/practice_solutions/L2.P9.visual_2d_input_motion/export_preset.cfg @@ -0,0 +1,50 @@ +[preset.0] + +name="L2.P9.visual_2d_input_motion" +platform="Web" +runnable=true +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="" +patches=PackedStringArray() +patch_delta_encoding=false +patch_delta_compression_level_zstd=19 +patch_delta_min_reduction=0.1 +patch_delta_include_filters="*" +patch_delta_exclude_filters="" +encryption_include_filters="" +encryption_exclude_filters="" +seed=0 +encrypt_pck=false +encrypt_directory=false +script_export_mode=2 +export_files=PackedStringArray() + +[preset.0.options] + +custom_template/debug="" +custom_template/release="" +variant/extensions_support=false +variant/thread_support=false +vram_texture_compression/for_desktop=true +vram_texture_compression/for_mobile=false +html/export_icon=true +html/custom_html_shell="" +html/head_include="" +html/canvas_resize_policy=2 +html/focus_canvas_on_start=true +html/experimental_virtual_keyboard=false +progressive_web_app/enabled=false +progressive_web_app/ensure_cross_origin_isolation_headers=true +progressive_web_app/offline_page="" +progressive_web_app/display=1 +progressive_web_app/orientation=0 +progressive_web_app/icon_144x144="" +progressive_web_app/icon_180x180="" +progressive_web_app/icon_512x512="" +progressive_web_app/background_color=Color(0, 0, 0, 1) +threads/emscripten_pool_size=8 +threads/godot_pool_size=4 diff --git a/examples/sample-book/gdpractice/L2.P9.visual_2d/practice_solutions/L2.P9.visual_2d_input_motion/practice.gd b/examples/sample-book/gdpractice/L2.P9.visual_2d/practice_solutions/L2.P9.visual_2d_input_motion/practice.gd new file mode 100644 index 000000000..73768addd --- /dev/null +++ b/examples/sample-book/gdpractice/L2.P9.visual_2d/practice_solutions/L2.P9.visual_2d_input_motion/practice.gd @@ -0,0 +1,8 @@ +extends Node2D + +const SPEED := 200.0 + + +func _physics_process(delta: float) -> void: + if Input.is_action_pressed("move_right"): + position.x += SPEED * delta # position.x += 0 diff --git a/examples/sample-book/gdpractice/L2.P9.visual_2d/practice_solutions/L2.P9.visual_2d_input_motion/practice.tscn b/examples/sample-book/gdpractice/L2.P9.visual_2d/practice_solutions/L2.P9.visual_2d_input_motion/practice.tscn new file mode 100644 index 000000000..25b7f70ea --- /dev/null +++ b/examples/sample-book/gdpractice/L2.P9.visual_2d/practice_solutions/L2.P9.visual_2d_input_motion/practice.tscn @@ -0,0 +1,15 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="res://practice_solutions/L2.P9.visual_2d_input_motion/practice.gd" id="2_9_practice_script"] + + +[node name="Visual2DInputMotion" type="Node2D"] +script = ExtResource("2_9_practice_script") + + + +[node name="ColorRect" type="ColorRect" parent="."] +offset_left = -25.0 +offset_top = -25.0 +offset_right = 25.0 +offset_bottom = 25.0 diff --git a/examples/sample-book/gdpractice/L2.P9.visual_2d/practices/L2.P9.visual_2d_input_motion/practice.gd b/examples/sample-book/gdpractice/L2.P9.visual_2d/practices/L2.P9.visual_2d_input_motion/practice.gd new file mode 100644 index 000000000..e69de29bb diff --git a/examples/sample-book/gdpractice/L2.P9.visual_2d/practices/L2.P9.visual_2d_input_motion/practice.tscn b/examples/sample-book/gdpractice/L2.P9.visual_2d/practices/L2.P9.visual_2d_input_motion/practice.tscn new file mode 100644 index 000000000..0b2b1d44e --- /dev/null +++ b/examples/sample-book/gdpractice/L2.P9.visual_2d/practices/L2.P9.visual_2d_input_motion/practice.tscn @@ -0,0 +1,12 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="res://practices/L2.P9.visual_2d_input_motion/practice.gd" id="1_edxaw"] + +[node name="Visual2DInputMotion" type="Node2D" unique_id=2142483323] +script = ExtResource("1_edxaw") + +[node name="ColorRect" type="ColorRect" parent="." unique_id=1691221339] +offset_left = -25.0 +offset_top = -25.0 +offset_right = 25.0 +offset_bottom = 25.0 diff --git a/examples/sample-book/gdpractice/first_script/icon.svg b/examples/sample-book/gdpractice/first_script/icon.svg new file mode 100644 index 000000000..c6bbb7d82 --- /dev/null +++ b/examples/sample-book/gdpractice/first_script/icon.svg @@ -0,0 +1 @@ + diff --git a/examples/sample-book/gdpractice/first_script/practice_solutions/first_script/export_preset.cfg b/examples/sample-book/gdpractice/first_script/practice_solutions/first_script/export_preset.cfg new file mode 100644 index 000000000..d42fb4149 --- /dev/null +++ b/examples/sample-book/gdpractice/first_script/practice_solutions/first_script/export_preset.cfg @@ -0,0 +1,50 @@ +[preset.0] + +name="first_script" +platform="Web" +runnable=true +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="" +patches=PackedStringArray() +patch_delta_encoding=false +patch_delta_compression_level_zstd=19 +patch_delta_min_reduction=0.1 +patch_delta_include_filters="*" +patch_delta_exclude_filters="" +encryption_include_filters="" +encryption_exclude_filters="" +seed=0 +encrypt_pck=false +encrypt_directory=false +script_export_mode=2 +export_files=PackedStringArray() + +[preset.0.options] + +custom_template/debug="" +custom_template/release="" +variant/extensions_support=false +variant/thread_support=false +vram_texture_compression/for_desktop=true +vram_texture_compression/for_mobile=false +html/export_icon=true +html/custom_html_shell="" +html/head_include="" +html/canvas_resize_policy=2 +html/focus_canvas_on_start=true +html/experimental_virtual_keyboard=false +progressive_web_app/enabled=false +progressive_web_app/ensure_cross_origin_isolation_headers=true +progressive_web_app/offline_page="" +progressive_web_app/display=1 +progressive_web_app/orientation=0 +progressive_web_app/icon_144x144="" +progressive_web_app/icon_180x180="" +progressive_web_app/icon_512x512="" +progressive_web_app/background_color=Color(0, 0, 0, 1) +threads/emscripten_pool_size=8 +threads/godot_pool_size=4 diff --git a/examples/sample-book/gdpractice/first_script/practice_solutions/first_script/practice.gd b/examples/sample-book/gdpractice/first_script/practice_solutions/first_script/practice.gd new file mode 100644 index 000000000..b3d4bbc1b --- /dev/null +++ b/examples/sample-book/gdpractice/first_script/practice_solutions/first_script/practice.gd @@ -0,0 +1,11 @@ +extends Sprite2D + +var speed = 400 +var angular_speed = PI + +func _init(): + pass + + +func _process(delta): + rotation += angular_speed * delta diff --git a/examples/sample-book/gdpractice/first_script/practice_solutions/first_script/practice.tscn b/examples/sample-book/gdpractice/first_script/practice_solutions/first_script/practice.tscn new file mode 100644 index 000000000..b026da246 --- /dev/null +++ b/examples/sample-book/gdpractice/first_script/practice_solutions/first_script/practice.tscn @@ -0,0 +1,9 @@ +[gd_scene format=3 uid="uid://bjhwjm0antqag"] + +[ext_resource type="Texture2D" uid="uid://bdpv0unclpdb7" path="res://icon.svg" id="1_8yiog"] +[ext_resource type="Script" uid="uid://emhnchkq5m3g" path="res://practice_solutions/first_script/practice.gd" id="2_i5sem"] + +[node name="PracticeSlot" type="Sprite2D" unique_id=171888414] +position = Vector2(584, 314) +texture = ExtResource("1_8yiog") +script = ExtResource("2_i5sem") diff --git a/examples/sample-book/gdpractice/first_script/practices/first_script/practice.tscn b/examples/sample-book/gdpractice/first_script/practices/first_script/practice.tscn new file mode 100644 index 000000000..471455215 --- /dev/null +++ b/examples/sample-book/gdpractice/first_script/practices/first_script/practice.tscn @@ -0,0 +1,7 @@ +[gd_scene format=3 uid="uid://bpn5euisle6xo"] + +[ext_resource type="Texture2D" uid="uid://bdpv0unclpdb7" path="res://icon.svg" id="1_14gxo"] + +[node name="PracticeSlot" type="Sprite2D" unique_id=1816805197] +position = Vector2(584, 314) +texture = ExtResource("1_14gxo") diff --git a/examples/sample-book/gdpractice/first_script/project.binary b/examples/sample-book/gdpractice/first_script/project.binary new file mode 100644 index 0000000000000000000000000000000000000000..db668c9743c9b5433c85f1312ba754542a145716 GIT binary patch literal 460 zcma)&%}N6?6orE*ZbTFW6*q%!)Wrm?xNuu+MO=tJzz~z$^ukOMk~pPr?};6uiz+S- zC)}TpbMrW!Om~G4$B+u4Wvw!MC$)Dow$iDNDzvtt6KKLQ<#!!9#feW}L23&4DE%Xx{?Z%G)5N`kV!4}tn1WvSMww?dqv z7Us*uI?0id!?Pi0+0oIkxCejsZLVHdlULs$XDH$h8EJSmhI-Z{P2`?py~pn_KwB1^~r1Qb&32%}?-xux+E)Z@&H?qV7EYi2fpd0dCWQ&Hw-a literal 0 HcmV?d00001 diff --git a/examples/sample-book/rune.xml b/examples/sample-book/rune.xml index fdfe80a9a..113b0fc32 100644 --- a/examples/sample-book/rune.xml +++ b/examples/sample-book/rune.xml @@ -72,6 +72,23 @@ along with MathBook XML. If not, see . + + + An non-interactive GDScript program, using <pubtitle>Runestone</pubtitle> + + + extends Node + + func _ready(): + print("Hello, World!") + + + + + +
    @@ -88,6 +105,7 @@ along with MathBook XML. If not, see . +

    Otherwise they are rendered as text with syntax coloring. Either way, if a language is not specified, docinfo/programs/@language will be checked to determine what language to assume the code is written in.

    @@ -116,6 +134,212 @@ along with MathBook XML. If not, see . +

    A GDScript program will run interactively in the browser.

    + + + An interactive GDScript program, using <pubtitle>Runestone</pubtitle> +

    Run the following code. + You'll need to fix the function `get_double` that takes 1 `int` parameter and returns an `int` for the test to pass.

    + + + # Complete get_double() so that it returns twice its input. + extends Node + + + # Returns double `value`. + func get_double(value: int) -> int: + return value + + + extends PracticeTest + + ## L1.P1 — Double the value + ## + ## A "simple" GDPractice example: no scene state and no _test_space — just a + ## direct comparison of get_double() between the practice and the solution via + ## _call_all(), plus a structural guardrail from requirements.gd. + + const Requirements := preload("res://addons/gdpractice/tester/requirements.gd") + + ## Inputs used to compare get_double() between the practice and the solution. + const TEST_VALUES: Array[int] = [0, 1, 2, 5, -3] + + + func _build_requirements() -> void: + super() + Requirements.setup(_practice_base_path) + _add_callable_requirement( + "get_double()'s signature must not change", + func() -> String: + return ( + "" + if Requirements._check_methods() + else "Don't change get_double()'s parameters or return type — only fill in its body." + ) + ) + + + func _build_checks() -> void: + for test_value: int in TEST_VALUES: + # GDScript lambdas capture local variables by value, so each closure + # below keeps its own snapshot of `test_value`. + _add_simple_check( + "get_double(%d) returns %d" % [test_value, test_value * 2], + func() -> String: + var result := _call_all("get_double", [test_value]) + if result.practice != result.solution: + return ( + "Expected get_double(%d) to return %d, but got %s." + % [test_value, result.solution, result.practice] + ) + return "" + ) + + + +
    + +

    Here's a GDScript program with some graphics.

    + + + An interactive GDScript program, with graphics +

    This is the first script from the Godot getting started step-by-step example. + The first thing to try is adding the following code: + + + var speed = 400 + var angular_speed = PI + + func _process(delta): + rotation += angular_speed * delta + + + + Then change it to the following code: + + + var speed = 400 + var angular_speed = PI + + func _process(delta): + rotation += angular_speed * delta + + var velocity = Vector2.UP.rotated(rotation) * speed + + position += velocity * delta + + +

    + + + extends Sprite2D + + func _init(): + pass + + + + extends PracticeTest + + + func _build_checks() -> void: + var practice_slot = _practice + + var c1 := Check.new() + c1.description = "Run graphical program" + c1.checker = func() -> String: + return "" + + + + checks.append_array([c1]) + + + +
    + + +

    Here's a GDScript program with split graphics. testing making a change

    + + + An interactive GDScript program, with split graphics +

    This is an example of split screen code. +

    + + + extends Node2D + + const SPEED := 200.0 + + + func _physics_process(delta: float) -> void: + if Input.is_action_pressed("move_right"): + position.x += 0 + + + + + extends PracticeTest + + func _init() -> void: + side_by_side = true + if not InputMap.has_action("move_right"): + InputMap.add_action("move_right") + var event := InputEventKey.new() + event.keycode = KEY_D + InputMap.action_add_event("move_right", event) + + + func _build_requirements() -> void: + super() + _add_actions_requirement(["move_right"]) + + + func _setup_state() -> void: + _practice.position = Vector2.ZERO + _solution.position = Vector2.ZERO + + + func _setup_populate_test_space() -> void: + Input.action_press("move_right") + await _connect_timed(0.5, get_tree().physics_frame, _populate_test_space) + Input.action_release("move_right") + + + func _populate_test_space() -> void: + _test_space.append({ + practice_position = _practice.position, + solution_position = _solution.position, + }) + + + func _build_checks() -> void: + _add_simple_check( + "holding move_right should move the practice node to match the solution", + func() -> String: + var last: Dictionary = _test_space.back() + return ( + "" + if last.practice_position.is_equal_approx(last.solution_position) + else ( + "position doesn't match the solution. Make sure position.x is " + + "updated based on SPEED in _physics_process() when move_right is pressed." + ) + ) + ) + + + + +
    +

    A Java program will only be interactive if hosted on a Runestone server.