- PHP 76.3%
- JavaScript 17.4%
- CSS 6.3%
The mode radio buttons weren't showing Timer Mode as selected by default because the ?? operator doesn't handle empty strings. Changed to use !empty() to properly fallback to 'normal' when mode is '' or 'default'. |
||
|---|---|---|
| .playwright-mcp | ||
| admin | ||
| includes | ||
| public | ||
| templates | ||
| .env.example | ||
| .gitignore | ||
| aiml-games-framework.php | ||
| composer.json | ||
| config.php | ||
| mago-2.toml | ||
| README.md | ||
| readme.txt | ||
| todo_list.md | ||
| uninstall.php | ||
AIML Games Framework
A comprehensive WordPress plugin framework for educational AI and machine learning games using a modern hub & spoke architecture with CPT-first content modeling.
Overview
The AIML Games Framework provides a centralized foundation for managing multiple educational game plugins. It implements a hub & spoke architecture where:
- Hub (Framework): Provides shared services like user management, leaderboards, analytics, content types (CPTs), and admin interface
- Spokes (Games): Individual game plugins that extend the framework for specific game types
This README documents the Custom Post Type (CPT) system architecture, developer workflows for aiml_word, aiml_game, and aiml_game_set, and taxonomy usage examples.
Key Features
Core Framework Services
- User Management: Player profiles, progress tracking, achievements
- Global Leaderboard: Cross-game scoring and rankings
- Analytics: Gameplay metrics and reporting
- CPT System: Word/game/game-set content modeling with meta and taxonomies
- Game Registry: Plugin discovery and dependency management
Developer Tools
- Abstract Base Class for games
- Validation and logging
- Admin partials and assets for CPT UI
CPT System Architecture
The framework registers three CPTs and two taxonomies, with standardized meta conventions and admin UI.
Post Types
-
aiml_word
- Purpose: Canonical word entities with definition and extra data
- UI: Visible in admin; non-public front-end by default; REST-enabled
- Meta (canonical):
- _aiml_word_definition (string)
- _aiml_word_extra_json (JSON string; validated)
- Taxonomies:
- aiml_word_theme (hierarchical)
- aiml_word_difficulty (hierarchical)
-
aiml_game
- Purpose: A single playable game instance with settings
- UI: Visible in admin; non-public front-end; REST-enabled
- Meta:
- _aiml_game_settings_json (JSON string; validated)
- Game-specific settings live here (e.g., difficulty, round limits) by convention
- _aiml_game_settings_json (JSON string; validated)
-
aiml_game_set
- Purpose: Ordered collection of aiml_game posts for progression/series
- UI: Visible in admin; non-public front-end; REST-enabled
- Meta:
- _aiml_game_set_order (array of game post IDs; sanitized and deduped)
Registration and behaviors are implemented in includes/class-content-types.php.
Key reference identifiers from code:
- CPT slugs:
- AIML_Games_Content_Types::CPT_WORD
- AIML_Games_Content_Types::CPT_GAME
- AIML_Games_Content_Types::CPT_GAME_SET
- Taxonomy slugs:
- AIML_Games_Content_Types::TAX_WORD_THEME
- AIML_Games_Content_Types::TAX_WORD_DIFFICULTY
- Canonical meta keys:
- AIML_Games_Content_Types::META_WORD_DEFINITION
- AIML_Games_Content_Types::META_WORD_EXTRA_JSON
- AIML_Games_Content_Types::META_GAME_SETTINGS_JSON
- AIML_Games_Content_Types::META_GAME_SET_ORDER
Admin meta boxes are rendered via admin/partials/cpt-meta-boxes.php, with nonces and JSON validation. Admin assets are enqueued only on relevant screens.
Taxonomies
Both taxonomies attach to aiml_word and are hierarchical, UI-visible, REST-enabled:
- aiml_word_theme
- For thematic grouping (e.g., Animals, Countries)
- aiml_word_difficulty
- For difficulty grouping (e.g., Easy, Medium, Hard)
Default terms are ensured idempotently on each init by AIML_Games_Content_Types::ensure_default_terms().
Meta Naming Convention
Canonical meta keys follow: aiml{cpt}_{field}
- cpt is the post type slug (aiml_word, aiml_game, aiml_game_set)
- field is snake_case
- JSON fields use a _json suffix
Admin UX and Safeguards
- Meta boxes for each CPT with nonce verification and sanitization
- JSON textareas validated; invalid JSON triggers an admin notice with actionable feedback
- Game set membership saves ensure no cycles and sanitize to an ordered unique int list
- Cache invalidation for affected helper buckets when posts change or delete
Developer Guide: Working with CPTs
This section covers standard workflows for aiml_word, aiml_game, and aiml_game_set, including CRUD, meta usage, taxonomy operations, and querying patterns.
aiml_word
Use aiml_word to store vocabulary used by games.
Create a word (secure example with sanitization and error handling):
// Raw input values (e.g., from a form or external source)
$raw_title = 'algorithm';
$raw_content = 'A step-by-step procedure for calculations.';
$raw_definition = 'A step-by-step procedure for calculations.';
$raw_extra = array( 'synonyms' => array( 'procedure', 'method' ) );
// Sanitize scalar text fields
$title = sanitize_text_field( $raw_title );
// Allow only safe HTML in content (adjust allowed tags as needed)
$allowed_content_tags = array(
'a' => array( 'href' => array(), 'title' => array(), 'rel' => array(), 'target' => array() ),
'em' => array(),
'strong' => array(),
'p' => array(),
'br' => array(),
'ul' => array(),
'ol' => array(),
'li' => array(),
);
$content = wp_kses( $raw_content, $allowed_content_tags );
// Sanitize meta fields
$definition = sanitize_text_field( $raw_definition );
// Encode complex meta as JSON safely
$extra_json = wp_json_encode( $raw_extra );
// Insert the post
$post_id = wp_insert_post( array(
'post_type' => 'aiml_word',
'post_status' => 'publish',
'post_title' => $title,
'post_content'=> $content,
), true ); // Pass true to return WP_Error on failure
// Check for insertion errors
if ( is_wp_error( $post_id ) ) {
// Handle the error appropriately (log it, display admin notice, etc.)
error_log( sprintf( 'Failed to insert aiml_word: %s', $post_id->get_error_message() ) );
return;
}
// Save sanitized meta only after successful insertion
update_post_meta( $post_id, '_aiml_word_definition', $definition );
update_post_meta( $post_id, '_aiml_word_extra_json', $extra_json );
Assign taxonomies:
wp_set_object_terms($post_id, array('Computer Science'), 'aiml_word_theme');
wp_set_object_terms($post_id, array('Medium'), 'aiml_word_difficulty');
Query words by taxonomy:
$words = get_posts(array(
'post_type' => 'aiml_word',
'posts_per_page' => 50,
'tax_query' => array(
'relation' => 'AND',
array(
'taxonomy' => 'aiml_word_theme',
'field' => 'name',
'terms' => array('Computer Science'),
),
array(
'taxonomy' => 'aiml_word_difficulty',
'field' => 'name',
'terms' => array('Medium'),
),
),
'no_found_rows' => true,
));
Read meta safely with canonical-first pattern:
$def = get_post_meta($post_id, '_aiml_word_definition', true);
$extra_json = get_post_meta($post_id, '_aiml_word_extra_json', true);
$extra = is_string($extra_json) ? json_decode($extra_json, true) : array();
aiml_game
Stores a playable instance and its settings payload.
Create a game:
$game_id = wp_insert_post(array(
'post_type' => 'aiml_game',
'post_status' => 'publish',
'post_title' => 'Word Scramble - Set 1 - Easy',
));
$settings = array(
'game_type' => 'word-scramble',
'aiml_game_difficulty'=> 'easy',
'aiml_game_max_rounds'=> 10,
'ws_min_letters' => 3,
'ws_shuffle_seed' => wp_generate_password(8, false),
);
update_post_meta($game_id, '_aiml_game_settings_json', wp_json_encode($settings));
Query games:
$games = get_posts(array(
'post_type' => 'aiml_game',
'posts_per_page' => 20,
'post_status' => array('publish','draft','private'),
'orderby' => 'date',
'order' => 'DESC',
'no_found_rows' => true,
));
Load and validate settings:
$raw = get_post_meta($game_id, '_aiml_game_settings_json', true);
$settings = array();
if (is_string($raw) && $raw !== '') {
$decoded = json_decode($raw, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
$settings = $decoded;
} else {
// Log the JSON error to help diagnose malformed data and avoid using invalid settings
error_log(sprintf(
'[aiml-games-framework] JSON decode error for game_id %d: %s; raw length=%d',
(int) $game_id,
function_exists('json_last_error_msg') ? json_last_error_msg() : ('code ' . json_last_error()),
strlen($raw)
));
$settings = array(); // reset to safe default
}
}
// Apply defensive defaults here as needed
aiml_game_set
Represents an ordered collection of aiml_game posts.
Create a set and assign game order:
$set_id = wp_insert_post(array(
'post_type' => 'aiml_game_set',
'post_status' => 'publish',
'post_title' => 'Intro Series - Part 1',
));
$ordered_ids = array($game_id /*, ... more game IDs ... */);
update_post_meta($set_id, '_aiml_game_set_order', array_map('absint', $ordered_ids));
Fetch ordered games for a set:
$ordered = get_post_meta($set_id, '_aiml_game_set_order', true);
$ordered = is_array($ordered) ? array_values(array_unique(array_map('absint', $ordered))) : array();
if (!empty($ordered)) {
$games = get_posts(array(
'post_type' => 'aiml_game',
'post__in' => $ordered,
'orderby' => 'post__in',
'posts_per_page' => count($ordered),
'no_found_rows' => true,
));
}
Cycle protection:
- The save handler prevents invalid or cyclical relationships per AIML_Games_Content_Types::save_game_set().
Admin Integration
Meta boxes:
- aiml_game: Game Settings (JSON)
- aiml_game_set: Games in Set (ordered list)
- aiml_word: Word Details (definition, extra JSON)
Admin assets are conditionally enqueued only for these CPT screens; JSON and meta update errors surface as admin notices.
Shortcodes and Rendering
The framework provides a default game set shortcode to render progression and front-end UX:
Game plugins should use CPT-based queries for content.
Taxonomy Usage Examples
Create or ensure terms:
// Ensure a theme exists and get term ID, with robust WP_Error handling
$term = term_exists('Animals', 'aiml_word_theme');
// term_exists() may return 0, null, array, or WP_Error
if (is_wp_error($term)) {
error_log(sprintf('[aiml-games-framework] term_exists error (theme): %s', $term->get_error_message()));
return; // or handle gracefully in your context
}
if (!$term) {
$term = wp_insert_term('Animals', 'aiml_word_theme');
if (is_wp_error($term)) {
error_log(sprintf('[aiml-games-framework] wp_insert_term error (theme): %s', $term->get_error_message()));
return; // or handle gracefully in your context
}
}
$theme_term_id = is_array($term) ? (int)$term['term_id'] : (int)$term;
// Ensure a difficulty term, explicitly checking for WP_Error
$diff = term_exists('Easy', 'aiml_word_difficulty');
if (is_wp_error($diff)) {
error_log(sprintf('[aiml-games-framework] term_exists error (difficulty): %s', $diff->get_error_message()));
return; // or handle gracefully
}
if (!$diff) {
$diff = wp_insert_term('Easy', 'aiml_word_difficulty');
if (is_wp_error($diff)) {
error_log(sprintf('[aiml-games-framework] wp_insert_term error (difficulty): %s', $diff->get_error_message()));
return; // or handle gracefully
}
}
$diff_term_id = is_array($diff) ? (int)$diff['term_id'] : (int)$diff;
Assign terms to a word:
wp_set_object_terms($post_id, array($theme_term_id), 'aiml_word_theme', false);
wp_set_object_terms($post_id, array($diff_term_id), 'aiml_word_difficulty', false);
Query by theme and difficulty:
$words = get_posts(array(
'post_type' => 'aiml_word',
'posts_per_page' => -1,
'tax_query' => array(
'relation' => 'AND',
array(
'taxonomy' => 'aiml_word_theme',
'field' => 'term_id',
'terms' => array($theme_term_id),
),
array(
'taxonomy' => 'aiml_word_difficulty',
'field' => 'term_id',
'terms' => array($diff_term_id),
),
),
'no_found_rows' => true,
));
REST usage:
- Both taxonomies are show_in_rest=true, so block editors and REST clients can manage terms programmatically.
Creating New Game Plugins
The framework includes a clone script to quickly scaffold new game plugins based on the Albino Panther template.
Using the Clone Script
# From the framework directory
./scripts/clone-game.sh "New Game Name"
# Examples
./scripts/clone-game.sh "Hangman"
./scripts/clone-game.sh "Trivia Quest"
./scripts/clone-game.sh "Word Search"
What the Script Does
- Clones the
aiml-games-albino-pantherplugin as a template - Renames files to match the new game slug (e.g.,
class-hangman.php) - Replaces content throughout all PHP, JS, CSS, and JSON files:
- Display names (e.g., "Albino Panther" → "Hangman")
- Constants (e.g.,
ALBINO_PANTHER→HANGMAN) - PHP class names (e.g.,
Albino_Panther→Hangman) - JS identifiers (e.g.,
AlbinoPantherGame→HangmanGame) - Slugs (e.g.,
albino-panther→hangman)
- Cleans up by removing
.git,.claude, and other dev directories - Validates that no leftover template references remain
Post-Clone Checklist
After running the clone script:
- Review the new plugin files in
wp-content/plugins/aiml-games-{slug}/ - Update
config.phpwith game-specific settings - Implement game logic in
includes/class-{slug}.php - Update the display template in
public/partials/{slug}-display.php - Activate the plugin in WordPress admin
Uninstall Behavior
The framework includes a comprehensive uninstall routine to clean up CPTs, taxonomies, tables, and options when appropriate dependency conditions are met.
- See uninstall.php for complete vs partial cleanup rules:
- If other aiml-games-* plugins are active, only framework-specific data is removed
- If no related plugins remain, removes:
- All aiml_game, aiml_game_set, aiml_word posts
- aiml_word_theme and aiml_word_difficulty terms
- Framework tables (leaderboard, analytics, sessions, user progress, etc.)
- Options and user meta with aiml_games_ prefix
- Flushes rewrite rules
Game plugins (e.g., Albino Panther, Word Scramble) should preserve CPT data on their own uninstall; the framework owns shared CPT cleanup.
Debug Logging
The framework provides comprehensive debug logging through the aiml_debug_log() method. Debug logs use a mixed format combining human-readable and JSON entries:
Log Format Policy
- Each log event generates two lines in the debug log file:
- Human-readable line:
[timestamp UTC] [Plugin Name Debug] message | context=value | level=LEVEL - JSON line: Complete structured data in NDJSON format for programmatic parsing
- Human-readable line:
Mixed Format Example
[31-Dec-2024 23:45:12 UTC] [AIML Games Framework Debug] User creation started | context=user_creation | level=INFO
{"timestamp":"2024-12-31 23:45:12","timestamp_utc":"2024-12-31 23:45:12","level":"INFO","environment":"DEV","context":"user_creation","message":"User creation started","user_id":123,"ip_address":"127.0.0.1","request_id":"abc123","data":{"form_data":"..."}}
Consumer Guidelines
- Human readers: Read human-readable lines for quick debugging
- Log parsers: Skip non-JSON lines, parse JSON lines as NDJSON
- Configuration:
- Set
AIML_DEBUG_LOGconstant to enable logging - Use
$hr_include_metaparameter to control human-readable metadata inclusion
- Set
Log File Location
- Default:
wp-content/aiml-debug.log - Configurable via
AIML_DEBUG_LOGconstant or method parameter - Creates parent directories automatically if needed
Troubleshooting
Common CPT issues:
- Invalid JSON in meta fields:
- Fix JSON and re-save; admin will show notices for errors
- Missing default terms:
- Defaults auto-heal on init via ensure_default_terms()
- Permission issues:
- CPTs use explicit capabilities for predictable access
Debug logging issues:
- No debug output: Ensure
AIML_DEBUG_LOGconstant is defined or pass$enable_debug = true - Mixed format confusion: Parsers should skip non-JSON lines and parse JSON lines only
- Directory permissions: Check write permissions on log directory
File References and Key Classes
- Content Types and CPT logic: includes/class-content-types.php
- Admin CPT partials: admin/partials/cpt-meta-boxes.php
- Game set shortcode: includes/class-game-set-shortcode.php
- Public rendering helpers: public/class-public.php
Security and Performance Notes
- Nonces, sanitization, and capability checks are enforced in CPT save handlers
- JSON fields are validated with error reporting
- Queries should use no_found_rows and field scoping to reduce overhead
- Caches for helper buckets are invalidated on relevant post changes
Available Filters
aiml_games_{game_slug}_force_enqueue
Allows developers to force asset enqueuing for any game plugin when rendering outside normal contexts (e.g., Gutenberg blocks).
Parameters:
$force(bool): Default value isfalse. Returntrueto force enqueue assets.
Example usage:
// Force enqueue assets for albino-panther game
add_filter('aiml_albino_panther_force_enqueue', '__return_true');
Use cases:
- Gutenberg block rendering
- AJAX-loaded game content
- Custom rendering contexts
Changelog (CPT docs)
- Updated to include CPT architecture, migration flow, developer usage for aiml_word, aiml_game, aiml_game_set, and taxonomy examples aligned with implementation plan and current code.