🎮ArcadeLab

Tanner's Super Fun Math Game

by EpicFlare19
1443 lines39.8 KB
▶ Play
<!DOCTYPE html>

<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Tanner's Super Fun Math Game</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
  background: #0a0a2e;
  overflow: hidden;
  touch-action: none;
  font-family: 'Segoe UI', sans-serif;
  user-select: none;
  -webkit-user-select: none;
}
canvas {
  display: block;
  margin: 0 auto;
}
#mathOverlay {
  display: none;
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  background: rgba(0,0,0,0.75);
  z-index: 100;
  justify-content: center;
  align-items: center;
  flex-direction: column;
}
#mathOverlay.active { display: flex; }
#mathBox {
  background: linear-gradient(135deg, #1a1a4e, #2a2a6e);
  border: 3px solid #ffcc00;
  border-radius: 16px;
  padding: 30px 40px;
  text-align: center;
  max-width: 90vw;
  width: 420px;
  box-shadow: 0 0 40px rgba(255,204,0,0.3);
}
#mathBox h2 {
  color: #ffcc00;
  font-size: 18px;
  margin-bottom: 10px;
  text-transform: uppercase;
  letter-spacing: 2px;
}
#mathQuestion {
  color: #fff;
  font-size: 28px;
  font-weight: bold;
  margin: 15px 0 20px;
  min-height: 40px;
}
#mathAnswers {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}
.mathBtn {
  background: linear-gradient(135deg, #2255aa, #3366cc);
  color: #fff;
  border: 2px solid #4488ee;
  border-radius: 10px;
  padding: 14px 10px;
  font-size: 20px;
  font-weight: bold;
  cursor: pointer;
  transition: all 0.15s;
}
.mathBtn:hover, .mathBtn:active {
  background: linear-gradient(135deg, #3366cc, #4488ee);
  transform: scale(1.05);
  border-color: #ffcc00;
}
#mathTimer {
  color: #ff6666;
  font-size: 16px;
  margin-top: 12px;
  font-weight: bold;
}
#mathResult {
  color: #fff;
  font-size: 22px;
  font-weight: bold;
  margin-top: 12px;
  min-height: 30px;
}
#touchControls {
  display: none;
  position: fixed;
  bottom: 0; left: 0; right: 0;
  height: 180px;
  z-index: 50;
  pointer-events: none;
  -webkit-tap-highlight-color: transparent;
}
#touchControls.active { display: block; }

/* === D-PAD === */
#dpad {
position: absolute;
left: 16px;
bottom: 20px;
width: 144px;
height: 144px;
pointer-events: none;
}
.dpad-btn {
position: absolute;
width: 48px;
height: 48px;
background: rgba(40, 40, 60, 0.85);
border: 2px solid rgba(180, 180, 220, 0.5);
color: rgba(220, 220, 255, 0.8);
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
pointer-events: auto;
touch-action: none;
-webkit-tap-highlight-color: transparent;
transition: background 0.05s;
}
.dpad-btn.pressed {
background: rgba(80, 120, 255, 0.7);
border-color: rgba(130, 180, 255, 0.9);
color: #fff;
}
#dUp    { top: 0; left: 48px; border-radius: 8px 8px 0 0; }
#dDown  { bottom: 0; left: 48px; border-radius: 0 0 8px 8px; }
#dLeft  { top: 48px; left: 0; border-radius: 8px 0 0 8px; }
#dRight { top: 48px; right: 0; border-radius: 0 8px 8px 0; }
#dCenter {
position: absolute;
top: 48px; left: 48px;
width: 48px; height: 48px;
background: rgba(30, 30, 50, 0.9);
border: 2px solid rgba(120, 120, 180, 0.3);
pointer-events: none;
}

/* === ACTION BUTTONS === */
#actionBtns {
position: absolute;
right: 16px;
bottom: 24px;
width: 160px;
height: 150px;
pointer-events: none;
}
.action-btn {
position: absolute;
width: 58px;
height: 58px;
border-radius: 50%;
border: 2.5px solid;
font-size: 13px;
font-weight: bold;
letter-spacing: 0.5px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
pointer-events: auto;
touch-action: none;
-webkit-tap-highlight-color: transparent;
transition: background 0.05s, transform 0.05s;
box-shadow: 0 3px 6px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.15);
}
.action-btn.pressed { transform: scale(0.92); }
.action-btn span.label { font-size: 9px; opacity: 0.6; margin-top: 1px; }

#btnA {
right: 0; bottom: 40px;
background: rgba(200, 50, 50, 0.75);
border-color: rgba(255, 100, 100, 0.7);
color: #ffcccc;
}
#btnA.pressed { background: rgba(255, 80, 80, 0.9); }

#btnB {
right: 68px; bottom: 0;
background: rgba(50, 140, 50, 0.75);
border-color: rgba(100, 220, 100, 0.7);
color: #ccffcc;
}
#btnB.pressed { background: rgba(80, 200, 80, 0.9); }

#btnX {
right: 68px; bottom: 80px;
background: rgba(50, 80, 200, 0.75);
border-color: rgba(100, 130, 255, 0.7);
color: #ccddff;
}
#btnX.pressed { background: rgba(80, 120, 255, 0.9); }

#btnY {
right: 0; bottom: 115px;
width: 46px; height: 46px;
background: rgba(180, 150, 30, 0.75);
border-color: rgba(255, 220, 60, 0.7);
color: #fff5cc;
font-size: 16px;
}
#btnY.pressed { background: rgba(255, 210, 50, 0.9); }
</style>

</head>
<body>
<canvas id="game"></canvas>

<div id="mathOverlay">
  <div id="mathBox">
    <h2>⚡ MATH ATTACK! ⚡</h2>
    <div id="mathQuestion"></div>
    <div id="mathAnswers"></div>
    <div id="mathTimer"></div>
    <div id="mathResult"></div>
  </div>
</div>

