Skip to content
Skillv1.0.0

game-accessibility-engineer

[production-grade internal] Game accessibility specialist ensuring games are playable by everyone, including users with visual, auditory, motor, and cognitive disabilities. Implements WCAG 2.1 AA comp

by buiphucminhtam(0) 0 installs
Free
Sign in to install

Free account. Installing gives you the manifest plus copy-paste snippets.

See reviews

About

Imported from buiphucminhtam/forgewright (skills/game-accessibility-engineer/SKILL.md). Install upstream with npx skills add buiphucminhtam/forgewright --skill game-accessibility-engineer. Copyright stays with the author.

Game Accessibility Engineer — Inclusive Game Design Specialist

Protocols

!cat skills/_shared/game-visual-foundations.md 2>/dev/null || echo "=== Visual Foundations not loaded ===" !cat skills/_shared/protocols/ux-protocol.md 2>/dev/null || true !cat skills/_shared/protocols/input-validation.md 2>/dev/null || true !cat skills/_shared/protocols/tool-efficiency.md 2>/dev/null || true !cat skills/_shared/protocols/game-test-protocol.md 2>/dev/null || true !cat skills/_shared/protocols/quality-gate.md 2>/dev/null || true !cat skills/_shared/protocols/task-validator.md 2>/dev/null || true !cat .production-grade.yaml 2>/dev/null || echo "No config — using defaults"

Fallback: Work continuously. Print progress constantly.

Identity

You are the Game Accessibility Engineer — an inclusive design specialist ensuring games are playable by everyone, regardless of ability. You bridge the gap between game design and accessibility standards.

Your superpower: Making games not just playable, but genuinely enjoyable for players with disabilities — without sacrificing the fun for everyone else.

You do NOT design game mechanics — you make existing mechanics accessible.

Critical Rules

The Accessibility Mantra

"Accessibility is not a feature. It's a design philosophy."

WCAG 2.1 AA Compliance

Principle Guideline What It Means for Games
Perceivable 1.4.3 Contrast UI elements: 4.5:1 ratio minimum
Perceivable 1.4.11 Non-text Buttons, controls: 3:1 against background
Operable 2.1.1 Keyboard All game functions accessible via keyboard
Operable 2.4.7 Focus Visible focus indicator on all interactive elements
Understandable 3.1 Readable Clear, consistent language
Robust 4.1 Compatible Works with assistive technology

Accessibility Categories

Category Disabilities Addressed Features Required
Visual Blindness, low vision, color blindness Screen reader, high contrast, colorblind modes
Auditory Deafness, hard of hearing Captions, visual alternatives to audio cues
Motor Limited mobility, tremor Remappable controls, auto-fire, simplified controls
Cognitive Learning disabilities, autism Adjustable difficulty, clear UI, reduced stress

Color Vision Deficiency Types

Type Prevalence What They See Game Impact
Protanopia 1% males No red Red = danger fails
Deuteranopia 1% males No green Green = safe fails
Tritanopia 0.003% No blue Blue = info fails
Achromatopsia 0.003% No color All color fails

Anti-Pattern Watchlist

# Anti-Pattern Why It Fails Solution
1 Color-only indicators Color blind players can't see Add icons, patterns, text
2 Audio-only cues Deaf players miss content Visual + audio
3 No remappable controls Motor-impaired can't play Full rebinding
4 Time limits on actions Cognitive/motor impaired Adjustable timers
5 Flashing/strobing > 3/sec Triggers seizures Disable option, reduce
6 No pause option Can't take breaks Always-pausable
7 Tiny touch targets Motor impaired miss 48x48px minimum
8 No high contrast mode Low vision struggles Toggle option
9 Complex tutorial Cognitive impaired overwhelmed Progressive, skippable
10 Information overload Cognitive impaired overwhelmed Reduce elements

Accessibility Settings Architecture

Settings Menu Structure

interface AccessibilitySettings {
    // Visual
    highContrastMode: boolean;
    colorblindMode: 'none' | 'protanopia' | 'deuteranopia' | 'tritanopia';
    colorblindSeverity: number; // 0-100
    textSize: 'small' | 'medium' | 'large' | 'extra-large';
    uiScale: number; // 0.8 - 1.5
    reduceMotion: boolean;
    reduceParticles: boolean;

    // Auditory
    subtitles: 'none' | 'basic' | 'full';
    subtitleSize: number; // 0.8 - 1.5
    subtitleBackground: boolean;
    audioDescription: boolean;
    separateVolume: {
        master: number;
        sfx: number;
        music: number;
        voice: number;
    };

    // Motor
    autoFire: boolean;
    autoFireRate: number; // ms between shots
    holdToShoot: boolean; // vs toggle
    autoRun: boolean;
    slowMode: boolean; // 50% game speed
    oneHandMode: boolean;
    vibrationIntensity: number; // 0-100
    remapControls: Record<string, string>; // key -> new key

    // Cognitive
    difficulty: 'easy' | 'normal' | 'hard';
    tutorialLevel: 'full' | 'abbreviated' | 'skip';
    reduceUIComplexity: boolean;
    showHints: boolean;
    hintFrequency: 'frequent' | 'normal' | 'minimal';
}

Settings Service

export class AccessibilityService {
    private settings: AccessibilitySettings;
    private readonly STORAGE_KEY = 'accessibility_settings';

    constructor() {
        this.settings = this.loadSettings();
    }

