No description
  • PHP 76.3%
  • JavaScript 17.4%
  • CSS 6.3%
Find a file
Ruben Ramirez 953b787ffc Fix default game mode selection in Start Game modal
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'.
2026-03-05 08:27:22 -06:00
.playwright-mcp Fix Save Score form for Template game type 2026-02-25 02:58:34 -06:00
admin Remove benchmark system and cleanup unused code 2026-03-03 14:39:53 -06:00
includes Add Wordle, Word Flash, Type Flash, and Progressive Reveal game script configurations 2026-03-05 06:52:56 -06:00
public Fix default game mode selection in Start Game modal 2026-03-05 08:27:22 -06:00
templates Add Ultimate Member profile tab and word thumbnail support 2026-02-27 22:46:40 -06:00
.env.example Enhance debug logging system with dual JSON/human-readable output 2025-11-06 15:20:54 -06:00
.gitignore Cleanup 2026-03-04 10:56:19 -06:00
aiml-games-framework.php Remove benchmark system and cleanup unused code 2026-03-03 14:39:53 -06:00
composer.json Installed Mago, CodeGraphContext and, MegaMemory 2026-02-11 12:22:51 -06:00
config.php Add word_search to game type fallback map 2026-03-03 04:50:02 -06:00
mago-2.toml Apply Mago code quality fixes and WordPress configuration 2026-02-25 04:03:34 -06:00
README.md Update README.md to explain scripts/clone-game.sh 2026-03-02 21:51:56 -06:00
readme.txt Standardize framework by removing game-specific code 2026-03-02 12:18:22 -06:00
todo_list.md Fixed single template for Word Scramble games. 2025-08-18 20:36:09 -05:00
uninstall.php Fix individual game leaderboards and improve save score flow 2026-02-25 21:43:59 -06:00

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_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

  1. Clones the aiml-games-albino-panther plugin as a template
  2. Renames files to match the new game slug (e.g., class-hangman.php)
  3. Replaces content throughout all PHP, JS, CSS, and JSON files:
    • Display names (e.g., "Albino Panther" → "Hangman")
    • Constants (e.g., ALBINO_PANTHERHANGMAN)
    • PHP class names (e.g., Albino_PantherHangman)
    • JS identifiers (e.g., AlbinoPantherGameHangmanGame)
    • Slugs (e.g., albino-pantherhangman)
  4. Cleans up by removing .git, .claude, and other dev directories
  5. Validates that no leftover template references remain

Post-Clone Checklist

After running the clone script:

  1. Review the new plugin files in wp-content/plugins/aiml-games-{slug}/
  2. Update config.php with game-specific settings
  3. Implement game logic in includes/class-{slug}.php
  4. Update the display template in public/partials/{slug}-display.php
  5. 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:
    1. Human-readable line: [timestamp UTC] [Plugin Name Debug] message | context=value | level=LEVEL
    2. JSON line: Complete structured data in NDJSON format for programmatic parsing

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_LOG constant to enable logging
    • Use $hr_include_meta parameter to control human-readable metadata inclusion

Log File Location

  • Default: wp-content/aiml-debug.log
  • Configurable via AIML_DEBUG_LOG constant 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_LOG constant 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

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 is false. Return true to 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.