<div id="touchControls">
  <!-- D-Pad -->
  <div id="dpad">
    <div class="dpad-btn" id="dUp">▲</div>
    <div class="dpad-btn" id="dDown">▼</div>
    <div class="dpad-btn" id="dLeft">◀</div>
    <div class="dpad-btn" id="dRight">▶</div>
    <div id="dCenter"></div>
  </div>
  <!-- Action Buttons -->
  <div id="actionBtns">
    <div class="action-btn" id="btnA">A<span class="label">ATK</span></div>
    <div class="action-btn" id="btnB">B<span class="label">JUMP</span></div>
    <div class="action-btn" id="btnX">X<span class="label">JUMP</span></div>
    <div class="action-btn" id="btnY">★</div>
  </div>
</div>

<script>
// ============================================
// TANNER'S SUPER FUN MATH GAME
// A Smash Bros-style platform fighter
// ============================================

const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');

// --- SCREEN SETUP ---
let W, H, SCALE;
function resize() {
  W = window.innerWidth;
  H = window.innerHeight;
  canvas.width = W;
  canvas.height = H;
  SCALE = Math.min(W / 800, H / 600);
}
resize();
window.addEventListener('resize', resize);

// Touch detection — always show on touch devices
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
if (isTouchDevice) {
  document.getElementById('touchControls').classList.add('active');
}

// --- GAME STATE ---
const STATE = { MENU: 0, PLAYING: 1, MATH: 2, KO_PAUSE: 3, GAME_OVER: 4 };
let gameState = STATE.MENU;
let gameTime = 0;
let lastMathTime = 0;
const MATH_INTERVAL = 12000; // ms between math questions
let shakeTimer = 0;
let shakeIntensity = 0;
let particles = [];
let announceText = '';
let announceTimer = 0;
let koFreezeTimer = 0;

// --- INPUT ---
const keys = {};
const touchState = { left: false, right: false, jump: false, attack: false, special: false };
window.addEventListener('keydown', e => { keys[e.code] = true; e.preventDefault(); });
window.addEventListener('keyup', e => { keys[e.code] = false; e.preventDefault(); });

// Robust touch controls — track active touches per button
const activeTouches = {};

function bindTouch(elementId, onDown, onUp) {
  const el = document.getElementById(elementId);
  if (!el) return;

  el.addEventListener('touchstart', e => {
    e.preventDefault();
    e.stopPropagation();
    for (const t of e.changedTouches) {
      activeTouches[t.identifier] = elementId;
    }
    el.classList.add('pressed');
    onDown();
  }, { passive: false });

  el.addEventListener('touchend', e => {
    e.preventDefault();
    e.stopPropagation();
    for (const t of e.changedTouches) {
      delete activeTouches[t.identifier];
    }
    // Only release if no other touches on this element
    let stillPressed = false;
    for (const id in activeTouches) {
      if (activeTouches[id] === elementId) stillPressed = true;
    }
    if (!stillPressed) {
      el.classList.remove('pressed');
      onUp();
    }
  }, { passive: false });

  el.addEventListener('touchcancel', e => {
    e.preventDefault();
    for (const t of e.changedTouches) {
      delete activeTouches[t.identifier];
    }
    el.classList.remove('pressed');
    onUp();
  }, { passive: false });
}

// D-Pad
bindTouch('dLeft',  () => { touchState.left = true; },  () => { touchState.left = false; });
bindTouch('dRight', () => { touchState.right = true; }, () => { touchState.right = false; });
bindTouch('dUp',    () => { touchState.jump = true; },  () => { touchState.jump = false; });
bindTouch('dDown',  () => { /* crouch/fastfall someday */ }, () => {});

// Action buttons: A = attack, B = jump, X = jump, Y/★ = super
bindTouch('btnA', () => { touchState.attack = true; },  () => { touchState.attack = false; });
bindTouch('btnB', () => { touchState.jump = true; },    () => { touchState.jump = false; });
bindTouch('btnX', () => { touchState.jump = true; },    () => { touchState.jump = false; });
bindTouch('btnY', () => { touchState.special = true; }, () => { touchState.special = false; });

function isDown(action) {
  switch(action) {
    case 'left': return keys['ArrowLeft'] || keys['KeyA'] || touchState.left;
    case 'right': return keys['ArrowRight'] || keys['KeyD'] || touchState.right;
    case 'jump': return keys['ArrowUp'] || keys['KeyW'] || keys['Space'] || touchState.jump;
    case 'attack': return keys['KeyZ'] || keys['KeyJ'] || touchState.attack;
    case 'special': return keys['KeyX'] || keys['KeyK'] || touchState.special;
  }
}

let prevKeys = {};
function justPressed(action) {
  const down = isDown(action);
  const key = 'jp_' + action;
  const just = down && !prevKeys[key];
  prevKeys[key] = down;
  return just;
}

// --- PLATFORMS ---
function getPlatforms() {
  const cx = W / 2, cy = H / 2;
  const s = SCALE;
  return [
    // Main stage
    { x: cx - 220 * s, y: cy + 100 * s, w: 440 * s, h: 20 * s, main: true },
    // Floating platforms
    { x: cx - 280 * s, y: cy - 10 * s, w: 120 * s, h: 14 * s },
    { x: cx + 160 * s, y: cy - 10 * s, w: 120 * s, h: 14 * s },
    { x: cx - 60 * s, y: cy - 100 * s, w: 120 * s, h: 14 * s },
  ];
}

// --- FIGHTER CLASS ---
class Fighter {
  constructor(name, color, accentColor, x, isPlayer) {
    this.name = name;
    this.color = color;
    this.accent = accentColor;
    this.isPlayer = isPlayer;
    this.stocks = 3;
    this.damage = 0;
    this.superCharge = 0; // 0-100
    this.reset(x);
  }

  reset(x) {
    const s = SCALE;
    this.x = x || W / 2;
    this.y = H / 2 - 100 * s;
    this.vx = 0;
    this.vy = 0;
    this.w = 30 * s;
    this.h = 44 * s;
    this.grounded = false;
    this.jumps = 0;
    this.maxJumps = 2;
    this.facing = this.isPlayer ? 1 : -1;
    this.attacking = false;
    this.attackTimer = 0;
    this.attackCooldown = 0;
    this.hitlag = 0;
    this.stunTimer = 0;
    this.invincible = 60;
    this.animFrame = 0;
    this.animTimer = 0;
    this.superAttacking = false;
    this.superTimer = 0;
    this.hasHit = false;
  }

  get cx() { return this.x + this.w / 2; }
  get cy() { return this.y + this.h / 2; }