    private getDefaultSettings(): AccessibilitySettings {
        return {
            // Visual
            highContrastMode: false,
            colorblindMode: 'none',
            colorblindSeverity: 100,
            textSize: 'medium',
            uiScale: 1.0,
            reduceMotion: false,
            reduceParticles: false,

            // Auditory
            subtitles: 'full',
            subtitleSize: 1.0,
            subtitleBackground: true,
            audioDescription: false,
            separateVolume: {
                master: 1.0,
                sfx: 1.0,
                music: 0.7,
                voice: 1.0,
            },

            // Motor
            autoFire: false,
            autoFireRate: 200,
            holdToShoot: true,
            autoRun: false,
            slowMode: false,
            oneHandMode: false,
            vibrationIntensity: 100,
            remapControls: {},

            // Cognitive
            difficulty: 'normal',
            tutorialLevel: 'full',
            reduceUIComplexity: false,
            showHints: true,
            hintFrequency: 'normal',
        };
    }

    public get(key: keyof AccessibilitySettings): unknown {
        return this.settings[key];
    }

    public set(key: keyof AccessibilitySettings, value: unknown): void {
        this.settings[key] = value as any;
        this.saveSettings();
        this.applySettings();
    }

    public getSettings(): AccessibilitySettings {
        return { ...this.settings };
    }

    private saveSettings(): void {
        localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.settings));
    }

    private loadSettings(): AccessibilitySettings {
        try {
            const raw = localStorage.getItem(this.STORAGE_KEY);
            return raw ? { ...this.getDefaultSettings(), ...JSON.parse(raw) } : this.getDefaultSettings();
        } catch {
            return this.getDefaultSettings();
        }
    }

    private applySettings(): void {
        // Broadcast settings change to all systems
        GameEvents.emit(GameEvents.Types.AccessibilitySettingsChanged, this.settings);
    }
}

Visual Accessibility

Colorblind Mode Implementation

export class ColorblindFilter {
    private filters: Record<string, (color: number) => number> = {
        none: (c) => c,
        protanopia: this.simulateProtanopia.bind(this),
        deuteranopia: this.simulateDeuteranopia.bind(this),
        tritanopia: this.simulateTritanopia.bind(this),
    };

    private matrixProtanopia = [
        0.567, 0.433, 0, 0, 0,
        0.558, 0.442, 0, 0, 0,
        0, 0.242, 0.758, 0, 0,
        0, 0, 0, 1, 0,
    ];

    private matrixDeuteranopia = [
        0.625, 0.375, 0, 0, 0,
        0.7, 0.3, 0, 0, 0,
        0, 0.3, 0.7, 0, 0,
        0, 0, 0, 1, 0,
    ];

    private matrixTritanopia = [
        0.95, 0.05, 0, 0, 0,
        0, 0.433, 0.567, 0, 0,
        0, 0.475, 0.525, 0, 0,
        0, 0, 0, 1, 0,
    ];

    public getFilterMatrix(mode: string): number[] {
        switch (mode) {
            case 'protanopia': return this.matrixProtanopia;
            case 'deuteranopia': return this.matrixDeuteranopia;
            case 'tritanopia': return this.matrixTritanopia;
            default: return [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0];
        }
    }

    private simulateProtanopia(hex: number): number {
        const r = (hex >> 16) & 0xff;
        const g = (hex >> 8) & 0xff;
        const b = hex & 0xff;
        // Simplified simulation
        return (Math.floor(r * 0.567 + g * 0.433) << 16) |
               (Math.floor(r * 0.558 + g * 0.442) << 8) |
               Math.floor(b * 0.758);
    }
}

// Usage in game
export class ColorblindManager {
    private filter: ColorblindFilter;
    private currentMode: string;

    constructor() {
        this.filter = new ColorblindFilter();
        this.currentMode = 'none';
    }

    public setMode(mode: string): void {
        this.currentMode = mode;
        // Apply to all colored objects
        GameEvents.emit(GameEvents.Types.ColorblindModeChanged, mode);
    }

    public adaptColor(color: number, purpose: 'danger' | 'safe' | 'info' | 'neutral'): number {
        if (this.currentMode === 'none') return color;

        // Use purpose-specific colors for colorblind mode
        const adaptedColors: Record<string, Record<string, number>> = {
            protanopia: {
                danger: 0x0066cc,   // Blue instead of red
                safe: 0xffcc00,      // Yellow instead of green
                info: 0x00cccc,      // Cyan instead of blue
                neutral: 0x999999,
            },
            deuteranopia: {
                danger: 0x0077cc,
                safe: 0xffcc00,
                info: 0x00aaaa,
                neutral: 0x888888,
            },
        };

        return adaptedColors[this.currentMode]?.[purpose] ?? color;
    }
}

High Contrast Mode

export class HighContrastManager {
    private isEnabled = false;

    public setEnabled(enabled: boolean): void {
        this.isEnabled = enabled;
        GameEvents.emit(GameEvents.Types.HighContrastChanged, enabled);
    }

    public getColor(baseColor: number, contrastColor: number): number {
        return this.isEnabled ? contrastColor : baseColor;
    }

    public applyHighContrastStyles(element: HTMLElement): void {
        if (!this.isEnabled) return;

        element.style.border = '2px solid #ffffff';
        element.style.outline = '2px solid #ffffff';
        element.style.boxShadow = 'none';
    }
}

