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:
Single-player vs AI
Movement (left/right), dodge, jab, heavy punch, block
Health & stamina bars, round timer
Simple AI with aggression and defense
Smooth animations (frame-based), collision/hit detection
Pause, restart, and basic sound using WebAudio (no external files)
Save all of this as index.html
and open it in your browser.
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 <canvas> — 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>
Create a file called index.html
.
Paste the whole code above into it and save.
Open it in Chrome, Firefox, or Edge. (If sounds don’t play, click the page to allow audio or interact once — browsers block autoplay.)
Add 2-player local mode (split keyboard).
Add animated sprites (PNG sheets) and better hit frames.
Better AI (state machines, predicted blocks/dodges).
More moves: uppercuts, combos, clinch.
Sound effects and crowd noise (requires adding small audio files).
Mobile-friendly touch controls / on-screen buttons.
Save match history, round replays, or tournament mode.
Want me to add any of those — or tune difficulty, controls, or visuals? Which feature next: 2-player, better AI, or sprites + animations?