  getAttackBox() {
    const s = SCALE;
    const range = this.superAttacking ? 70 * s : 40 * s;
    return {
      x: this.facing > 0 ? this.x + this.w : this.x - range,
      y: this.y - 5 * s,
      w: range,
      h: this.h + 10 * s
    };
  }

  update(dt, platforms, opponent) {
    const s = SCALE;
    const gravity = 1800 * s;
    const speed = 350 * s;
    const jumpForce = -580 * s;
    const friction = 0.85;

    if (this.invincible > 0) this.invincible--;
    if (this.hitlag > 0) { this.hitlag--; return; }
    if (this.stunTimer > 0) { this.stunTimer -= dt; }

    if (this.attackCooldown > 0) this.attackCooldown -= dt;

    // AI or player control
    if (this.stunTimer <= 0) {
      if (this.isPlayer) {
        this.playerControl(speed, jumpForce, dt);
      } else {
        this.aiControl(speed, jumpForce, dt, opponent);
      }
    }

    // Physics
    this.vy += gravity * dt;
    this.x += this.vx * dt;
    this.y += this.vy * dt;

    // Platform collision
    this.grounded = false;
    for (const p of platforms) {
      if (this.vy >= 0 &&
          this.x + this.w > p.x && this.x < p.x + p.w &&
          this.y + this.h > p.y && this.y + this.h < p.y + p.h + this.vy * dt + 5 * s) {
        this.y = p.y - this.h;
        this.vy = 0;
        this.grounded = true;
        this.jumps = 0;
      }
    }

    // Air friction
    if (!this.grounded) {
      this.vx *= 0.995;
    }

    // Attack timer
    if (this.attacking) {
      this.attackTimer -= dt;
      if (this.attackTimer <= 0) {
        this.attacking = false;
        this.superAttacking = false;
      }
    }
    if (this.superAttacking) {
      this.superTimer -= dt;
    }

    // Animation
    this.animTimer += dt;
    if (this.animTimer > 0.15) {
      this.animTimer = 0;
      this.animFrame = (this.animFrame + 1) % 4;
    }

    // Check blast zone
    if (this.x < -100 * s || this.x > W + 100 * s ||
        this.y < -200 * s || this.y > H + 100 * s) {
      return 'blastzone';
    }
    return null;
  }

  playerControl(speed, jumpForce, dt) {
    if (isDown('left')) {
      this.vx = -speed;
      this.facing = -1;
    } else if (isDown('right')) {
      this.vx = speed;
      this.facing = 1;
    } else {
      this.vx *= 0.8;
    }

    if (justPressed('jump') && this.jumps < this.maxJumps) {
      this.vy = jumpForce;
      this.jumps++;
      spawnParticles(this.cx, this.y + this.h, 5, '#ffffff', 'burst');
    }

    if (justPressed('attack') && this.attackCooldown <= 0 && !this.attacking) {
      this.attacking = true;
      this.attackTimer = 0.2;
      this.attackCooldown = 0.35;
      this.hasHit = false;
    }

    if (justPressed('special') && this.superCharge >= 100 && !this.attacking) {
      this.attacking = true;
      this.superAttacking = true;
      this.attackTimer = 0.5;
      this.superTimer = 0.5;
      this.attackCooldown = 0.6;
      this.superCharge = 0;
      this.hasHit = false;
      shakeTimer = 0.3;
      shakeIntensity = 8;
      spawnParticles(this.cx, this.cy, 20, '#ffcc00', 'explosion');
    }
  }

  aiControl(speed, jumpForce, dt, player) {
    const dx = player.cx - this.cx;
    const dy = player.cy - this.cy;
    const dist = Math.sqrt(dx * dx + dy * dy);
    const s = SCALE;

    // Face player
    this.facing = dx > 0 ? 1 : -1;

    // Move toward player but keep fighting distance (not overlapping!)
    const idealDist = 55 * s;
    if (Math.abs(dx) > idealDist + 20 * s) {
      // Too far — approach
      this.vx = (dx > 0 ? 1 : -1) * speed * 0.65;
    } else if (Math.abs(dx) < idealDist - 10 * s) {
      // Too close — back off
      this.vx = (dx > 0 ? -1 : 1) * speed * 0.4;
    } else {
      this.vx *= 0.8;
    }

    // Jump if player is above or to recover
    if ((dy < -50 * s || this.y > H / 2 + 80 * s) && this.jumps < this.maxJumps && Math.random() < 0.03) {
      this.vy = jumpForce;
      this.jumps++;
    }

    // Attack when in range (not too close)
    if (dist < 90 * s && dist > 25 * s && this.attackCooldown <= 0 && !this.attacking && Math.random() < 0.04) {
      this.attacking = true;
      this.attackTimer = 0.2;
      this.attackCooldown = 0.6;
      this.hasHit = false;
    }

    // Use super if charged
    if (this.superCharge >= 100 && dist < 100 * s && dist > 25 * s && !this.attacking && Math.random() < 0.015) {
      this.attacking = true;
      this.superAttacking = true;
      this.attackTimer = 0.5;
      this.superTimer = 0.5;
      this.attackCooldown = 0.7;
      this.superCharge = 0;
      this.hasHit = false;
      shakeTimer = 0.3;
      shakeIntensity = 8;
      spawnParticles(this.cx, this.cy, 20, '#ff4444', 'explosion');
    }
  }