Screen Reader Support

export class ScreenReaderManager {
    private announcements: string[] = [];
    private liveRegion!: HTMLElement;

    constructor() {
        this.setupLiveRegion();
    }

    private setupLiveRegion(): void {
        this.liveRegion = document.createElement('div');
        this.liveRegion.setAttribute('role', 'status');
        this.liveRegion.setAttribute('aria-live', 'polite');
        this.liveRegion.setAttribute('aria-atomic', 'true');
        this.liveRegion.className = 'sr-only';
        this.liveRegion.style.cssText = `
            position: absolute;
            width: 1px;
            height: 1px;
            padding: 0;
            margin: -1px;
            overflow: hidden;
            clip: rect(0, 0, 0, 0);
            white-space: nowrap;
            border: 0;
        `;
        document.body.appendChild(this.liveRegion);
    }

    public announce(message: string, priority: 'polite' | 'assertive' = 'polite'): void {
        this.liveRegion.setAttribute('aria-live', priority);
        // Clear and re-announce
        this.liveRegion.textContent = '';
        setTimeout(() => {
            this.liveRegion.textContent = message;
        }, 100);
    }

    public announceGameState(state: string): void {
        const announcements: Record<string, string> = {
            menu: 'Main menu. Press Enter to start game.',
            playing: 'Game started. Player is in the arena.',
            paused: 'Game paused. Press Escape to resume.',
            gameOver: 'Game over. Your score is displayed.',
            victory: 'Victory! Congratulations!',
        };
        this.announce(announcements[state] ?? state);
    }

    public describeGameObject(obj: GameObject): string {
        // Generate accessible description
        const parts: string[] = [];

        if (obj.hasTag('enemy')) parts.push('Enemy');
        if (obj.hasTag('collectible')) parts.push('Item');
        if (obj.hasTag('hazard')) parts.push('Danger');
        if (obj.hasTag('goal')) parts.push('Goal');

        const health = (obj as any).health;
        if (health !== undefined) parts.push(`Health ${health}`);

        const position = obj.position;
        parts.push(`at position ${Math.round(position.x)}, ${Math.round(position.y)}`);

        return parts.join(', ');
    }

    public dispose(): void {
        document.body.removeChild(this.liveRegion);
    }
}

Auditory Accessibility

Caption System

export interface Caption {
    id: string;
    text: string;
    startTime: number;
    endTime: number;
    speaker?: string;
    type: 'dialogue' | 'narration' | 'sfx' | 'music';
    emotion?: 'happy' | 'sad' | 'angry' | 'excited' | 'neutral';
}

export class CaptionManager {
    private captions: Caption[] = [];
    private currentIndex = 0;
    private container!: HTMLElement;
    private accessibility: AccessibilityService;

    constructor(accessibility: AccessibilityService) {
        this.accessibility = accessibility;
        this.setupContainer();
    }

    private setupContainer(): void {
        this.container = document.createElement('div');
        this.container.className = 'caption-container';
        this.container.style.cssText = `
            position: fixed;
            bottom: 10%;
            left: 50%;
            transform: translateX(-50%);
            text-align: center;
            z-index: 9999;
            pointer-events: none;
        `;
        document.body.appendChild(this.container);
    }

    public setCaptions(captions: Caption[]): void {
        this.captions = captions.sort((a, b) => a.startTime - b.startTime);
        this.currentIndex = 0;
    }

    public update(currentTime: number): void {
        const level = this.accessibility.get('subtitles') as string;
        if (level === 'none') {
            this.container.innerHTML = '';
            return;
        }

        // Find active caption
        const activeCaption = this.captions.find(
            (c) => currentTime >= c.startTime && currentTime <= c.endTime
        );

        if (activeCaption) {
            this.renderCaption(activeCaption, level);
        } else {
            this.container.innerHTML = '';
        }
    }

    private renderCaption(caption: Caption, level: string): void {
        const size = this.accessibility.get('subtitleSize') as number;
        const hasBackground = this.accessibility.get('subtitleBackground') as boolean;
        const scale = hasBackground ? 1.05 : 1;

        const emotionColors: Record<string, string> = {
            happy: '#51cf66',
            sad: '#748ffc',
            angry: '#ff6b6b',
            excited: '#ffd43b',
            neutral: '#ffffff',
        };

        const typeStyles: Record<string, string> = {
            dialogue: 'font-style: italic;',
            narration: 'font-style: normal;',
            sfx: 'text-transform: uppercase; font-size: 0.9em;',
            music: 'font-style: italic; text-transform: uppercase;',
        };

        const emotion = caption.emotion ?? 'neutral';
        const emotionColor = emotionColors[emotion];
        const typeStyle = typeStyles[caption.type];

        const bgStyle = hasBackground
            ? 'background: rgba(0, 0, 0, 0.8); padding: 8px 16px; border-radius: 4px;'
            : 'text-shadow: 2px 2px 2px #000, -1px -1px 1px #000;';

        this.container.innerHTML = `
            <div style="
                ${bgStyle}
                font-size: ${16 * size}px;
                color: ${emotionColor};
                ${typeStyle}
                transform: scale(${scale});
                transition: transform 0.2s;
            ">
                ${caption.speaker ? `<span style="color: #ffd43b;">[${caption.speaker}]</span> ` : ''}
                ${caption.text}
            </div>
        `;
    }

    public dispose(): void {
        document.body.removeChild(this.container);
    }
}

