{"id":20591659,"url":"https://github.com/gdquest/gdpractice","last_synced_at":"2025-08-20T20:32:57.601Z","repository":{"id":232010664,"uuid":"666716035","full_name":"GDQuest/GDPractice","owner":"GDQuest","description":"A framework to create interactive coding practices in the Godot engine. ","archived":false,"fork":false,"pushed_at":"2025-04-03T13:14:51.000Z","size":1602,"stargazers_count":58,"open_issues_count":1,"forks_count":1,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-04-03T14:26:08.372Z","etag":null,"topics":["education","godot","godot-4","godot-engine"],"latest_commit_sha":null,"homepage":"","language":"GDScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/GDQuest.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"custom":["https://gdquest.mavenseed.com/"]}},"created_at":"2023-07-15T10:40:04.000Z","updated_at":"2025-04-03T13:14:55.000Z","dependencies_parsed_at":"2024-05-02T08:35:53.601Z","dependency_job_id":"f41b33f3-c3f6-4577-98e3-6c9766d8a61d","html_url":"https://github.com/GDQuest/GDPractice","commit_stats":null,"previous_names":["gdquest/godot-practice-framework"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/GDQuest/GDPractice","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GDQuest%2FGDPractice","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GDQuest%2FGDPractice/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GDQuest%2FGDPractice/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GDQuest%2FGDPractice/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/GDQuest","download_url":"https://codeload.github.com/GDQuest/GDPractice/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GDQuest%2FGDPractice/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":271378683,"owners_count":24749193,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-20T02:00:09.606Z","response_time":69,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["education","godot","godot-4","godot-engine"],"created_at":"2024-11-16T07:41:12.485Z","updated_at":"2025-08-20T20:32:57.109Z","avatar_url":"https://github.com/GDQuest.png","language":"GDScript","funding_links":["https://gdquest.mavenseed.com/"],"categories":[],"sub_categories":[],"readme":"# GDPractice\n\nGDPractice is a complete solution for creating interactive coding practices in the Godot engine.\n\nAfter iterating over solutions for interactive practices, which we first open-sourced in [Learn GDScript from Zero](https://github.com/GDQuest/learn-gdscript), we went back to the drawing board to create a more robust and flexible solution with Godot 4. GDPractice is the result of that work. It's already used in [Learn 2D Gamedev From Zero with Godot 4](https://school.gdquest.com/products/learn_2d_gamedev_godot_4/) to teach game development to high school students and aspiring developers worldwide.\n\n## Features\n\n- **Requirements and checks:** You can register requirements and checks to validate the user's work. Requirements are prerequisites needed for your practice tests to run that help avoid errors if learners remove or rename properties, functions, etc. Checks are the actual tests that validate the user's work. The framework provides a simple API to create common checks and requirements.\n- **Test space:** To make practices suitable for teaching gamedev, writing unit tests that check the learner's code is not enough. A lot of production game code is not designed to be testable with unit tests. What if you want to check that the player is looking at the mouse each frame or that entering an area triggers a particular animation? GDPractice allows you to apply parameters of the learner's code to the solution at runtime, capture state from both the solution and the practice copy, and compare the two over time to validate the learner's work.\n- **Simulate and display simulated input:** You can simulate input events required to test the user's work deterministically*. GDPractice captures these events and displays them to the learner.\n- **Build system:**\n    - The framework generates practice starter files based on the solution, so there's a single source of truth for each practice. It supports diffing scenes, scripts, and other resources.\n    - You can generate two projects from a single Godot project: a workbook project for the user to complete the practices and a solution project with the correct solutions. It allows teachers to control the solutions and distribute the workbook projects to students.\n    - Change any project settings when generating workbook projects. For example, you can remove all input actions from the workbook project if the practice requires the learner to create them.\n- **Hides addon files:** GDPractice hides the addons/ and other files from the user in the workbook project to offer them a more streamlined experience browsing the project. The files are hidden from the FileSystem dock and quick picker dialogs.\n\n\\* Note that the practices' behavior can vary a little depending on the system, the learner's framerate, and input devices.\n\n## How to integrate into other projects\n\nYou can copy three of the addons/ in this repository to your project to use GDPractice:\n\n- `addons/gdpractice/`\n- `addons/gdquest_sparkly_bag`\n- `addons/gdquest_theme_utils`\n\nThe first addon is the framework itself, and the last two are little code libraries used by GDPractice and some other open-source technologies we maintain, like [GDTour](https://github.com/GDQuest/gdtour), the interactive Godot tutorial framework.\n\nThe repository also comes with [gdplug](https://github.com/imjp94/gd-plug), a powerful tool to manage add-ons in your Godot projects and download them from open-source repositories. You can use it to add GDPractice or any other Godot addon to your project. First, copy the `addons/gdplug/` folder to your project, then create a file named `plug.gd` at the root of your Godot project with the following content:\n\n```gdscript\n#!/usr/bin/env -S godot --headless --script\nextends \"res://addons/gd-plug/plug.gd\"\n\n\nfunc _plugging() -\u003e void:\n\tplug(\n\t\t\"git@github.com:GDQuest/GDPractice.git\",\n\t\t{include = [\"addons/gdpractice\", \"addons/gdquest_sparkly_bag\", \"addons/gdquest_theme_utils\"]},\n\t)\n```\n\nThen, run the following command in your terminal to download the addons:\n\n```bash\ngodot --headless --script plug.gd update\n```\n\n## How to use\n\nDue to our current workload, we still have limited documentation for GDPractice. However, several example practices are in the `practice_solutions/` folder of this repository.\n\nA limitation of Godot is that we do not have a great system to register entry points or hooks to tell an add-on what configuration to use. So we rely on having GDScript files at specific paths to make GDPractice work:\n\n1. `res://practice_solutions/metadata.gd`: This file should extend the `res://addons/gdpractice/metadata.gd` class. It's used to register and list the practices in your project.\n2. `res://practice_solutions/build_settings.gd`: This file is optional. It's used to override the default build settings. Open the `res://addons/gdpractice/build_settings.gd` file to see the available settings you can override.\n3. `res://practice_solutions/diff.gd`: This file is optional. It's used to edit the project settings at build time. For example, you can remove all input actions from the workbook project if the course or some practices require students to create them.\n\n### Building projects\n\nTo build the workbook and solution projects, you can use the `build.gd` script in the `res://addons/gdpractice/` folder. Run the script with `godot --headless --script addons/gdpractice/build.gd -- --help` to get its documentation and all the options.\n\nTo build, and for practices to work, the system requires you to create a practice metadata file at the path `res://practice_solutions/metadata.gd`. This file should extend the class `res://addons/gdpractice/metadata.gd`.\n\n### Changing build settings\n\nYou can control the settings applied at build time by creating a file named `res://practice_solutions/build_settings.gd`. This file should extend the `res://addons/gdpractice/build_settings.gd` class.\n\nOpen the `res://addons/gdpractice/build_settings.gd` file to see the available settings you can override.\n\n### Editing project configuration at build time\n\nYou can edit the project settings (the `project.godot` file) at build time by creating a script named `diff.gd` in the `res://practice_solutions/` folder.\n\nThis is useful for removing or adding input actions, changing the project's main scene, or changing the project settings in the workbook and solution projects.\n\nThe project settings are used as the reference for the solution project, and the `diff.gd` script is applied when generating the workbook project.\n\nFor example, this code snippet removes all input actions from the workbook, except for the `ui_*` actions included by default by Godot.\n\n```gdscript\nstatic func edit_project_configuration() -\u003e void:\n\tconst INPUT_KEY := \"input/%s\"\n\tfor action in InputMap.get_actions():\n\t\tif action.begins_with(\"ui\"):\n\t\t\tcontinue\n\t\tProjectSettings.set_setting(INPUT_KEY % action, null)\n\t\tProjectSettings.save()\n```\n\n## How practice tests run\n\nEvery practice has a script named test.gd that extends the built-in script tester/test.gd. This script is responsible for running the practice tests. The test.gd script exposes a few virtual functions it calls in preparation before running the checks.\n\nThe testing system waits for nodes in the tree to be ready before running the tests. It ensures that the practice and solution are initialized before collecting data.\n\nThe system runs the following functions in order and in relatively rapid succession:\n\n1. `_build_requirements()`: checks pre-requisites before running other functions.\n2. `_setup_state()`: used to harmonize the properties of the student practice and solution scene.\n3. `_setup_populate_test_space()`: used to collect data from the running practice and solution scene.\n4. `_build_checks()`: creates unit tests used to test student code.\n\nHere's some more detail. Two functions are provided to build requirements and checks:\n\n1. `_build_requirements()`: This function is called before the practice starts running. It should check if the practice has all the necessary variables, functions, signal connections, and so on to run without errors. For example, you can use this to check if a node or property exists in the student practice so that you can safely access it without error later on. If the requirements are not met, the other functions will not run to avoid errors. To add requirements, add `Requirement` objects to the `requirements` array.\n2. `_build_checks()`: This function is called after the practice has run for a moment. It should be used to check if the practice is behaving as expected. If the checks fail, the practice will be marked as failed. To add checks, create `Check` objects and add them to the `checks` array.\n\nTwo functions allow the practice system to capture data from the practice and solution:\n\n1. `_setup_state()`: This async function is called before the practice starts running, once all nodes are ready in the scene tree. It should be used to copy the state of the practice to the solution. Use this to ensure that the practice and solution start with the same properties. For example, you can copy the speed or health of a character from the practice to the solution.\n2. `_setup_populate_test_space()`: This async function is called right after `_setup_state()`. It should be used to capture data from the practice and solution. Use this to collect the state of the practice and the solution during the practice. For example, you can capture the position of a character in the practice and solution to compare them later.\n\nThe two functions above run one after the other and are separated just for conceptual clarity.\n\n## How to collect data from the practice and solution\n\nThe needs of practice tests are very different, so the data you collect and how you collect it is entirely up to you. You collect all the data you need in `_setup_populate_test_space()`. You can store the data however you want, though I recommend using the provided `_test_space` array. Some helper functions in the `Test` class make it easier to check the data later on.\n\n### Storing data over multiple frames\n\nTo store data from multiple frames, use `await` in the `_setup_populate_test_space()` function. This will allow you to run code over multiple frames. For example, you can use the following code to store the position of a character in the practice and solution over ten frames:\n\n```gdscript\nfunc _setup_populate_test_space() -\u003e void:\n\tfor i in range(10):\n\t\tvar data := {\n\t\t\t\"practice_global_position\": _practice.global_position,\n\t\t\t\"solution_global_position\": _solution.global_position\n\t\t}\n\t\t_test_space.append(data)\n\t\tawait get_tree().physics_frame\n```\n\nI prefer using an inner class to store the data, to get static typing, and to make it easier to access the data later on. Here's the same example using an inner class:\n\n```gdscript\nclass TestData:\n\tvar practice_global_position := Vector2.ZERO\n\tvar solution_global_position := Vector2.ZERO\n\n\nfunc _setup_populate_test_space() -\u003e void:\n\tfor i in range(10):\n\t\tvar data := TestData.new()\n\t\tdata.practice_global_position = _practice.global_position\n\t\tdata.solution_global_position = _solution.global_position\n\t\t_test_space.append(data)\n\t\tawait get_tree().physics_frame\n```\n\nAlternatively, the test script provides the method `_connect_timed()` to collect data over some time. Here's a typical example: collecting frame data over one second:\n\n```gdscript\nfunc _setup_populate_test_space() -\u003e void:\n\tawait _connect_timed(1.0, get_tree().process_frame, _populate_test_space)\n\n\nfunc _populate_test_space() -\u003e void:\n\tvar data := TestData.new()\n\tdata.practice_global_position = _practice.global_position\n\tdata.solution_global_position = _solution.global_position\n\t_test_space.append(data)\n```\n\n## Simulating player input\n\nOne key difference between game dev practices and usual interactive programming exercises is that we simulate player input and need to ensure that the student code accounts for this input.\n\nGodot has two main ways to check for player input:\n\n1. Polling in the `_process()` and `_physics_process()` functions.\n2. Using the `_input()` functions with input events.\n\nSimilarly, we use two different approaches to inject and simulate inputs depending on the approach used by the practice:\n\n1. Polling: We can simulate player input in the processing loop by calling `Input.action_press()` and `Input.action_release()`.\n2. Input events: We can simulate player input by creating an `InputEvent` object and calling `Input.parse_input_event()`.\n\n### Examples\n\nThe following example simulates the player moving to the right for 0.3 seconds and collecting data in the test space.\n\n```gdscript\nfunc _setup_populate_test_space() -\u003e void:\n\tInput.action_press(\"move_right\")\n\tawait _connect_timed(0.3, get_tree().process_frame, _populate_test_space)\n\tInput.action_release(\"move_right\")\n\n\nfunc _populate_test_space() -\u003e void:\n\t_test_space.append({\n\t\t\"practice_position\": _practice.position,\n\t\t\"solution_position\": _practice.position,\n\t})\n```\n\nFor input events, it's different as we need to create an `InputEvent` object and call `Input.parse_input_event()`. We use them more for one-time events like mouse clicks or key presses. Here's an example of simulating pressing the space bar:\n\n```gdscript\nfunc _setup_populate_test_space() -\u003e void:\n\tvar event = InputEventKey.new()\n\tevent.scancode = KEY_SPACE\n\tevent.pressed = true\n\tInput.parse_input_event(event)\n\n\t# ... collect data\n```\n\n## Troubleshooting\n\n### Instantiated scenes in the workbook project practices point to solution scenes\n\nIn some cases, the instantiated scenes in the workbook project practices may appear to point to the solution scenes instead of the files in the `res://practices/` folder.\n\nThis can be due to a cache problem in the Godot editor. To fix this, you can try the following:\n\n1. Close the Godot editor.\n2. Delete the `.godot/` folder in the project directory.\n3. Open the Godot editor and reload the project.\n\nYou can also open the `.tscn` files of generated practices in a text editor and ensure the paths to the scenes are correct.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgdquest%2Fgdpractice","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgdquest%2Fgdpractice","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgdquest%2Fgdpractice/lists"}