  draw() {
    const s = SCALE;
    ctx.save();

    // Invincibility flash
    if (this.invincible > 0 && Math.floor(this.invincible / 3) % 2) {
      ctx.globalAlpha = 0.4;
    }

    // Stun visual
    if (this.stunTimer > 0) {
      ctx.globalAlpha = 0.7 + Math.sin(Date.now() * 0.02) * 0.3;
    }

    const cx = this.x + this.w / 2;
    const cy = this.y + this.h / 2;

    // Super glow
    if (this.superCharge >= 100) {
      ctx.shadowColor = this.isPlayer ? '#ffcc00' : '#ff4444';
      ctx.shadowBlur = 15 + Math.sin(Date.now() * 0.005) * 8;
    }
    if (this.superAttacking) {
      ctx.shadowColor = this.isPlayer ? '#ffcc00' : '#ff4444';
      ctx.shadowBlur = 30;
    }

    // Body
    const bob = this.grounded ? Math.sin(this.animTimer * 15) * 2 * s : 0;
    ctx.fillStyle = this.color;
    roundRect(this.x + 2 * s, this.y + bob + 12 * s, this.w - 4 * s, this.h - 12 * s, 6 * s);
    ctx.fill();

    // Head
    ctx.fillStyle = this.color;
    ctx.beginPath();
    ctx.arc(cx, this.y + bob + 10 * s, 14 * s, 0, Math.PI * 2);
    ctx.fill();

    // Eyes
    const ex = this.facing * 4 * s;
    ctx.fillStyle = '#fff';
    ctx.beginPath();
    ctx.arc(cx + ex - 4 * s, this.y + bob + 8 * s, 4 * s, 0, Math.PI * 2);
    ctx.arc(cx + ex + 4 * s, this.y + bob + 8 * s, 4 * s, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = '#111';
    ctx.beginPath();
    ctx.arc(cx + ex - 3 * s + this.facing * s, this.y + bob + 8 * s, 2 * s, 0, Math.PI * 2);
    ctx.arc(cx + ex + 5 * s + this.facing * s, this.y + bob + 8 * s, 2 * s, 0, Math.PI * 2);
    ctx.fill();

    // Mouth
    if (this.attacking) {
      ctx.fillStyle = '#fff';
      ctx.beginPath();
      ctx.arc(cx + ex, this.y + bob + 15 * s, 3 * s, 0, Math.PI);
      ctx.fill();
    } else {
      ctx.strokeStyle = '#fff';
      ctx.lineWidth = 1.5 * s;
      ctx.beginPath();
      ctx.arc(cx + ex, this.y + bob + 14 * s, 3 * s, 0.1, Math.PI - 0.1);
      ctx.stroke();
    }

    // Attack arm
    if (this.attacking) {
      ctx.fillStyle = this.accent;
      const armX = this.facing > 0 ? this.x + this.w : this.x;
      const armLen = this.superAttacking ? 45 * s : 25 * s;
      ctx.fillRect(armX, this.y + bob + 20 * s, this.facing * armLen, 8 * s);

      // Attack effect
      if (this.superAttacking) {
        ctx.fillStyle = this.isPlayer ? 'rgba(255,204,0,0.6)' : 'rgba(255,68,68,0.6)';
        const ex2 = armX + this.facing * armLen;
        ctx.beginPath();
        ctx.arc(ex2, this.y + bob + 24 * s, 20 * s + Math.sin(Date.now() * 0.01) * 5 * s, 0, Math.PI * 2);
        ctx.fill();
      } else {
        ctx.fillStyle = 'rgba(255,255,255,0.5)';
        const ex2 = armX + this.facing * armLen;
        for (let i = 0; i < 3; i++) {
          const a = (Date.now() * 0.01 + i * 2) % 6.28;
          ctx.fillRect(ex2 + Math.cos(a) * 10 * s, this.y + bob + 18 * s + Math.sin(a) * 10 * s, 4 * s, 4 * s);
        }
      }
    }

    // Name tag
    ctx.shadowBlur = 0;
    ctx.fillStyle = 'rgba(0,0,0,0.5)';
    ctx.font = `bold ${11 * s}px sans-serif`;
    ctx.textAlign = 'center';
    const nameW = ctx.measureText(this.name).width;
    ctx.fillRect(cx - nameW / 2 - 4 * s, this.y - 18 * s, nameW + 8 * s, 14 * s);
    ctx.fillStyle = '#fff';
    ctx.fillText(this.name, cx, this.y - 7 * s);

    ctx.restore();
  }
}

// --- PLAYERS ---
let player, cpu;
function initFighters() {
  const s = SCALE;
  player = new Fighter('TANNER', '#2277ff', '#66aaff', W / 2 - 100 * s, true);
  cpu = new Fighter('MATH BOT', '#ee3344', '#ff7788', W / 2 + 100 * s, false);
}
initFighters();

// --- MATH SYSTEM ---
function genMathQuestion() {
  const types = [
    'orderOps', 'orderOps', 'fraction', 'fraction',
    'algebra', 'algebra', 'percent', 'exponent', 'negative'
  ];
  const type = types[Math.floor(Math.random() * types.length)];

  let question, answer, choices;

  switch (type) {
    case 'orderOps': {
      const templates = [
        () => {
          const a = randInt(2, 9), b = randInt(2, 9), c = randInt(1, 9);
          return { q: `${a} + ${b} × ${c}`, a: a + b * c };
        },
        () => {
          const a = randInt(10, 30), b = randInt(2, 5), c = randInt(1, 5);
          return { q: `${a} - ${b} × ${c}`, a: a - b * c };
        },
        () => {
          const a = randInt(2, 6), b = randInt(2, 6), c = randInt(1, 9);
          return { q: `(${a} + ${b}) × ${c}`, a: (a + b) * c };
        },
        () => {
          const a = randInt(2, 8), b = randInt(2, 8), c = randInt(2, 5), d = randInt(1, 5);
          return { q: `${a} + ${b} × ${c} - ${d}`, a: a + b * c - d };
        },
      ];
      const t = templates[Math.floor(Math.random() * templates.length)]();
      question = t.q;
      answer = t.a;
      break;
    }
    case 'fraction': {
      const templates = [
        () => {
          const pairs = [[2,3],[3,4],[2,5],[3,5],[4,5],[2,7],[3,7],[5,6]];
          const [a, b] = pairs[Math.floor(Math.random() * pairs.length)];
          const [c, d] = pairs[Math.floor(Math.random() * pairs.length)];
          const num = a * d + c * b;
          const den = b * d;
          const g = gcd(num, den);
          return { q: `${a}/${b} + ${c}/${d}`, a: `${num/g}/${den/g}` };
        },
        () => {
          const a = randInt(1, 5), b = randInt(2, 6);
          const c = randInt(1, 5), d = randInt(2, 6);
          const num = a * c, den = b * d;
          const g = gcd(num, den);
          const ans = den / g === 1 ? `${num/g}` : `${num/g}/${den/g}`;
          return { q: `${a}/${b} × ${c}/${d}`, a: ans };
        },
      ];
      const t = templates[Math.floor(Math.random() * templates.length)]();
      question = t.q;
      answer = t.a;
      // Generate wrong fraction answers
      choices = generateFractionChoices(answer);
      break;
    }
    case 'algebra': {
      const templates = [
        () => {
          const x = randInt(1, 12);
          const a = randInt(2, 6);
          const b = randInt(1, 20);
          return { q: `${a}x + ${b} = ${a * x + b}, x = ?`, a: x };
        },
        () => {
          const x = randInt(1, 10);
          const a = randInt(2, 5);
          const b = randInt(1, 10);
          return { q: `${a}x - ${b} = ${a * x - b}, x = ?`, a: x };
        },
        () => {
          const x = randInt(2, 8);
          return { q: `x² = ${x * x}, x = ?`, a: x };
        },
      ];
      const t = templates[Math.floor(Math.random() * templates.length)]();
      question = t.q;
      answer = t.a;
      break;
    }
    case 'percent': {
      const percents = [10, 15, 20, 25, 30, 40, 50, 75];
      const p = percents[Math.floor(Math.random() * percents.length)];
      const base = [40, 60, 80, 100, 120, 150, 200, 250, 300][Math.floor(Math.random() * 9)];
      question = `${p}% of ${base}`;
      answer = p * base / 100;
      break;
    }
    case 'exponent': {
      const base = randInt(2, 6);
      const exp = randInt(2, 4);
      question = `${base}${toSuperscript(exp)}`;
      answer = Math.pow(base, exp);
      break;
    }
    case 'negative': {
      const a = randInt(-15, -1);
      const b = randInt(-10, 15);
      const op = Math.random() > 0.5;
      if (op) {
        question = `(${a}) + ${b >= 0 ? b : '(' + b + ')'}`;
        answer = a + b;
      } else {
        question = `(${a}) × ${b >= 0 ? b : '(' + b + ')'}`;
        answer = a * b;
      }
      break;
    }
  }

  // Generate choices if not already set
  if (!choices) {
    choices = generateNumericChoices(answer);
  }

  return { question: `${question} = ?`, answer: String(answer), choices };
}

function generateNumericChoices(correct) {
  const c = [String(correct)];
  const offsets = shuffle([-3, -2, -1, 1, 2, 3, 5, -5, 4, -4]);
  let i = 0;
  while (c.length < 4 && i < offsets.length) {
    const wrong = String(correct + offsets[i]);
    if (!c.includes(wrong) && wrong !== String(correct)) c.push(wrong);
    i++;
  }
  while (c.length < 4) c.push(String(correct + c.length * 7));
  return shuffle(c);
}

function generateFractionChoices(correct) {
  const c = [correct];
  // Parse correct
  const parts = String(correct).split('/');
  let num = parseInt(parts[0]);
  let den = parts.length > 1 ? parseInt(parts[1]) : 1;

  const wrongs = [
    `${num + 1}/${den}`,
    `${num}/${den + 1}`,
    `${num - 1}/${den}`,
    `${num + 1}/${den + 1}`,
    `${num * 2}/${den * 2 + 1}`,
    `${num + 2}/${den}`,
  ];
  for (const w of shuffle(wrongs)) {
    if (!c.includes(w) && c.length < 4) c.push(w);
  }
  while (c.length < 4) c.push(`${num + c.length}/${den + 1}`);
  return shuffle(c);
}

function randInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }
function gcd(a, b) { a = Math.abs(a); b = Math.abs(b); while (b) { [a, b] = [b, a % b]; } return a; }
function shuffle(arr) { for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [arr[i], arr[j]] = [arr[j], arr[i]]; } return arr; }
function toSuperscript(n) { return String(n).split('').map(d => '⁰¹²³⁴⁵⁶⁷⁸⁹'[d]).join(''); }