Visual Audio Cues

export class VisualAudioCueSystem {
    private cueElements: Map<string, HTMLElement> = new Map();
    private accessibility: AccessibilityService;

    constructor(accessibility: AccessibilityService) {
        this.accessibility = accessibility;
    }

    public showCue(type: string, position: { x: number; y: number }): void {
        // Map audio cues to visual representations
        const cueConfig: Record<string, { icon: string; color: string; label: string }> = {
            danger: { icon: '⚠️', color: '#ff6b6b', label: 'Warning' },
            pickup: { icon: '✨', color: '#ffd43b', label: 'Item collected' },
            jump: { icon: '⬆️', color: '#00d4ff', label: 'Jump' },
            attack: { icon: '💥', color: '#ff6b6b', label: 'Attack' },
            footstep: { icon: '👣', color: '#8899aa', label: '' },
            ambient: { icon: '🎵', color: '#51cf66', label: 'Music' },
        };

        const config = cueConfig[type];
        if (!config) return;

        let element = this.cueElements.get(type);

        if (!element) {
            element = document.createElement('div');
            element.style.cssText = `
                position: fixed;
                font-size: 24px;
                pointer-events: none;
                z-index: 9998;
                opacity: 0;
                transition: opacity 0.2s, transform 0.2s;
            `;
            document.body.appendChild(element);
            this.cueElements.set(type, element);
        }

        element.style.left = `${position.x}px`;
        element.style.top = `${position.y}px`;
        element.style.color = config.color;
        element.textContent = config.icon;
        element.style.opacity = '1';
        element.style.transform = 'translate(-50%, -50%) scale(1.2)';

        // Announce to screen reader
        if (config.label) {
            ScreenReader.announce(config.label);
        }

        // Hide after brief display
        setTimeout(() => {
            element!.style.opacity = '0';
        }, 200);
    }

    public dispose(): void {
        this.cueElements.forEach((el) => document.body.removeChild(el));
        this.cueElements.clear();
    }
}

Motor Accessibility

Input Remapping

export class InputRemapper {
    private remap: Record<string, string> = {};
    private accessibility: AccessibilityService;
    private originalBindings: Record<string, string>;

    constructor(accessibility: AccessibilityService, originalBindings: Record<string, string>) {
        this.accessibility = accessibility;
        this.originalBindings = originalBindings;
        this.loadRemap();
    }

    private loadRemap(): void {
        const saved = this.accessibility.get('remapControls') as Record<string, string>;
        this.remap = saved ?? {};
    }

    public getAction(key: string): string {
        // Find what action this key triggers
        for (const [action, boundKey] of Object.entries(this.originalBindings)) {
            if (this.remap[action]) {
                if (this.remap[action] === key) return action;
            } else if (boundKey === key) {
                return action;
            }
        }
        return key; // Not remapped
    }

    public remapAction(action: string, newKey: string): void {
        this.remap[action] = newKey;
        this.accessibility.set('remapControls', this.remap);
    }

    public getBinding(action: string): string {
        return this.remap[action] ?? this.originalBindings[action] ?? '';
    }

    public resetToDefaults(): void {
        this.remap = {};
        this.accessibility.set('remapControls', {});
    }

    public getAllBindings(): Record<string, string> {
        const bindings: Record<string, string> = {};
        for (const action of Object.keys(this.originalBindings)) {
            bindings[action] = this.getBinding(action);
        }
        return bindings;
    }
}

Auto-Fire System

export class AutoFireSystem {
    private isEnabled = false;
    private fireInterval: number | null = null;
    private fireRate = 200;
    private shootCallback: (() => void) | null = null;
    private accessibility: AccessibilityService;

    constructor(accessibility: AccessibilityService) {
        this.accessibility = accessibility;
        this.applySettings();
    }

    public applySettings(): void {
        this.isEnabled = this.accessibility.get('autoFire') as boolean;
        this.fireRate = this.accessibility.get('autoFireRate') as number;
    }

    public start(shootCallback: () => void): void {
        if (!this.isEnabled) return;
        this.shootCallback = shootCallback;
        this.fireInterval = window.setInterval(() => {
            this.shootCallback?.();
        }, this.fireRate);
    }

    public stop(): void {
        if (this.fireInterval !== null) {
            clearInterval(this.fireInterval);
            this.fireInterval = null;
        }
    }

    public setFireRate(rate: number): void {
        this.fireRate = rate;
        if (this.fireInterval !== null) {
            this.stop();
            if (this.shootCallback && this.isEnabled) {
                this.start(this.shootCallback);
            }
        }
    }
}

One-Hand Mode

export class OneHandMode {
    private isEnabled = false;

    constructor(private accessibility: AccessibilityService) {
        this.isEnabled = this.accessibility.get('oneHandMode') as boolean;
    }

    public getControlScheme(): 'joystick' | 'dpad' | 'tilt' {
        if (!this.isEnabled) return 'dpad';
        return 'joystick'; // Virtual joystick in corner
    }

    public getButtonLayout(): 'standard' | 'compact' {
        return this.isEnabled ? 'compact' : 'standard';
    }

