79731582

Date: 2025-08-11 00:42:38
Score: 1.5
Natty:
Report link

Nice — let’s make a playable browser boxing game right now. Below is a single self-contained index.html you can save and open in any modern browser. It’s lightweight, uses <canvas>, supports:

Save all of this as index.html and open it in your browser.

Game file — save as index.html

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Mini Boxing — Single HTML Game</title>
<style>
  :root{
    --bg:#111;
    --panel:#0f1720;
    --accent:#f97316;
    --bar-bg:#334155;
    --friendly:#10b981;
    --danger:#ef4444;
    font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
  }
  html,body{height:100%;margin:0;background:linear-gradient(180deg,var(--bg),#000);color:#eee;display:flex;align-items:center;justify-content:center}
  .wrap{width:960px;max-width:96vw;background:linear-gradient(180deg,#08101a,#0c1319);border-radius:12px;padding:18px;box-shadow:0 10px 30px rgba(0,0,0,0.6)}
  header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}
  h1{font-size:18px;margin:0}
  .controls{font-size:13px;opacity:0.9}
  canvas{display:block;background:#081018;border-radius:8px; width: 100%; height: 480px; image-rendering: auto;}
  .hud{display:flex;gap:12px;margin-top:8px;align-items:center;justify-content:space-between}
  .panel{background:var(--panel);padding:10px;border-radius:8px;flex:1}
  .bars{display:flex;flex-direction:column;gap:8px}
  .bar{height:14px;background:var(--bar-bg);border-radius:7px;overflow:hidden;position:relative}
  .bar > i{position:absolute;left:0;top:0;bottom:0;width:100%;transform-origin:left;transition:width 0.12s linear}
  .health-i{background:linear-gradient(90deg,var(--friendly),#60a5fa)}
  .enemy-i{background:linear-gradient(90deg,#f43f5e,#f97316)}
  .stamina-i{background:linear-gradient(90deg,#c7f9dc,#60a5fa)}
  .meta{display:flex;gap:8px;align-items:center;justify-content:space-between;margin-top:8px}
  .meta .big{font-weight:700}
  .footer{display:flex;gap:8px;justify-content:flex-end;margin-top:10px}
  button{background:var(--accent);border:none;padding:8px 12px;border-radius:8px;color:#081018;font-weight:700;cursor:pointer}
  button.secondary{background:#334155;color:#cbd5e1}
  small{opacity:0.8}
</style>
</head>
<body>
<div class="wrap">
  <header>
    <h1>Mini Boxing — vs AI</h1>
    <div class="controls">
      Controls — Move: A / D, Jab: J, Heavy: K, Dodge: L, Block: Space, Pause: P
    </div>
  </header>

  <canvas id="game" width="920" height="480"></canvas>

  <div class="hud">
    <div class="panel" style="flex:0.62">
      <div style="display:flex;justify-content:space-between;align-items:center">
        <div>
          <div style="font-size:13px">Player</div>
          <div class="bars" style="width:320px">
            <div class="bar"><i id="player-health" class="health-i" style="width:100%"></i></div>
            <div class="bar"><i id="player-stam" class="stamina-i" style="width:100%"></i></div>
          </div>
        </div>
        <div style="text-align:right">
          <div style="font-size:13px">Opponent</div>
          <div class="bars" style="width:200px">
            <div class="bar"><i id="enemy-health" class="enemy-i" style="width:100%"></i></div>
            <div class="bar"><i id="enemy-stam" class="stamina-i" style="width:100%"></i></div>
          </div>
        </div>
      </div>

      <div class="meta">
        <div><small>Round:</small> <span id="round" class="big">1</span></div>
        <div><small>Time left:</small> <span id="timer" class="big">60</span>s</div>
        <div><small>State:</small> <span id="state">FIGHT</span></div>
      </div>
    </div>

    <div style="flex:0.38;display:flex;flex-direction:column;gap:8px">
      <div class="panel" style="text-align:center">
        <div style="font-size:14px;font-weight:700">Score</div>
        <div style="display:flex;gap:12px;justify-content:center;margin-top:8px">
          <div><small>Player</small><div id="score-player" style="font-size:18px">0</div></div>
          <div><small>AI</small><div id="score-ai" style="font-size:18px">0</div></div>
        </div>
      </div>
      <div style="display:flex;gap:8px">
        <button id="btn-restart" class="secondary">Restart</button>
        <button id="btn-toggle" >Pause</button>
      </div>
    </div>
  </div>

  <div class="footer"><small>Made with &lt;canvas&gt; — lightweight demo</small></div>
</div>

<script>
/* -------------------------
   Simple 2D Boxing Game
   -------------------------
   Controls:
    - A / D : move left / right
    - J : jab (quick light punch)
    - K : heavy punch (slower, more damage)
    - L : dodge (fast short evade)
    - Space: block (reduce incoming damage)
    - P : pause
*/

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

const W = canvas.width, H = canvas.height;
let paused = false;

/* Utility */
function clamp(v,a,b){return Math.max(a,Math.min(b,v))}
function now(){ return performance.now(); }

/* Player & Enemy basic config */
const physics = {
  friction: 0.85,
  gravity: 0, // no vertical gravity (boxing on flat)
  arenaPadding: 120
};

class Fighter {
  constructor(opts){
    this.name = opts.name;
    this.x = opts.x;
    this.y = H - 140;
    this.w = 50; this.h = 120;
    this.vx = 0;
    this.facing = opts.facing; // 1 = right, -1 = left
    this.color = opts.color;
    this.maxHealth = 100;
    this.health = this.maxHealth;
    this.maxStam = 100;
    this.stamina = this.maxStam;
    this.state = 'idle'; // idle, move, jab, heavy, block, dodge, stunned
    this.stateTimer = 0;
    this.comboCooldown = 0;
    this.lastHit = 0;
    this.score = 0;
    this.canMove = true;
  }

  reset(x,facing){
    this.x = x; this.facing = facing; this.vx = 0;
    this.health = this.maxHealth; this.stamina = this.maxStam;
    this.state='idle'; this.stateTimer=0; this.lastHit=0;
  }

  getHitbox(){
    // simplified rectangular hitbox
    return {x: this.x - this.w/2, y:this.y - this.h, w:this.w, h:this.h};
  }

  getPunchBox(){
    // jab: small reach; heavy: larger reach and height
    if(this.state === 'jab'){
      return {x: this.x + this.facing*30, y:this.y - 80, w:40, h:30};
    } else if(this.state === 'heavy'){
      return {x: this.x + this.facing*45, y:this.y - 90, w:60, h:40};
    }
    return null;
  }

  damage(amount){
    this.health = clamp(this.health - amount, 0, this.maxHealth);
  }

  changeState(st, t=300){
    this.state = st; this.stateTimer = t;
    if(st==='jab') { this.stamina = clamp(this.stamina - 8, 0, this.maxStam); }
    if(st==='heavy'){ this.stamina = clamp(this.stamina - 20, 0, this.maxStam); }
    if(st==='dodge'){ this.stamina = clamp(this.stamina - 12, 0, this.maxStam); }
    if(st==='block'){ /* drains slowly elsewhere */ }
  }

  update(dt){
    // timers
    if(this.stateTimer>0) this.stateTimer = Math.max(0, this.stateTimer - dt);

    if(this.stateTimer === 0 && ['jab','heavy','dodge','stunned'].includes(this.state)){
      // revert to idle automatically
      this.state = 'idle';
    }

    // stamina regen when idle or moving
    if(!['jab','heavy','dodge'].includes(this.state)){
      this.stamina = clamp(this.stamina + dt*0.02, 0, this.maxStam);
    }

    // velocity
    this.x += this.vx * dt * 0.06;
    this.vx *= physics.friction;

    // clamp arena
    const leftBound = physics.arenaPadding;
    const rightBound = W - physics.arenaPadding;
    this.x = clamp(this.x, leftBound, rightBound);
  }

  draw(ctx){
    // shadow
    ctx.fillStyle = "rgba(0,0,0,0.25)";
    const hb = this.getHitbox();
    ctx.fillRect(hb.x+6, hb.y+this.h+4, hb.w-12, 8);

    // body
    ctx.fillStyle = this.color;
    ctx.fillRect(hb.x, hb.y, hb.w, hb.h);

    // head
    ctx.fillStyle = '#222';
    ctx.fillRect(hb.x + hb.w*0.15, hb.y - 28, hb.w*0.7, 28);

    // arms when attacking
    const pb = this.getPunchBox();
    if(pb){
      ctx.fillStyle = '#ffdbb5';
      ctx.fillRect(pb.x - pb.w/2, pb.y, pb.w, pb.h);
    }

    // simple face
    ctx.fillStyle = '#111';
    ctx.fillRect(hb.x + hb.w*0.35, hb.y - 22, 6, 6);
    ctx.fillRect(hb.x + hb.w*0.55, hb.y - 22, 6, 6);
  }
}

/* Game state */
const player = new Fighter({name:'You', x: W*0.3, facing: 1, color:'#2563eb'});
const enemy  = new Fighter({name:'AI',  x: W*0.7, facing: -1, color:'#ef4444'});
let round = 1;
let roundTime = 60; // seconds per round
let timeLeft = roundTime;
let roundActive = true;
let lastFrame = now();

/* Input handling */
const keys = {};
window.addEventListener('keydown', e => {
  if(e.key === 'p' || e.key === 'P'){ togglePause(); }
  keys[e.key.toLowerCase()] = true;
  // block with space
  if(e.code === 'Space') keys[' '] = true;
});
window.addEventListener('keyup', e => { keys[e.key.toLowerCase()] = false; if(e.code==='Space') keys[' '] = false; });

/* Buttons */
document.getElementById('btn-restart').onclick = () => { restartMatch(); };
document.getElementById('btn-toggle').onclick = () => { togglePause(); };

function togglePause(){
  paused = !paused;
  document.getElementById('btn-toggle').textContent = paused ? 'Resume' : 'Pause';
}

/* Simple WebAudio beep for hits */
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
function playBeep(freq=440, dur=0.06, gain=0.25){
  try{
    const o = audioCtx.createOscillator();
    const g = audioCtx.createGain();
    o.type = 'sine';
    o.frequency.value = freq;
    g.gain.value = gain;
    o.connect(g); g.connect(audioCtx.destination);
    o.start();
    o.stop(audioCtx.currentTime + dur);
  }catch(e){}
}

/* Hit detection */
function rectsOverlap(a,b){
  return !(a.x + a.w < b.x || b.x + b.w < a.x || a.y + a.h < b.y || b.y + b.h < a.y);
}

function tryResolvePunch(attacker, defender){
  const pb = attacker.getPunchBox();
  if(!pb) return false;
  const dhb = defender.getHitbox();
  const punchRect = {x: pb.x - pb.w/2, y: pb.y, w: pb.w, h: pb.h};
  const hit = rectsOverlap(punchRect, dhb);
  if(!hit) return false;

  // if defender blocking and facing attacker -> reduce damage
  let damage = (attacker.state === 'jab') ? 6 : 18;
  let stun = (attacker.state === 'jab') ? 150 : 420;
  if(defender.state === 'block'){
    damage *= 0.3;
    stun *= 0.5;
    // slight stamina drain for blocker
    defender.stamina = clamp(defender.stamina - 6, 0, defender.maxStam);
  } else if(defender.state === 'dodge'){
    // dodge chance avoids
    const dodgeSuccess = Math.random() < 0.75;
    if(dodgeSuccess){
      playBeep(900,0.03,0.08);
      return false;
    }
  }

  defender.damage(damage);
  defender.state = 'stunned';
  defender.stateTimer = stun;
  playBeep(300 + Math.random()*600, 0.08, 0.25);
  attacker.lastHit = now();

  return true;
}

/* AI (very simple) */
const ai = {
  thinkTimer: 0,
  decide(dt){
    this.thinkTimer -= dt;
    if(this.thinkTimer > 0) return;
    this.thinkTimer = 400 + Math.random()*600;

    // simple behavior based on relative position and stamina
    const dist = Math.abs(enemy.x - player.x);
    const facing = (enemy.x < player.x) ? 1 : -1;
    enemy.facing = facing;

    // if low stamina - block or retreat
    if(enemy.stamina < 20){
      if(Math.random() < 0.6) { enemy.changeState('block', 600); enemy.vx = -facing * 0.2; return; }
      enemy.changeState('dodge', 200);
      enemy.vx = -facing * 1.2;
      return;
    }

    if(dist > 160){
      // close gap
      enemy.vx = 0.9 * facing;
      enemy.changeState('idle',0);
    } else {
      // attack or block
      const r = Math.random();
      if(r < 0.45){
        enemy.changeState('jab', 180);
        enemy.vx = 0;
      } else if(r < 0.65 && enemy.stamina>30){
        enemy.changeState('heavy', 420);
        enemy.vx = 0;
      } else if(r < 0.82){
        enemy.changeState('block', 300);
      } else {
        enemy.changeState('dodge', 240);
        enemy.vx = -facing * 1.4;
      }
    }
  }
};

/* Game loop & logic */
function update(dt){
  if(paused) return;

  // round timer
  timeLeft -= dt/1000;
  if(timeLeft <= 0){
    // round ends, decide winner by health
    endRound();
    return;
  }

  // Player controls
  if(player.state !== 'stunned'){
    // move left/right
    if(keys['a']){ player.vx = -2.2; player.facing = -1; }
    if(keys['d']){ player.vx = 2.2; player.facing = 1; }
    if(!keys['a'] && !keys['d']){ /* no input */ }

    // block
    if(keys[' '] && player.stamina > 5){
      player.changeState('block');
    } else {
      if(player.state === 'block' && player.stateTimer === 0) player.state = 'idle';
    }

    // jab
    if(keys['j'] && player.stamina > 7 && player.state !== 'jab' && player.state !== 'heavy'){
      player.changeState('jab', 180);
      player.vx = 0;
    }

    // heavy
    if(keys['k'] && player.stamina > 18 && player.state !== 'heavy'){
      player.changeState('heavy', 420);
      player.vx = 0;
    }

    // dodge
    if(keys['l'] && player.stamina > 10){
      player.changeState('dodge', 240);
      player.vx = 6 * (player.facing * -1); // quick shift away
    }
  }

  // slight stamina drain when blocking
  if(player.state === 'block'){ player.stamina = clamp(player.stamina - dt*0.015, 0, player.maxStam); }
  if(enemy.state === 'block'){ enemy.stamina = clamp(enemy.stamina - dt*0.015, 0, enemy.maxStam); }

  // AI decision
  ai.decide(dt);

  // Update fighters
  player.update(dt); enemy.update(dt);

  // resolve punches: check attacker state transitions -> if just in attack state, attempt hit
  if(player.state === 'jab' && player.stateTimer > 0 && now() - player.lastHit > 150){
    if(tryResolvePunch(player, enemy)){ player.lastHit = now(); }
  }
  if(player.state === 'heavy' && player.stateTimer > 0 && now() - player.lastHit > 300){
    if(tryResolvePunch(player, enemy)){ player.lastHit = now(); }
  }

  if(enemy.state === 'jab' && enemy.stateTimer > 0 && now() - enemy.lastHit > 150){
    if(tryResolvePunch(enemy, player)){ enemy.lastHit = now(); }
  }
  if(enemy.state === 'heavy' && enemy.stateTimer > 0 && now() - enemy.lastHit > 300){
    if(tryResolvePunch(enemy, player)){ enemy.lastHit = now(); }
  }

  // check KO
  if(player.health <= 0 || enemy.health <= 0){
    endRound();
    return;
  }
}

function endRound(){
  paused = true;
  roundActive = false;
  document.getElementById('state').textContent = 'ROUND END';
  if(player.health > enemy.health){
    player.score += 1;
    document.getElementById('score-player').textContent = player.score;
    showMessage('You win the round!');
  } else if(enemy.health > player.health){
    enemy.score += 1;
    document.getElementById('score-ai').textContent = enemy.score;
    showMessage('AI wins the round!');
  } else {
    showMessage('Draw!');
  }
  setTimeout(()=> {
    // next round setup or match end
    if(round >= 3 || player.score === 2 || enemy.score === 2){
      showMessage('Match over! Restart to play again.');
      document.getElementById('state').textContent = 'MATCH OVER';
    } else {
      round += 1;
      nextRound();
    }
  }, 1200);
}

function nextRound(){
  roundActive = true;
  timeLeft = roundTime;
  player.reset(W*0.28, 1);
  enemy.reset(W*0.72, -1);
  paused = false;
  document.getElementById('round').textContent = round;
  document.getElementById('state').textContent = 'FIGHT';
}

/* Restart full match */
function restartMatch(){
  player.score = 0; enemy.score = 0;
  document.getElementById('score-player').textContent = '0';
  document.getElementById('score-ai').textContent = '0';
  round = 1;
  document.getElementById('round').textContent = '1';
  player.reset(W*0.3, 1); enemy.reset(W*0.7, -1);
  timeLeft = roundTime; roundActive = true; paused = false;
  document.getElementById('state').textContent = 'FIGHT';
}

/* Visual helpers */
function drawArena(){
  // ring floor
  ctx.fillStyle = '#07121a';
  ctx.fillRect(0, H-80, W, 80);

  // crowd-ish gradients
  const g = ctx.createLinearGradient(0,0,0,H-80);
  g.addColorStop(0, '#061018');
  g.addColorStop(1, '#051216');
  ctx.fillStyle = g;
  ctx.fillRect(0,0,W,H-80);

  // center line
  ctx.fillStyle = 'rgba(255,255,255,0.02)';
  ctx.fillRect(physics.arenaPadding-6, H-120, 4, 120);
  ctx.fillRect(W-physics.arenaPadding+2, H-120, 4, 120);
}

/* Message helper */
let overlayMsg = '';
let overlayT = 0;
function showMessage(txt){
  overlayMsg = txt; overlayT = 1800;
}

/* Render loop */
function render(dt){
  // clear
  ctx.clearRect(0,0,W,H);

  drawArena();

  // fighters
  player.draw(ctx);
  enemy.draw(ctx);

  // HUD on canvas: draw center timer big
  ctx.save();
  ctx.font = "700 42px Inter, Arial";
  ctx.fillStyle = "rgba(255,255,255,0.06)";
  ctx.textAlign = "center";
  ctx.fillText(Math.ceil(timeLeft), W/2, 60);
  ctx.restore();

  // overlay message
  if(overlayT > 0){
    overlayT = Math.max(0, overlayT - dt);
    ctx.save();
    ctx.globalAlpha = overlayT/1800;
    ctx.fillStyle = "#fff";
    ctx.font = "700 28px Inter, Arial";
    ctx.textAlign = "center";
    ctx.fillText(overlayMsg, W/2, H/2 - 30);
    ctx.restore();
  }
}

/* DOM HUD update loop */
function updateHUD(){
  document.getElementById('player-health').style.width = (player.health/player.maxHealth*100)+'%';
  document.getElementById('player-stam').style.width = (player.stamina/player.maxStam*100)+'%';
  document.getElementById('enemy-health').style.width = (enemy.health/enemy.maxHealth*100)+'%';
  document.getElementById('enemy-stam').style.width = (enemy.stamina/enemy.maxStam*100)+'%';
  document.getElementById('timer').textContent = Math.max(0, Math.ceil(timeLeft));
  // state label
  document.getElementById('state').textContent = (paused ? 'PAUSED' : (roundActive ? 'FIGHT' : 'ROUND END'));
}

/* Main loop */
function loop(){
  const t = now();
  const dt = t - lastFrame;
  lastFrame = t;
  if(!paused) update(dt);
  render(dt);
  updateHUD();
  requestAnimationFrame(loop);
}

/* Start */
restartMatch();
loop();
</script>
</body>
</html>

How to run

  1. Create a file called index.html.

  2. Paste the whole code above into it and save.

  3. Open it in Chrome, Firefox, or Edge. (If sounds don’t play, click the page to allow audio or interact once — browsers block autoplay.)

Ideas for improvements (I can implement any of these if you want)

Want me to add any of those — or tune difficulty, controls, or visuals? Which feature next: 2-player, better AI, or sprites + animations?

Reasons:
  • Long answer (-1):
  • Has code block (-0.5):
  • Ends in question mark (2):
  • Low reputation (1):
Posted by: Henok Teka