// --- MATH OVERLAY ---
let mathActive = false;
let mathTimeLeft = 0;
let mathInterval = null;
let mathCorrectAnswer = '';

function showMathQuestion() {
  if (mathActive || gameState !== STATE.PLAYING) return;

  const q = genMathQuestion();
  gameState = STATE.MATH;
  mathActive = true;
  mathCorrectAnswer = q.answer;
  mathTimeLeft = 10;

  const overlay = document.getElementById('mathOverlay');
  overlay.classList.add('active');
  document.getElementById('mathQuestion').textContent = q.question;
  document.getElementById('mathResult').textContent = '';
  document.getElementById('mathTimer').textContent = `⏱ ${mathTimeLeft}s`;

  const answersDiv = document.getElementById('mathAnswers');
  answersDiv.innerHTML = '';
  q.choices.forEach(choice => {
    const btn = document.createElement('button');
    btn.className = 'mathBtn';
    btn.textContent = choice;
    btn.onclick = () => answerMath(choice);
    answersDiv.appendChild(btn);
  });

  mathInterval = setInterval(() => {
    mathTimeLeft--;
    document.getElementById('mathTimer').textContent = `⏱ ${mathTimeLeft}s`;
    if (mathTimeLeft <= 0) {
      answerMath(null);
    }
  }, 1000);
}

function answerMath(choice) {
  clearInterval(mathInterval);
  const resultEl = document.getElementById('mathResult');
  const answersDiv = document.getElementById('mathAnswers');

  // Highlight correct answer
  Array.from(answersDiv.children).forEach(btn => {
    btn.disabled = true;
    if (btn.textContent === mathCorrectAnswer) {
      btn.style.background = 'linear-gradient(135deg, #22aa44, #44cc66)';
      btn.style.borderColor = '#44cc66';
    } else if (btn.textContent === choice) {
      btn.style.background = 'linear-gradient(135deg, #aa2222, #cc4444)';
      btn.style.borderColor = '#cc4444';
    }
  });

  if (choice === mathCorrectAnswer) {
    resultEl.textContent = '✅ CORRECT! +30 SUPER CHARGE!';
    resultEl.style.color = '#44ff44';
    player.superCharge = Math.min(100, player.superCharge + 30);
    spawnParticles(player.cx, player.cy, 15, '#ffcc00', 'explosion');
  } else {
    resultEl.textContent = choice === null ?
      `⏰ TIME'S UP! Answer: ${mathCorrectAnswer}` :
      `❌ WRONG! Answer: ${mathCorrectAnswer}`;
    resultEl.style.color = '#ff6666';
    cpu.damage = Math.max(0, cpu.damage - 15);
    cpu.superCharge = Math.min(100, cpu.superCharge + 20);
  }

  setTimeout(() => {
    document.getElementById('mathOverlay').classList.remove('active');
    mathActive = false;
    gameState = STATE.PLAYING;
    lastMathTime = gameTime;
  }, 1800);
}