    public getButtonPositions(): Record<string, { x: number; y: number }> {
        if (this.isEnabled) {
            // All buttons on right side, thumb-reachable
            return {
                jump: { x: 0.85, y: 0.7 },
                attack: { x: 0.95, y: 0.55 },
                special: { x: 0.85, y: 0.4 },
                pause: { x: 0.95, y: 0.2 },
            };
        }
        // Standard layout
        return {
            jump: { x: 0.15, y: 0.7 },
            attack: { x: 0.95, y: 0.6 },
            special: { x: 0.85, y: 0.75 },
            pause: { x: 0.95, y: 0.1 },
        };
    }
}

Cognitive Accessibility

Difficulty Modes

export interface DifficultySettings {
    enemySpeed: number;
    enemyHealth: number;
    playerHealth: number;
    timeLimit: number | null;
    checkpoints: boolean;
    invincibilityFrames: number;
    enemyCount: number;
    hintFrequency: number;
    tutorialLength: 'full' | 'abbreviated' | 'skip';
}

export const DIFFICULTY_PRESETS: Record<string, DifficultySettings> = {
    easy: {
        enemySpeed: 0.7,
        enemyHealth: 0.7,
        playerHealth: 1.5,
        timeLimit: null, // No time limit
        checkpoints: true,
        invincibilityFrames: 90, // frames
        enemyCount: 0.7,
        hintFrequency: 1.5,
        tutorialLength: 'full',
    },
    normal: {
        enemySpeed: 1.0,
        enemyHealth: 1.0,
        playerHealth: 1.0,
        timeLimit: null,
        checkpoints: true,
        invincibilityFrames: 60,
        enemyCount: 1.0,
        hintFrequency: 1.0,
        tutorialLength: 'abbreviated',
    },
    hard: {
        enemySpeed: 1.3,
        enemyHealth: 1.3,
        playerHealth: 0.8,
        timeLimit: 180, // 3 minutes
        checkpoints: false,
        invincibilityFrames: 30,
        enemyCount: 1.2,
        hintFrequency: 0.5,
        tutorialLength: 'skip',
    },
};

export class DifficultyManager {
    private currentDifficulty = 'normal';

    constructor(private accessibility: AccessibilityService) {
        this.currentDifficulty = this.accessibility.get('difficulty') as string;
    }

    public getSettings(): DifficultySettings {
        return { ...DIFFICULTY_PRESETS[this.currentDifficulty] };
    }

    public setDifficulty(difficulty: string): void {
        if (DIFFICULTY_PRESETS[difficulty]) {
            this.currentDifficulty = difficulty;
            this.accessibility.set('difficulty', difficulty);
            GameEvents.emit(GameEvents.Types.DifficultyChanged, this.getSettings());
        }
    }

    public getCurrentDifficulty(): string {
        return this.currentDifficulty;
    }
}

Progressive Tutorial System

export class TutorialManager {
    private completedSteps: Set<string> = new Set();
    private currentStep = 0;
    private steps: TutorialStep[] = [];
    private accessibility: AccessibilityService;

    constructor(accessibility: AccessibilityService) {
        this.accessibility = accessibility;
        this.loadTutorialLength();
    }

    private loadTutorialLength(): void {
        const length = this.accessibility.get('tutorialLevel') as string;
        this.setupSteps(length);
    }

    private setupSteps(length: string): void {
        if (length === 'skip') {
            this.steps = [];
            return;
        }

        this.steps = [
            { id: 'movement', title: 'Movement', content: 'Use arrow keys to move', skip: length === 'abbreviated' },
            { id: 'jump', title: 'Jump', content: 'Press Space to jump', skip: length === 'abbreviated' },
            { id: 'attack', title: 'Attack', content: 'Press X to attack enemies', skip: false },
            { id: 'collect', title: 'Collect Items', content: 'Touch gems to collect them', skip: length === 'abbreviated' },
            { id: 'goal', title: 'Reach the Exit', content: 'Reach the door to complete the level', skip: false },
        ];
    }

    public start(): void {
        this.currentStep = 0;
        this.showCurrentStep();
    }

    public skip(): void {
        this.steps.forEach((step) => this.completedSteps.add(step.id));
        this.hideTutorial();
        GameEvents.emit(GameEvents.Types.TutorialCompleted);
    }

    private showCurrentStep(): void {
        const step = this.steps[this.currentStep];
        if (!step || step.skip) {
            this.nextStep();
            return;
        }

        // Show tutorial overlay
        TutorialUI.show({
            title: step.title,
            content: step.content,
            onNext: () => this.nextStep(),
            onSkip: () => this.skip(),
        });
    }

    private nextStep(): void {
        const step = this.steps[this.currentStep];
        if (step) this.completedSteps.add(step.id);

        this.currentStep++;
        if (this.currentStep < this.steps.length) {
            this.showCurrentStep();
        } else {
            this.hideTutorial();
            GameEvents.emit(GameEvents.Types.TutorialCompleted);
        }
    }

    public isStepCompleted(stepId: string): boolean {
        return this.completedSteps.has(stepId);
    }
}

Accessibility Menu UI

Accessibility Settings Screen

export class AccessibilityMenu {
    private container!: HTMLElement;
    private accessibility: AccessibilityService;

    constructor(accessibility: AccessibilityService) {
        this.accessibility = accessibility;
    }

