NFR Design Patterns - Nokia Snake Game

Design patterns used

NFR Design Patterns - Nokia Snake Game

Overview

Unit Name: Nokia Snake Game (Monolithic)
Purpose: Define design patterns for implementing non-functional requirements
Scope: Performance, security, mobile, offline, resilience, and accessibility patterns

Performance Design Patterns

Pattern 1: Fixed Time Step with Frame Skipping

Category: Performance - Game Loop
Problem: Need consistent gameplay across devices with varying performance
Solution: Fixed time step for game logic, skip rendering frames if behind schedule

Implementation:

class GameLoop {
  constructor() {
    this.targetFPS = 60;
    this.fixedTimeStep = 1000 / this.targetFPS; // 16.67ms
    this.maxFrameSkip = 5; // Maximum frames to skip
    this.accumulator = 0;
    this.lastTime = performance.now();
  }
  
  start() {
    this.running = true;
    this.loop();
  }
  
  loop() {
    if (!this.running) return;
    
    const currentTime = performance.now();
    const deltaTime = currentTime - this.lastTime;
    this.lastTime = currentTime;
    
    // Add to accumulator
    this.accumulator += deltaTime;
    
    // Update game logic with fixed time step
    let framesSkipped = 0;
    while (this.accumulator >= this.fixedTimeStep && framesSkipped < this.maxFrameSkip) {
      this.updateGameLogic(this.fixedTimeStep);
      this.accumulator -= this.fixedTimeStep;
      framesSkipped++;
    }
    
    // Render current state
    this.render();
    
    // Continue loop
    requestAnimationFrame(() => this.loop());
  }
  
  updateGameLogic(deltaTime) {
    // Update snake position, check collisions, etc.
    gameState.update(deltaTime);
  }
  
  render() {
    // Render current game state
    renderer.render(gameState);
  }
}

Benefits:

  • Consistent gameplay speed across devices
  • Smooth rendering on capable devices
  • Graceful degradation on slow devices

Trade-offs:

  • May skip rendering frames on slow devices
  • Slightly more complex than simple loop

Pattern 2: Object Pooling for Game Entities

Category: Performance - Memory Management
Problem: Frequent allocation/deallocation of snake segments and food causes GC pauses
Solution: Pre-allocate object pools and reuse objects

Implementation:

class ObjectPool {
  constructor(createFn, resetFn, initialSize = 100) {
    this.createFn = createFn;
    this.resetFn = resetFn;
    this.available = [];
    this.inUse = new Set();
    
    // Pre-allocate objects
    for (let i = 0; i < initialSize; i++) {
      this.available.push(this.createFn());
    }
  }
  
  acquire() {
    let obj;
    if (this.available.length > 0) {
      obj = this.available.pop();
    } else {
      obj = this.createFn();
    }
    this.inUse.add(obj);
    return obj;
  }
  
  release(obj) {
    if (this.inUse.has(obj)) {
      this.inUse.delete(obj);
      this.resetFn(obj);
      this.available.push(obj);
    }
  }
  
  releaseAll() {
    this.inUse.forEach(obj => {
      this.resetFn(obj);
      this.available.push(obj);
    });
    this.inUse.clear();
  }
}

// Usage
const snakeSegmentPool = new ObjectPool(
  () => ({ x: 0, y: 0, color: '#00FF00' }),
  (segment) => { segment.x = 0; segment.y = 0; },
  200 // Pre-allocate 200 segments
);

const foodPool = new ObjectPool(
  () => ({ x: 0, y: 0, type: 'REGULAR', value: 10 }),
  (food) => { food.x = 0; food.y = 0; food.type = 'REGULAR'; },
  10 // Pre-allocate 10 food items
);

Benefits:

  • Eliminates GC pauses during gameplay
  • Predictable memory usage
  • Improved performance

Trade-offs:

  • Slightly more complex object management
  • Initial memory allocation

Pattern 3: WebGL Rendering with Canvas 2D Fallback

Category: Performance - Rendering
Problem: Need hardware acceleration but must support devices without WebGL
Solution: Feature detection with graceful fallback

Implementation:

class RendererFactory {
  static createRenderer(canvas) {
    // Feature detection before initialization
    if (this.isWebGLAvailable(canvas)) {
      console.log('Using WebGL renderer');
      return new WebGLRenderer(canvas);
    } else {
      console.log('WebGL not available, using Canvas 2D renderer');
      return new Canvas2DRenderer(canvas);
    }
  }
  
  static isWebGLAvailable(canvas) {
    try {
      const gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
      return !!gl;
    } catch (e) {
      return false;
    }
  }
}

class WebGLRenderer {
  constructor(canvas) {
    this.canvas = canvas;
    this.gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
    this.initializeWebGL();
  }
  
  initializeWebGL() {
    // Set up shaders, buffers, etc.
  }
  
  render(gameState) {
    // WebGL rendering implementation
  }
}

class Canvas2DRenderer {
  constructor(canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
  }
  
  render(gameState) {
    // Canvas 2D rendering implementation
  }
}

Benefits:

  • Hardware acceleration when available
  • Broad device compatibility
  • Transparent fallback

Trade-offs:

  • Two rendering implementations to maintain
  • Feature detection overhead

Security Design Patterns

Pattern 4: Whitelist-Based Input Validation

Category: Security - Input Validation (SECURITY-05)
Problem: Need to validate all user input to prevent injection attacks
Solution: Whitelist validation with comprehensive checks

Implementation:

class InputValidator {
  constructor() {
    this.allowedKeys = new Set([
      'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight',
      ' ', 'Escape', 'Enter', 'Tab'
    ]);
    
    this.allowedEventTypes = new Set(['keydown', 'keyup']);
  }
  
  validateKeyboardInput(event) {
    // Type checking
    if (!event || typeof event !== 'object') {
      this.logSecurityEvent('INVALID_INPUT_TYPE', { type: typeof event });
      return false;
    }
    
    // Property validation
    if (!event.hasOwnProperty('key') || !event.hasOwnProperty('type')) {
      this.logSecurityEvent('MISSING_INPUT_PROPERTIES', { event });
      return false;
    }
    
    // Whitelist validation - key
    if (!this.allowedKeys.has(event.key)) {
      this.logSecurityEvent('INVALID_KEY', { key: event.key });
      return false;
    }
    
    // Whitelist validation - event type
    if (!this.allowedEventTypes.has(event.type)) {
      this.logSecurityEvent('INVALID_EVENT_TYPE', { type: event.type });
      return false;
    }
    
    // Game state validation
    if (!this.validateGameMove(event.key, gameState)) {
      this.logSecurityEvent('INVALID_GAME_MOVE', { key: event.key, state: gameState.status });
      return false;
    }
    
    return true;
  }
  
  validateGameMove(key, gameState) {
    // Only validate direction keys during gameplay
    if (gameState.status !== 'PLAYING') return true;
    
    const currentDirection = gameState.snake.direction;
    const oppositeDirections = {
      'UP': 'DOWN', 'DOWN': 'UP',
      'LEFT': 'RIGHT', 'RIGHT': 'LEFT'
    };
    
    // Prevent 180-degree turns
    const newDirection = this.keyToDirection(key);
    if (newDirection && oppositeDirections[currentDirection] === newDirection) {
      return false;
    }
    
    return true;
  }
  
  keyToDirection(key) {
    const mapping = {
      'ArrowUp': 'UP', 'ArrowDown': 'DOWN',
      'ArrowLeft': 'LEFT', 'ArrowRight': 'RIGHT'
    };
    return mapping[key] || null;
  }
  
  logSecurityEvent(type, details) {
    securityLogger.log({
      type: 'INPUT_VALIDATION_FAILURE',
      subtype: type,
      details,
      timestamp: new Date().toISOString(),
      severity: 'MEDIUM'
    });
  }
}

Benefits:

  • Comprehensive input validation
  • Security event logging
  • Prevents invalid game states

Trade-offs:

  • Additional validation overhead
  • More complex input handling

Pattern 5: Global Error Boundary with Silent Recovery

Category: Security - Exception Handling (SECURITY-15)
Problem: Need to handle all errors gracefully without exposing sensitive information
Solution: Global error boundary with safe state fallback

Implementation:

class ErrorBoundary {
  constructor() {
    this.setupGlobalHandlers();
  }
  
  setupGlobalHandlers() {
    // Unhandled errors
    window.addEventListener('error', (event) => {
      this.handleError(event.error, 'UNHANDLED_ERROR', event);
      event.preventDefault();
    });
    
    // Unhandled promise rejections
    window.addEventListener('unhandledrejection', (event) => {
      this.handleError(event.reason, 'UNHANDLED_REJECTION', event);
      event.preventDefault();
    });
  }
  
  handleError(error, type, context) {
    // Log error securely (no sensitive data)
    this.logError(error, type, context);
    
    // Attempt recovery
    this.recoverFromError(error, type);
  }
  
  logError(error, type, context) {
    const errorLog = {
      type,
      message: error?.message || 'Unknown error',
      timestamp: new Date().toISOString(),
      severity: 'HIGH',
      userAgent: navigator.userAgent,
      // DO NOT include stack trace or sensitive data
    };
    
    securityLogger.log(errorLog);
    console.error('[ERROR]', errorLog);
  }
  
  recoverFromError(error, type) {
    try {
      // Attempt to save current state
      if (window.gameState && gameState.status === 'PLAYING') {
        storageManager.saveErrorRecoveryState(gameState);
      }
      
      // Reset to safe state (main menu)
      this.resetToSafeState();
    } catch (recoveryError) {
      // If recovery fails, force reload
      console.error('[RECOVERY_FAILED]', recoveryError);
      this.forceReload();
    }
  }
  
  resetToSafeState() {
    // Silent recovery - no user notification
    gameState.reset();
    gameState.status = 'MAIN_MENU';
    renderer.render(gameState);
  }
  
  forceReload() {
    // Last resort - reload page
    setTimeout(() => {
      window.location.reload();
    }, 1000);
  }
}

Benefits:

  • Graceful error handling
  • No sensitive data exposure
  • Automatic recovery

Trade-offs:

  • Silent failures may confuse users
  • Loss of current game state

Pattern 6: Audit Logging with Rotation

Category: Security - Audit Logging
Problem: Need comprehensive audit trail without filling localStorage
Solution: Structured logging with automatic rotation

Implementation:

class SecurityLogger {
  constructor() {
    this.storageKey = 'nokiaSnake_auditLog';
    this.maxEntries = 1000;
  }
  
  log(event) {
    const logEntry = {
      id: this.generateId(),
      timestamp: new Date().toISOString(),
      type: event.type,
      subtype: event.subtype || null,
      severity: event.severity || 'INFO',
      details: this.sanitizeDetails(event.details),
      userAgent: navigator.userAgent,
      sessionId: this.getSessionId()
    };
    
    // Log to console
    console.log(`[AUDIT:${logEntry.severity}]`, logEntry);
    
    // Persist to localStorage
    this.persistLog(logEntry);
  }
  
  persistLog(entry) {
    try {
      const logs = this.getLogs();
      logs.push(entry);
      
      // Rotate if exceeds max entries
      if (logs.length > this.maxEntries) {
        logs.shift(); // Remove oldest entry
      }
      
      localStorage.setItem(this.storageKey, JSON.stringify(logs));
    } catch (error) {
      console.error('[AUDIT_LOG_FAILED]', error);
    }
  }
  
  getLogs() {
    try {
      const data = localStorage.getItem(this.storageKey);
      return data ? JSON.parse(data) : [];
    } catch (error) {
      console.error('[AUDIT_LOG_READ_FAILED]', error);
      return [];
    }
  }
  
  sanitizeDetails(details) {
    // Remove any sensitive data
    if (!details) return null;
    
    const sanitized = { ...details };
    
    // Remove potential PII
    delete sanitized.email;
    delete sanitized.username;
    delete sanitized.password;
    
    return sanitized;
  }
  
  generateId() {
    return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  }
  
  getSessionId() {
    let sessionId = sessionStorage.getItem('nokiaSnake_sessionId');
    if (!sessionId) {
      sessionId = this.generateId();
      sessionStorage.setItem('nokiaSnake_sessionId', sessionId);
    }
    return sessionId;
  }
  