// --- PARTICLES ---
function spawnParticles(x, y, count, color, type) {
  for (let i = 0; i < count; i++) {
    const angle = Math.random() * Math.PI * 2;
    const speed = type === 'explosion' ? (80 + Math.random() * 200) * SCALE : (50 + Math.random() * 100) * SCALE;
    particles.push({
      x, y,
      vx: Math.cos(angle) * speed,
      vy: Math.sin(angle) * speed - (type === 'burst' ? 100 * SCALE : 0),
      life: 0.4 + Math.random() * 0.4,
      maxLife: 0.4 + Math.random() * 0.4,
      color,
      size: (2 + Math.random() * 4) * SCALE,
    });
  }
}

// --- COLLISION ---
function boxOverlap(a, b) {
  return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
}

function checkAttackHit(attacker, defender) {
  if (!attacker.attacking || attacker.hasHit) return;
  if (defender.invincible > 0) return;

  const atk = attacker.getAttackBox();
  const def = { x: defender.x, y: defender.y, w: defender.w, h: defender.h };

  if (boxOverlap(atk, def)) {
    attacker.hasHit = true; // Only one hit per swing!

    const isSuper = attacker.superAttacking;
    const dmg = isSuper ? 25 + Math.random() * 10 : 8 + Math.random() * 5;
    defender.damage += dmg;

    // Knockback based on damage %
    const kb = (defender.damage / 100 + 0.5) * (isSuper ? 900 : 400) * SCALE;
    const angle = isSuper ? -0.3 : -0.5;
    defender.vx = attacker.facing * kb * Math.cos(angle);
    defender.vy = kb * Math.sin(angle) - 200 * SCALE;
    defender.stunTimer = isSuper ? 0.5 : 0.2;

    // Hitlag
    attacker.hitlag = isSuper ? 6 : 3;
    defender.hitlag = isSuper ? 6 : 3;

    // Effects
    shakeTimer = isSuper ? 0.25 : 0.1;
    shakeIntensity = isSuper ? 10 : 4;
    spawnParticles(
      (attacker.cx + defender.cx) / 2,
      (attacker.cy + defender.cy) / 2,
      isSuper ? 15 : 6,
      isSuper ? '#ffcc00' : '#ffffff',
      isSuper ? 'explosion' : 'burst'
    );

    // Charge super from hitting
    attacker.superCharge = Math.min(100, attacker.superCharge + (isSuper ? 0 : 5));
  }
}

// --- DRAWING HELPERS ---
function roundRect(x, y, w, h, r) {
  ctx.beginPath();
  ctx.moveTo(x + r, y);
  ctx.lineTo(x + w - r, y);
  ctx.quadraticCurveTo(x + w, y, x + w, y + r);
  ctx.lineTo(x + w, y + h - r);
  ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
  ctx.lineTo(x + r, y + h);
  ctx.quadraticCurveTo(x, y + h, x, y + h - r);
  ctx.lineTo(x, y + r);
  ctx.quadraticCurveTo(x, y, x + r, y);
  ctx.closePath();
}

function drawBackground() {
  // Sky gradient
  const grad = ctx.createLinearGradient(0, 0, 0, H);
  grad.addColorStop(0, '#0a0a2e');
  grad.addColorStop(0.5, '#1a1a4e');
  grad.addColorStop(1, '#0d0d35');
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, W, H);

  // Stars
  const seed = 42;
  for (let i = 0; i < 60; i++) {
    const sx = ((seed * (i + 1) * 7.3) % W);
    const sy = ((seed * (i + 1) * 3.7) % (H * 0.6));
    const ss = 0.5 + ((i * 13) % 3) * 0.5;
    const bright = 0.3 + Math.sin(Date.now() * 0.001 + i) * 0.2;
    ctx.fillStyle = `rgba(255,255,255,${bright})`;
    ctx.fillRect(sx, sy, ss * SCALE, ss * SCALE);
  }
}

function drawPlatforms() {
  const platforms = getPlatforms();
  for (const p of platforms) {
    // Platform shadow
    ctx.fillStyle = 'rgba(0,0,0,0.3)';
    ctx.fillRect(p.x + 3 * SCALE, p.y + 3 * SCALE, p.w, p.h);

    // Platform body
    const grad = ctx.createLinearGradient(p.x, p.y, p.x, p.y + p.h);
    if (p.main) {
      grad.addColorStop(0, '#4a6fa5');
      grad.addColorStop(1, '#2a4a7a');
    } else {
      grad.addColorStop(0, '#5a7fb5');
      grad.addColorStop(1, '#3a5a8a');
    }
    ctx.fillStyle = grad;
    roundRect(p.x, p.y, p.w, p.h, 4 * SCALE);
    ctx.fill();

    // Top edge highlight
    ctx.fillStyle = 'rgba(255,255,255,0.2)';
    ctx.fillRect(p.x + 4 * SCALE, p.y, p.w - 8 * SCALE, 2 * SCALE);
  }
}

function drawHUD() {
  const s = SCALE;

  // Player HUD (left)
  drawFighterHUD(player, 20 * s, 15 * s, '#2277ff');

  // CPU HUD (right)
  drawFighterHUD(cpu, W - 220 * s, 15 * s, '#ee3344');

  // Announcement
  if (announceTimer > 0) {
    ctx.save();
    ctx.globalAlpha = Math.min(1, announceTimer * 2);
    ctx.fillStyle = '#fff';
    ctx.font = `bold ${36 * s}px sans-serif`;
    ctx.textAlign = 'center';
    ctx.shadowColor = '#000';
    ctx.shadowBlur = 10;
    ctx.fillText(announceText, W / 2, H / 2 - 40 * s);
    ctx.restore();
  }
}