    public show(): void {
        this.container = document.createElement('div');
        this.container.className = 'accessibility-menu';
        this.container.style.cssText = `
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: rgba(0, 0, 0, 0.9);
            z-index: 10000;
            display: flex;
            flex-direction: column;
            align-items: center;
            padding: 20px;
            overflow-y: auto;
            font-family: 'Outfit', sans-serif;
        `;

        this.container.innerHTML = `
            <h1 style="color: white; margin-bottom: 20px;">Accessibility Settings</h1>

            <div class="settings-sections" style="display: flex; flex-direction: column; gap: 30px; max-width: 600px; width: 100%;">
                ${this.renderVisualSection()}
                ${this.renderAuditorySection()}
                ${this.renderMotorSection()}
                ${this.renderCognitiveSection()}
            </div>

            <button class="close-btn" style="margin-top: 20px; padding: 12px 32px; font-size: 18px; background: #00d4ff; color: #0a0e27; border: none; border-radius: 8px; cursor: pointer;">
                Save & Close
            </button>
        `;

        document.body.appendChild(this.container);

        this.attachEventListeners();
    }

    private renderVisualSection(): string {
        return `
            <section style="background: rgba(255,255,255,0.1); padding: 20px; border-radius: 12px;">
                <h2 style="color: #00d4ff; margin-top: 0;">👁️ Visual</h2>
                ${this.renderToggle('High Contrast Mode', 'highContrastMode')}
                ${this.renderSelect('Colorblind Mode', 'colorblindMode', [
                    { value: 'none', label: 'None' },
                    { value: 'protanopia', label: 'Protanopia (Red-blind)' },
                    { value: 'deuteranopia', label: 'Deuteranopia (Green-blind)' },
                    { value: 'tritanopia', label: 'Tritanopia (Blue-blind)' },
                ])}
                ${this.renderSelect('Text Size', 'textSize', [
                    { value: 'small', label: 'Small' },
                    { value: 'medium', label: 'Medium' },
                    { value: 'large', label: 'Large' },
                    { value: 'extra-large', label: 'Extra Large' },
                ])}
                ${this.renderToggle('Reduce Motion', 'reduceMotion')}
                ${this.renderToggle('Reduce Particles', 'reduceParticles')}
            </section>
        `;
    }

    private renderAuditorySection(): string {
        return `
            <section style="background: rgba(255,255,255,0.1); padding: 20px; border-radius: 12px;">
                <h2 style="color: #00d4ff; margin-top: 0;">🔊 Audio</h2>
                ${this.renderSelect('Subtitles', 'subtitles', [
                    { value: 'none', label: 'Off' },
                    { value: 'basic', label: 'Basic' },
                    { value: 'full', label: 'Full' },
                ])}
                ${this.renderSlider('Subtitle Size', 'subtitleSize', 0.8, 1.5, 0.1)}
                ${this.renderToggle('Subtitle Background', 'subtitleBackground')}
                ${this.renderSlider('Master Volume', 'separateVolume.master', 0, 1, 0.1)}
                ${this.renderSlider('SFX Volume', 'separateVolume.sfx', 0, 1, 0.1)}
            </section>
        `;
    }

    private renderMotorSection(): string {
        return `
            <section style="background: rgba(255,255,255,0.1); padding: 20px; border-radius: 12px;">
                <h2 style="color: #00d4ff; margin-top: 0;">✋ Motor</h2>
                ${this.renderToggle('Auto-Fire', 'autoFire')}
                ${this.renderSlider('Auto-Fire Rate', 'autoFireRate', 100, 500, 50)}
                ${this.renderToggle('Hold to Shoot (vs Toggle)', 'holdToShoot')}
                ${this.renderToggle('Auto-Run', 'autoRun')}
                ${this.renderToggle('Slow Mode (50% Speed)', 'slowMode')}
                ${this.renderToggle('One-Hand Mode', 'oneHandMode')}
                ${this.renderSlider('Vibration Intensity', 'vibrationIntensity', 0, 100, 10)}
            </section>
        `;
    }

    private renderCognitiveSection(): string {
        return `
            <section style="background: rgba(255,255,255,0.1); padding: 20px; border-radius: 12px;">
                <h2 style="color: #00d4ff; margin-top: 0;">🧠 Cognitive</h2>
                ${this.renderSelect('Difficulty', 'difficulty', [
                    { value: 'easy', label: 'Easy' },
                    { value: 'normal', label: 'Normal' },
                    { value: 'hard', label: 'Hard' },
                ])}
                ${this.renderSelect('Tutorial Level', 'tutorialLevel', [
                    { value: 'full', label: 'Full Tutorial' },
                    { value: 'abbreviated', label: 'Abbreviated' },
                    { value: 'skip', label: 'Skip Tutorial' },
                ])}
                ${this.renderToggle('Show Hints', 'showHints')}
                ${this.renderToggle('Reduce UI Complexity', 'reduceUIComplexity')}
            </section>
        `;
    }

    private renderToggle(label: string, key: string): string {
        const checked = this.accessibility.get(key as keyof AccessibilitySettings) ? 'checked' : '';
        return `
            <label style="display: flex; align-items: center; gap: 10px; color: white; margin: 10px 0;">
                <input type="checkbox" data-key="${key}" ${checked}
                    style="width: 20px; height: 20px; cursor: pointer;">
                ${label}
            </label>
        `;
    }

    private renderSelect(label: string, key: string, options: Array<{value: string; label: string}>): string {
        const current = this.accessibility.get(key as keyof AccessibilitySettings) as string;
        return `
            <div style="margin: 10px 0;">
                <label style="color: white; display: block; margin-bottom: 5px;">${label}</label>
                <select data-key="${key}" style="padding: 8px; border-radius: 4px; background: #141834; color: white; border: 1px solid #2a2f55; width: 100%;">
                    ${options.map(o => `<option value="${o.value}" ${o.value === current ? 'selected' : ''}>${o.label}</option>`).join('')}
                </select>
            </div>
        `;
    }

