- PHP 79.6%
- JavaScript 15.5%
- CSS 4.9%
| admin | ||
| api/game-plugin-template | ||
| includes | ||
| public | ||
| templates/email | ||
| .env.example | ||
| .gitignore | ||
| aiml-games-framework.php | ||
| 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.
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., 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_word_scramble_fallback_url
Allows developers to provide a custom fallback URL for the Word Scramble game assets when the primary assets fail to load.
Parameters:
$url(mixed): Default value isfalse. Return a valid URL string to enable fallback asset loading.
Example usage:
add_filter('aiml_games_word_scramble_fallback_url', function($url) {
return 'https://your-cdn.com/path/to/word-scramble-assets.js';
});
Use cases:
- Provide CDN fallback URLs
- Use local backup asset files
- Implement custom asset delivery logic
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.