function drawFighterHUD(fighter, x, y, color) {
  const s = SCALE;
  const w = 200 * s;

  // Background
  ctx.fillStyle = 'rgba(0,0,0,0.6)';
  roundRect(x - 5 * s, y - 5 * s, w + 10 * s, 70 * s, 8 * s);
  ctx.fill();

  // Name
  ctx.fillStyle = color;
  ctx.font = `bold ${14 * s}px sans-serif`;
  ctx.textAlign = 'left';
  ctx.fillText(fighter.name, x + 5 * s, y + 14 * s);

  // Damage %
  const dmgColor = fighter.damage < 50 ? '#44ff44' :
                    fighter.damage < 100 ? '#ffcc00' :
                    fighter.damage < 150 ? '#ff8800' : '#ff3333';
  ctx.fillStyle = dmgColor;
  ctx.font = `bold ${22 * s}px sans-serif`;
  ctx.fillText(`${Math.floor(fighter.damage)}%`, x + 5 * s, y + 40 * s);

  // Stocks
  for (let i = 0; i < 3; i++) {
    ctx.fillStyle = i < fighter.stocks ? color : 'rgba(255,255,255,0.15)';
    ctx.beginPath();
    ctx.arc(x + 140 * s + i * 22 * s, y + 14 * s, 7 * s, 0, Math.PI * 2);
    ctx.fill();
  }

  // Super meter
  ctx.fillStyle = 'rgba(255,255,255,0.15)';
  ctx.fillRect(x + 5 * s, y + 50 * s, w - 10 * s, 8 * s);
  const superColor = fighter.superCharge >= 100 ?
    `hsl(${(Date.now() * 0.2) % 360}, 100%, 60%)` :
    fighter.isPlayer ? '#ffcc00' : '#ff6666';
  ctx.fillStyle = superColor;
  ctx.fillRect(x + 5 * s, y + 50 * s, (w - 10 * s) * (fighter.superCharge / 100), 8 * s);

  // Super label
  ctx.fillStyle = fighter.superCharge >= 100 ? '#fff' : 'rgba(255,255,255,0.5)';
  ctx.font = `${9 * s}px sans-serif`;
  ctx.fillText(fighter.superCharge >= 100 ? '★ SUPER READY!' : 'SUPER', x + 8 * s, y + 57 * s);
}

function drawControls() {
  if (isTouchDevice) return;
  const s = SCALE;
  ctx.save();
  ctx.globalAlpha = 0.4;
  ctx.fillStyle = '#fff';
  ctx.font = `${10 * s}px sans-serif`;
  ctx.textAlign = 'center';
  ctx.fillText('← → Move  |  ↑/W Jump  |  Z Attack  |  X Super (when meter full)', W / 2, H - 12 * s);
  ctx.restore();
}

// --- MENU ---
function drawMenu() {
  drawBackground();
  const s = SCALE;

  // Title
  ctx.save();
  ctx.textAlign = 'center';

  // Glow
  ctx.shadowColor = '#ffcc00';
  ctx.shadowBlur = 20 + Math.sin(Date.now() * 0.003) * 10;
  ctx.fillStyle = '#ffcc00';
  ctx.font = `bold ${42 * s}px sans-serif`;
  ctx.fillText("TANNER'S", W / 2, H / 2 - 80 * s);
  ctx.font = `bold ${28 * s}px sans-serif`;
  ctx.fillText('SUPER FUN MATH GAME', W / 2, H / 2 - 40 * s);
  ctx.shadowBlur = 0;

  // Subtitle
  ctx.fillStyle = '#aaccff';
  ctx.font = `${16 * s}px sans-serif`;
  ctx.fillText('⚔️ Smash Bros × Math = Epic! ⚔️', W / 2, H / 2);

  // Characters preview
  ctx.fillStyle = '#2277ff';
  ctx.beginPath();
  ctx.arc(W / 2 - 60 * s, H / 2 + 50 * s, 20 * s, 0, Math.PI * 2);
  ctx.fill();
  ctx.fillStyle = '#fff';
  ctx.font = `bold ${10 * s}px sans-serif`;
  ctx.fillText('TANNER', W / 2 - 60 * s, H / 2 + 80 * s);

  ctx.fillStyle = '#ee3344';
  ctx.beginPath();
  ctx.arc(W / 2 + 60 * s, H / 2 + 50 * s, 20 * s, 0, Math.PI * 2);
  ctx.fill();
  ctx.fillStyle = '#fff';
  ctx.fillText('MATH BOT', W / 2 + 60 * s, H / 2 + 80 * s);

  ctx.fillStyle = '#fff';
  ctx.font = `${12 * s}px sans-serif`;
  ctx.fillText('VS', W / 2, H / 2 + 55 * s);

  // Start prompt
  const pulse = 0.5 + Math.sin(Date.now() * 0.004) * 0.5;
  ctx.globalAlpha = 0.6 + pulse * 0.4;
  ctx.fillStyle = '#fff';
  ctx.font = `bold ${18 * s}px sans-serif`;
  ctx.fillText(isTouchDevice ? 'TAP TO START' : 'PRESS ENTER TO START', W / 2, H / 2 + 130 * s);

  ctx.restore();
}

function drawGameOver() {
  const s = SCALE;
  ctx.save();
  ctx.fillStyle = 'rgba(0,0,0,0.7)';
  ctx.fillRect(0, 0, W, H);

  ctx.textAlign = 'center';
  const winner = player.stocks > 0 ? 'TANNER' : 'MATH BOT';
  const winColor = player.stocks > 0 ? '#2277ff' : '#ee3344';

  ctx.shadowColor = winColor;
  ctx.shadowBlur = 20;
  ctx.fillStyle = winColor;
  ctx.font = `bold ${48 * s}px sans-serif`;
  ctx.fillText(`${winner} WINS!`, W / 2, H / 2 - 20 * s);

  ctx.shadowBlur = 0;
  ctx.fillStyle = '#fff';
  ctx.font = `${16 * s}px sans-serif`;
  if (player.stocks > 0) {
    ctx.fillText('🧮 Math power prevails! 🧮', W / 2, H / 2 + 20 * s);
  } else {
    ctx.fillText('Study more math to get stronger!', W / 2, H / 2 + 20 * s);
  }

  const pulse = 0.5 + Math.sin(Date.now() * 0.004) * 0.5;
  ctx.globalAlpha = 0.6 + pulse * 0.4;
  ctx.font = `bold ${14 * s}px sans-serif`;
  ctx.fillText(isTouchDevice ? 'TAP TO PLAY AGAIN' : 'PRESS ENTER TO PLAY AGAIN', W / 2, H / 2 + 70 * s);

  ctx.restore();
}