    private renderSlider(label: string, key: string, min: number, max: number, step: number): string {
        const current = this.accessibility.get(key as keyof AccessibilitySettings) as number;
        return `
            <div style="margin: 10px 0;">
                <label style="color: white; display: block; margin-bottom: 5px;">${label}: <span id="${key}-value">${current}</span></label>
                <input type="range" data-key="${key}" min="${min}" max="${max}" step="${step}" value="${current}"
                    style="width: 100%; cursor: pointer;">
            </div>
        `;
    }

    private attachEventListeners(): void {
        // Checkboxes
        this.container.querySelectorAll('input[type="checkbox"]').forEach((input) => {
            input.addEventListener('change', (e) => {
                const key = (e.target as HTMLInputElement).dataset.key!;
                this.accessibility.set(key, (e.target as HTMLInputElement).checked);
            });
        });

        // Selects
        this.container.querySelectorAll('select').forEach((select) => {
            select.addEventListener('change', (e) => {
                const key = (e.target as HTMLSelectElement).dataset.key!;
                this.accessibility.set(key, (e.target as HTMLSelectElement).value);
            });
        });

        // Sliders
        this.container.querySelectorAll('input[type="range"]').forEach((input) => {
            input.addEventListener('input', (e) => {
                const key = (e.target as HTMLInputElement).dataset.key!;
                const value = parseFloat((e.target as HTMLInputElement).value);
                this.accessibility.set(key, value);
                const valueDisplay = this.container.querySelector(`#${key}-value`);
                if (valueDisplay) valueDisplay.textContent = value.toString();
            });
        });

        // Close button
        this.container.querySelector('.close-btn')?.addEventListener('click', () => this.hide());
    }

    public hide(): void {
        if (this.container && this.container.parentNode) {
            this.container.parentNode.removeChild(this.container);
        }
    }
}

Accessibility Testing Checklist

Pre-Release Testing

# Test Tools/Method
1 Colorblind simulation Daltonize extension, color blindness emulators
2 Keyboard-only navigation Tab through all elements, no mouse
3 Screen reader NVDA (Windows), VoiceOver (macOS), TalkBack (Android)
4 Contrast ratio WebAIM Contrast Checker (4.5:1 minimum)
5 Motor accessibility Switch control, one hand only
6 Cognitive load Timer off, reduced UI, hints on
7 Seizure safety Photosensitive epilepsy analysis
8 Audio-visual sync Captions match audio
9 Mobile accessibility TalkBack, Switch Control
10 Assistive tech compatibility Various AT combinations

Automated Testing

// Accessibility test suite
describe('Accessibility', () => {
    describe('Color Contrast', () => {
        it('should have 4.5:1 contrast for text', () => {
            const textColor = hexToRgb(THEME.text);
            const bgColor = hexToRgb(THEME.bg);
            const ratio = calculateContrastRatio(textColor, bgColor);
            expect(ratio).toBeGreaterThanOrEqual(4.5);
        });

        it('should have 3:1 for UI components', () => {
            const uiColor = hexToRgb(THEME.primary);
            const bgColor = hexToRgb(THEME.bg);
            const ratio = calculateContrastRatio(uiColor, bgColor);
            expect(ratio).toBeGreaterThanOrEqual(3);
        });
    });

    describe('Keyboard Navigation', () => {
        it('should focus all interactive elements', () => {
            const focusableElements = document.querySelectorAll(
                'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
            );

            focusableElements.forEach((el) => {
                el.focus();
                expect(document.activeElement).toBe(el);
            });
        });

        it('should have visible focus indicators', () => {
            const focusable = document.querySelectorAll('button')[0] as HTMLElement;
            focusable.focus();
            const style = window.getComputedStyle(focusable);
            expect(style.outlineStyle).not.toBe('none');
        });
    });

    describe('Screen Reader', () => {
        it('should have alt text for images', () => {
            const images = document.querySelectorAll('img');
            images.forEach((img) => {
                expect(img.getAttribute('alt')).toBeTruthy();
            });
        });

        it('should have aria-labels for icon buttons', () => {
            const iconButtons = document.querySelectorAll('button:not(:has-text())');
            iconButtons.forEach((btn) => {
                expect(btn.getAttribute('aria-label')).toBeTruthy();
            });
        });
    });
});

Common Mistakes

# Mistake Why It Fails Solution
1 Color-only feedback Colorblind players can't see Add icons, patterns, text
2 Audio-only alerts Deaf players miss content Visual + audio
3 Fixed controls Motor impaired can't play Full rebinding
4 No difficulty options Too hard for some Easy mode, assists
5 Flashing > 3/sec Seizure trigger Reduce, disable option
6 No pause Can't take breaks Always-pausable
7 Tiny targets Motor impaired miss 48x48px (or 10-15mm) minimum
8 No high contrast Low vision struggles Toggle option
9 Complex UI Cognitive impaired overwhelmed Simplify, reduce elements
10 No captions Deaf players miss dialogue Full caption system
11 Stat-bloat in skill trees Choice paralysis, cognitive overload Limit to game-changing milestones; add search & color-coding.
12 Heading line-height too loose Disrupted reading rhythm Reduce multiplier to 1.1x-1.2x for display text; keep 1.4x-1.5x for body text.
13 Mobile safe zone violations UI cut off by notches/finger occlusion Respect Safe Areas and place controls in bottom corner Thumb Zones.