  // Query methods
  getLogsByType(type) {
    return this.getLogs().filter(log => log.type === type);
  }
  
  getLogsBySeverity(severity) {
    return this.getLogs().filter(log => log.severity === severity);
  }
  
  getLogsInTimeRange(startTime, endTime) {
    return this.getLogs().filter(log => {
      const timestamp = new Date(log.timestamp).getTime();
      return timestamp >= startTime && timestamp <= endTime;
    });
  }
}

Benefits:

  • Comprehensive audit trail
  • Automatic rotation prevents storage overflow
  • Queryable log data

Trade-offs:

  • localStorage space usage
  • Potential performance impact on writes

Mobile Optimization Patterns

Pattern 7: Touch Gesture Recognition (Medium Sensitivity)

Category: Mobile - Touch Input
Problem: Need intuitive touch controls for mobile devices
Solution: Swipe gesture detection with 30px minimum distance

Implementation: See logical-components.md for TouchController implementation

Benefits:

  • Intuitive mobile controls
  • Prevents accidental inputs
  • Responsive touch feedback

Pattern 8: Responsive Canvas Scaling

Category: Mobile - Responsive Design
Problem: Need to adapt to various screen sizes
Solution: Fit to container while maintaining aspect ratio

Implementation: See logical-components.md for canvas scaling implementation

Benefits:

  • Works on all screen sizes
  • Maintains visual consistency
  • No distortion

Offline Functionality Patterns

Pattern 9: State Persistence on User Actions

Category: Offline - Data Persistence
Problem: Need to save game state without impacting performance
Solution: Persist only on pause and game over events

Implementation: See logical-components.md for StorageManager implementation

Benefits:

  • Minimal performance impact
  • User-initiated saves
  • Data safety on critical events

Pattern 10: Centralized State Management

Category: Offline - State Management
Problem: Need predictable state updates and component communication
Solution: Redux-like store with unidirectional data flow

Implementation: See logical-components.md for GameStore implementation

Benefits:

  • Predictable state updates
  • Easy debugging
  • Centralized communication

Performance Monitoring Pattern

Pattern 11: FPS and Memory Tracking

Category: Performance - Monitoring
Problem: Need to monitor performance during gameplay
Solution: Track FPS and memory usage with minimal overhead

Implementation: See logical-components.md for PerformanceMonitor implementation

Benefits:

  • Real-time performance insights
  • Identify performance issues
  • Minimal overhead

Pattern Summary

Pattern Category Priority Complexity
Fixed Time Step with Frame Skipping Performance High Medium
Object Pooling Performance High Medium
WebGL with Canvas 2D Fallback Performance High High
Whitelist Input Validation Security Critical Medium
Global Error Boundary Security Critical Medium
Audit Logging with Rotation Security High Low
Touch Gesture Recognition Mobile High Medium
Responsive Canvas Scaling Mobile High Low
State Persistence on User Actions Offline Medium Low
Centralized State Management Offline High Medium
FPS and Memory Tracking Performance Medium Low

Pattern Dependencies

Fixed Time Step ──> Object Pooling
                └──> WebGL Rendering
                
Whitelist Validation ──> Audit Logging
                     └──> Error Boundary
                     
Touch Gestures ──> Centralized State
               └──> Input Validation
               
State Persistence ──> Centralized State
                  └──> Audit Logging

Implementation Priority

  1. Phase 1 - Core Patterns (Must have):

    • Centralized State Management
    • Fixed Time Step Game Loop
    • WebGL with Fallback
    • Whitelist Input Validation
    • Global Error Boundary
  2. Phase 2 - Optimization Patterns (Should have):

    • Object Pooling
    • Audit Logging
    • Touch Gesture Recognition
    • Responsive Canvas Scaling
  3. Phase 3 - Enhancement Patterns (Nice to have):

    • State Persistence
    • Performance Monitoring

Security Compliance

All patterns comply with security baseline requirements:

  • SECURITY-05 (Input Validation): Pattern 4 - Whitelist-Based Input Validation
  • SECURITY-15 (Exception Handling): Pattern 5 - Global Error Boundary

Conclusion

These design patterns provide a comprehensive framework for implementing all NFR requirements while maintaining code quality, performance, and security.