// --- MAIN LOOP ---
let lastTime = performance.now();

function gameLoop(now) {
  const rawDt = (now - lastTime) / 1000;
  const dt = Math.min(rawDt, 0.05);
  lastTime = now;

  // Clear
  ctx.clearRect(0, 0, W, H);

  // Camera shake
  if (shakeTimer > 0) {
    shakeTimer -= dt;
    const sx = (Math.random() - 0.5) * shakeIntensity * SCALE;
    const sy = (Math.random() - 0.5) * shakeIntensity * SCALE;
    ctx.save();
    ctx.translate(sx, sy);
  }

  switch (gameState) {
    case STATE.MENU:
      drawMenu();
      if (keys['Enter'] || keys['Space']) {
        startGame();
      }
      break;

    case STATE.PLAYING:
      gameTime += dt * 1000;
      updatePlaying(dt);
      drawPlaying();
      break;

    case STATE.MATH:
      drawPlaying();
      break;

    case STATE.KO_PAUSE:
      koFreezeTimer -= dt;
      drawPlaying();
      if (koFreezeTimer <= 0) {
        if (player.stocks <= 0 || cpu.stocks <= 0) {
          gameState = STATE.GAME_OVER;
        } else {
          respawnFighters();
          gameState = STATE.PLAYING;
        }
      }
      break;

    case STATE.GAME_OVER:
      drawPlaying();
      drawGameOver();
      if (keys['Enter'] || keys['Space']) {
        startGame();
      }
      break;
  }

  // Update particles (always)
  updateParticles(dt);
  drawParticles();

  // Announcement
  if (announceTimer > 0) announceTimer -= dt;

  if (shakeTimer > 0 || shakeTimer + dt > 0) {
    ctx.restore();
  }

  // Reset justPressed tracking
  // (handled in justPressed function)

  requestAnimationFrame(gameLoop);
}

function startGame() {
  gameState = STATE.PLAYING;
  gameTime = 0;
  lastMathTime = 0;
  particles = [];
  initFighters();
  announceText = 'FIGHT!';
  announceTimer = 1.5;
}

function updatePlaying(dt) {
  const platforms = getPlatforms();

  // Check for math question trigger
  if (gameTime - lastMathTime > MATH_INTERVAL) {
    showMathQuestion();
    return;
  }

  // Update fighters
  const pResult = player.update(dt, platforms, cpu);
  const cResult = cpu.update(dt, platforms, player);

  // Body-to-body separation (prevent overlapping)
  const overlap = (player.x + player.w) - cpu.x;
  const overlap2 = (cpu.x + cpu.w) - player.x;
  // Check if they're actually overlapping vertically too
  if (player.y + player.h > cpu.y && player.y < cpu.y + cpu.h) {
    if (player.cx < cpu.cx) {
      // Player is left of CPU
      const ox = (player.x + player.w) - cpu.x;
      if (ox > 0) {
        player.x -= ox / 2;
        cpu.x += ox / 2;
      }
    } else {
      // CPU is left of player
      const ox = (cpu.x + cpu.w) - player.x;
      if (ox > 0) {
        cpu.x -= ox / 2;
        player.x += ox / 2;
      }
    }
  }

  // Check hits
  checkAttackHit(player, cpu);
  checkAttackHit(cpu, player);

  // Check KOs
  if (pResult === 'blastzone') {
    player.stocks--;
    player.damage = 0;
    handleKO('TANNER', '#2277ff');
  }
  if (cResult === 'blastzone') {
    cpu.stocks--;
    cpu.damage = 0;
    handleKO('MATH BOT', '#ee3344');
  }
}

function handleKO(name, color) {
  gameState = STATE.KO_PAUSE;
  koFreezeTimer = 1.5;
  announceText = `💥 ${name} KO'd!`;
  announceTimer = 1.5;
  shakeTimer = 0.4;
  shakeIntensity = 12;
  spawnParticles(W / 2, H / 2, 30, color, 'explosion');
}

function respawnFighters() {
  const s = SCALE;
  if (player.stocks > 0) player.reset(W / 2 - 100 * s);
  if (cpu.stocks > 0) cpu.reset(W / 2 + 100 * s);
  announceText = 'GO!';
  announceTimer = 1;
}

function drawPlaying() {
  drawBackground();
  drawPlatforms();
  player.draw();
  cpu.draw();
  drawHUD();
  drawControls();
}

function updateParticles(dt) {
  for (let i = particles.length - 1; i >= 0; i--) {
    const p = particles[i];
    p.life -= dt;
    if (p.life <= 0) { particles.splice(i, 1); continue; }
    p.x += p.vx * dt;
    p.y += p.vy * dt;
    p.vy += 400 * SCALE * dt;
    p.vx *= 0.98;
  }
}

function drawParticles() {
  for (const p of particles) {
    ctx.globalAlpha = p.life / p.maxLife;
    ctx.fillStyle = p.color;
    ctx.fillRect(p.x - p.size / 2, p.y - p.size / 2, p.size, p.size);
  }
  ctx.globalAlpha = 1;
}

// Start game via tap or click (only on menu/gameover screens)
canvas.addEventListener('click', () => {
  if (gameState === STATE.MENU || gameState === STATE.GAME_OVER) startGame();
});
canvas.addEventListener('touchend', e => {
  if (gameState === STATE.MENU || gameState === STATE.GAME_OVER) {
    e.preventDefault();
    startGame();
  }
  // During gameplay, do NOT preventDefault — let events flow naturally
}, { passive: false });

// GO
requestAnimationFrame(gameLoop);
</script>

</body>
</html>

Game Source: Tanner's Super Fun Math Game

Creator: EpicFlare19

Libraries: none

Complexity: complex (1443 lines, 39.8 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: tanner-s-super-fun-math-game-epicflare19" to link back to the original. Then publish at arcadelab.ai/publish.