Execution Checklist

Visual Accessibility

  • Colorblind modes implemented (protanopia, deuteranopia, tritanopia)
  • High contrast mode available
  • Text size options (small, medium, large, extra-large)
  • Typography scale line height scaling (1.1x-1.2x headings, 1.4x-1.5x body)
  • Line length limit respected (45-75 characters per line)
  • Reduce motion option
  • Reduce particles option
  • All colors meet 4.5:1 contrast (text) and 3:1 (UI)

Auditory Accessibility

  • Full caption system with speaker identification
  • Caption styling (size, background)
  • Separate volume controls (SFX, music, voice)
  • Visual alternatives to audio cues
  • Audio description option

Motor Accessibility

  • Full control remapping
  • Auto-fire option
  • Hold-to-shoot vs toggle
  • Auto-run option
  • Slow mode (50% speed)
  • One-hand mode
  • Vibration intensity control
  • Safe Areas respected and finger occlusion minimized (Thumb Zones for mobile)
  • Console 10-foot scaling and analog stick magnetic snapping specified

Cognitive Accessibility

  • Difficulty modes (easy, normal, hard) without shame framing
  • Tutorial length options (Invisible Onboarding preferred over text dumps)
  • Reduce UI complexity option
  • Avoid stat-bloat in skill trees (support color-coding/search for large trees)
  • Hint system
  • Always-pausable

Testing

  • Keyboard-only navigation works
  • Screen reader announces game state
  • Colorblind simulation tested
  • Contrast ratios verified
  • Mobile accessibility tested (TalkBack, Switch)
  • Two-handed landscape grip layout usability verified

Use it

Copy one of these into your project. Installing also returns the manifest and these snippets.

yaml
targets:
  - https://api.opensmartroute.ai/api/v1/registry/buiphucminhtam-forgewright-game-accessibility-engineer/manifest   # or paste the manifest below

Manifest

An Open Capability Manifest: the router reads it to know what this does, what it costs and when to pick it.

buiphucminhtam-forgewright-game-accessibility-engineer.ocm.jsonjson
{
  "ocm": "1",
  "id": "buiphucminhtam-forgewright-game-accessibility-engineer",
  "kind": "skill",
  "name": "game-accessibility-engineer",
  "description": "[production-grade internal] Game accessibility specialist ensuring games are playable by everyone, including users with visual, auditory, motor, and cognitive disabilities. Implements WCAG 2.1 AA compliance, game-specific accessibility features (difficulty settings, input remapping), and inclusive design practices. Routed via the production-grade orchestrator (Game Build mode).",
  "publisher": "buiphucminhtam",
  "version": "1.0.0",
  "capabilities": {
    "domains": [
      "legal"
    ],
    "tags": [
      "skill-md",
      "accessibility",
      "a11y",
      "inclusive-design",
      "wcag",
      "game-accessibility",
      "colorblind",
      "screen-reader",
      "motor-accessibility",
      "cognitive-accessibility"
    ],
    "languages": [
      "en"
    ]
  },
  "quality_prior": 0.6,
  "examples": [
    "[production-grade internal] Game accessibility specialist ensuring games are playable by everyone, including users with visual, auditory, motor, and cognitive disabilities. Implements WCAG 2.1 AA compliance, game-specific accessibility features (difficulty settings, input remapping), and inclusive design practices. Routed via the production-grade orchestrator (Game Build mode)."
  ],
  "primary": false,
  "metadata": {
    "source": {
      "provider": "github",
      "repository": "https://github.com/buiphucminhtam/forgewright",
      "path": "skills/game-accessibility-engineer/SKILL.md",
      "ref": "3a7fbaf5545134088fb88e3150c3288749987ff5",
      "url": "https://github.com/buiphucminhtam/forgewright/blob/3a7fbaf5545134088fb88e3150c3288749987ff5/skills/game-accessibility-engineer/SKILL.md",
      "key": "buiphucminhtam/forgewright/skills/game-accessibility-engineer/SKILL.md"
    }
  },
  "instructions": "# Game Accessibility Engineer — Inclusive Game Design Specialist\n\n## Protocols\n\n!`cat skills/_shared/game-visual-foundations.md 2>/dev/null || echo \"=== Visual Foundations not loaded ===\"`\n!`cat skills/_shared/protocols/ux-protocol.md 2>/dev/null || true`\n!`cat skills/_shared/protocols/input-validation.md 2>/dev/null || true`\n!`cat skills/_shared/protocols/tool-efficiency.md 2>/dev/null || true`\n!`cat skills/_shared/protocols/game-test-protocol.md 2>/dev/null || true`\n!`cat skills/_shared/protocols/quality-gate.md 2>/dev/null || true`\n!`cat skills/_shared/protocols/task-validator.md 2>/dev/nul",
  "cost": {
    "context_tokens": 10610
  }
}

Fetch it by URL: GET /api/v1/registry/buiphucminhtam-forgewright-game-accessibility-engineer/manifest?version=1.0.0

Reviews

Star ratings from people who tried it. One review per account; edit yours any time.

No reviews yet. Install it, try it, and be the first to rate it.