Python关于老游复刻与创新2

这是双人乒乓球模拟游戏HTML代码,支持两位玩家在同一设备上使用键盘操控球拍对战。

```html

<!DOCTYPE html>

<html lang="zh-CN">

<head>

<meta charset="UTF-8">

<meta name="viewport" content="width=device-width, initial-scale=1.0">

<title>🏓 双人乒乓球</title>

<style>

* {

margin: 0;

padding: 0;

box-sizing: border-box;

}

body {

background: #0f0f1a;

display: flex;

justify-content: center;

align-items: center;

min-height: 100vh;

font-family: 'Segoe UI', 'PingFang SC', Roboto, 'Helvetica Neue', sans-serif;

user-select: none;

overflow: hidden;

}

.game-wrapper {

background: linear-gradient(145deg, #1a1a2e, #16213e);

padding: 30px 40px 35px;

border-radius: 28px;

box-shadow: 0 25px 50px rgba(0, 0, 0, 0.8), inset 0 0 0 1px rgba(255, 255, 255, 0.06);

display: flex;

flex-direction: column;

align-items: center;

}

.header {

display: flex;

justify-content: space-between;

align-items: center;

width: 100%;

margin-bottom: 18px;

padding: 0 8px;

}

.score-container {

display: flex;

align-items: center;

gap: 28px;

font-size: 28px;

font-weight: 700;

letter-spacing: 1px;

}

.score-p1 {

color: #4fc3f7;

text-shadow: 0 0 20px rgba(79, 195, 247, 0.3);

}

.score-p2 {

color: #ff6b6b;

text-shadow: 0 0 20px rgba(255, 107, 107, 0.3);

}

.score-divider {

color: rgba(255, 255, 255, 0.15);

font-weight: 300;

}

.controls-hint {

display: flex;

gap: 30px;

font-size: 13px;

color: rgba(255, 255, 255, 0.35);

letter-spacing: 0.5px;

}

.controls-hint span {

display: flex;

align-items: center;

gap: 8px;

}

.hint-key {

display: inline-block;

background: rgba(255, 255, 255, 0.08);

padding: 2px 10px;

border-radius: 4px;

font-size: 12px;

font-weight: 600;

color: rgba(255, 255, 255, 0.5);

border: 1px solid rgba(255, 255, 255, 0.06);

}

.hint-p1 .hint-key {

color: #4fc3f7;

border-color: rgba(79, 195, 247, 0.2);

}

.hint-p2 .hint-key {

color: #ff6b6b;

border-color: rgba(255, 107, 107, 0.2);

}

canvas {

display: block;

width: 800px;

height: 500px;

border-radius: 16px;

background: #0d0d1f;

box-shadow: inset 0 0 60px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.04);

cursor: none;

touch-action: none;

}

.footer {

display: flex;

justify-content: space-between;

align-items: center;

width: 100%;

margin-top: 16px;

padding: 0 8px;

}

.status-text {

color: rgba(255, 255, 255, 0.4);

font-size: 14px;

font-weight: 500;

letter-spacing: 0.3px;

min-height: 22px;

}

.status-text .highlight {

color: rgba(255, 255, 255, 0.7);

}

.btn-restart {

background: rgba(255, 255, 255, 0.06);

border: 1px solid rgba(255, 255, 255, 0.08);

color: rgba(255, 255, 255, 0.4);

padding: 6px 20px;

border-radius: 30px;

font-size: 13px;

font-weight: 500;

cursor: pointer;

transition: all 0.2s ease;

letter-spacing: 0.3px;

}

.btn-restart:hover {

background: rgba(255, 255, 255, 0.12);

color: rgba(255, 255, 255, 0.7);

border-color: rgba(255, 255, 255, 0.15);

}

/* 响应式 */

@media (max-width: 900px) {

.game-wrapper {

padding: 16px 18px 20px;

border-radius: 18px;

}

canvas {

width: 100%;

height: auto;

aspect-ratio: 800 / 500;

border-radius: 12px;

}

.header {

flex-direction: column;

gap: 10px;

margin-bottom: 12px;

}

.score-container {

font-size: 22px;

gap: 18px;

}

.controls-hint {

font-size: 11px;

gap: 16px;

flex-wrap: wrap;

justify-content: center;

}

.footer {

flex-direction: column;

gap: 8px;

margin-top: 12px;

}

.status-text {

font-size: 12px;

text-align: center;

}

}

@media (max-width: 500px) {

.game-wrapper {

padding: 10px 10px 14px;

border-radius: 12px;

}

.score-container {

font-size: 18px;

gap: 12px;

}

.controls-hint {

font-size: 10px;

gap: 10px;

}

.hint-key {

padding: 1px 6px;

font-size: 10px;

}

}

</style>

</head>

<body>

<div class="game-wrapper">

<!-- 头部:比分 + 操作提示 -->

<div class="header">

<div class="score-container">

<span class="score-p1" id="scoreP1">0</span>

<span class="score-divider">⚡</span>

<span class="score-p2" id="scoreP2">0</span>

</div>

<div class="controls-hint">

<span class="hint-p1">

<span class="hint-key">W</span> ↑

<span class="hint-key">S</span> ↓

<span style="color:rgba(255,255,255,0.2);margin-left:2px;">玩家1</span>

</span>

<span class="hint-p2">

<span class="hint-key">↑</span> ↑

<span class="hint-key">↓</span> ↓

<span style="color:rgba(255,255,255,0.2);margin-left:2px;">玩家2</span>

</span>

</div>

</div>

<!-- 游戏画布 -->

<canvas id="gameCanvas" width="800" height="500"></canvas>

<!-- 底部:状态 + 重启按钮 -->

<div class="footer">

<div class="status-text" id="statusText">按 <span class="highlight">空格键</span> 开始</div>

<button class="btn-restart" id="restartBtn">⟳ 重新开始</button>

</div>

</div>

<script>

// ============================================================

// 🏓 双人乒乓球 - 完整游戏逻辑

// 控制:玩家1 (左) W/S | 玩家2 (右) ↑/↓

// 目标:先得 11 分获胜

// ============================================================

// -------- DOM 引用 --------

const canvas = document.getElementById('gameCanvas');

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

const scoreP1El = document.getElementById('scoreP1');

const scoreP2El = document.getElementById('scoreP2');

const statusTextEl = document.getElementById('statusText');

const restartBtn = document.getElementById('restartBtn');

// -------- 固定参数 --------

const W = 800,

H = 500;

const PADDLE_W = 12;

const PADDLE_H = 80;

const BALL_RADIUS = 8;

const PADDLE_SPEED = 6;

const BALL_SPEED_INIT = 5.2;

const BALL_SPEED_MAX = 13;

const WIN_SCORE = 11;

const PADDLE_OFFSET = 30; // 球拍离边界的距离

// -------- 游戏状态 --------

const STATE = {

WAITING: 'waiting', // 等待开始

PLAYING: 'playing', // 进行中

SCORED: 'scored', // 得分后暂停

GAMEOVER: 'gameover' // 游戏结束

};

// -------- 游戏对象 --------

const player1 = {

x: PADDLE_OFFSET,

y: H / 2 - PADDLE_H / 2,

w: PADDLE_W,

h: PADDLE_H,

score: 0,

color: '#4fc3f7',

glowColor: 'rgba(79, 195, 247, 0.25)',

upKey: 'w',

downKey: 's',

name: '玩家1'

};

const player2 = {

x: W - PADDLE_OFFSET - PADDLE_W,

y: H / 2 - PADDLE_H / 2,

w: PADDLE_W,

h: PADDLE_H,

score: 0,

color: '#ff6b6b',

glowColor: 'rgba(255, 107, 107, 0.25)',

upKey: 'arrowup',

downKey: 'arrowdown',

name: '玩家2'

};

// 球

const ball = {

x: W / 2,

y: H / 2,

r: BALL_RADIUS,

vx: 0,

vy: 0,

speed: BALL_SPEED_INIT,

trail: \[\] // 轨迹

};

// 按键状态

const keys = {};

// 游戏控制

let gameState = STATE.WAITING;

let scoredTimer = 0;

let scoredMessage = '';

let scoredColor = '';

// -------- 辅助函数 --------

function resetBall(direction) {

ball.x = W / 2;

ball.y = H / 2 + (Math.random() - 0.5) * 80;

ball.speed = BALL_SPEED_INIT;

// direction: 1 朝右 (玩家2方向), -1 朝左 (玩家1方向)

const angle = (Math.random() - 0.5) * 0.8; // -40° ~ 40°

const dir = direction || (Math.random() > 0.5 ? 1 : -1);

ball.vx = Math.cos(angle) * ball.speed * dir;

ball.vy = Math.sin(angle) * ball.speed;

ball.trail = \[\];

}

function resetGame() {

player1.score = 0;

player2.score = 0;

player1.y = H / 2 - PADDLE_H / 2;

player2.y = H / 2 - PADDLE_H / 2;

updateScoreDisplay();

resetBall(1);

gameState = STATE.WAITING;

statusTextEl.innerHTML = '按 <span class="highlight">空格键</span> 开始';

scoredTimer = 0;

}

function updateScoreDisplay() {

scoreP1El.textContent = player1.score;

scoreP2El.textContent = player2.score;

}

function showScoredMessage(winner) {

if (winner === 1) {

scoredMessage = '🔥 玩家1 得分!';

scoredColor = player1.color;

} else {

scoredMessage = '🔥 玩家2 得分!';

scoredColor = player2.color;

}

statusTextEl.innerHTML = `<span style="color:{scoredColor}"\>{scoredMessage}</span>`;

}

function showGameOver(winner) {

if (winner === 1) {

scoredMessage = '🏆 玩家1 获胜!';

scoredColor = player1.color;

} else {

scoredMessage = '🏆 玩家2 获胜!';

scoredColor = player2.color;

}

statusTextEl.innerHTML = `<span style="color:{scoredColor};font-size:18px;font-weight:700;"\>{scoredMessage}</span> 按 空格键 重新开始`;

}

// -------- 物理 & 碰撞 --------

function clamp(v, min, max) {

return Math.max(min, Math.min(max, v));

}

function updatePaddles() {

// 玩家1

if (keysplayer1.upKey) {

player1.y -= PADDLE_SPEED;

}

if (keysplayer1.downKey) {

player1.y += PADDLE_SPEED;

}

player1.y = clamp(player1.y, 0, H - player1.h);

// 玩家2

if (keysplayer2.upKey) {

player2.y -= PADDLE_SPEED;

}

if (keysplayer2.downKey) {

player2.y += PADDLE_SPEED;

}

player2.y = clamp(player2.y, 0, H - player2.h);

}

function updateBall() {

if (gameState !== STATE.PLAYING) return;

// 移动球

ball.x += ball.vx;

ball.y += ball.vy;

// ---- 上下墙壁碰撞 ----

if (ball.y - ball.r < 0) {

ball.y = ball.r;

ball.vy = -ball.vy;

} else if (ball.y + ball.r > H) {

ball.y = H - ball.r;

ball.vy = -ball.vy;

}

// ---- 球拍碰撞 (玩家1) ----

const p1 = player1;

if (ball.vx < 0 &&

ball.x - ball.r < p1.x + p1.w &&

ball.x + ball.r > p1.x &&

ball.y > p1.y && ball.y < p1.y + p1.h) {

// 计算反弹角度

const relY = (ball.y - (p1.y + p1.h / 2)) / (p1.h / 2); // -1 ~ 1

const angle = relY * 0.75; // 最大 ±43°

const speed = Math.min(ball.speed + 0.3, BALL_SPEED_MAX);

ball.speed = speed;

ball.vx = Math.cos(angle) * speed;

ball.vy = Math.sin(angle) * speed;

ball.x = p1.x + p1.w + ball.r;

// 加一点轨迹特效

addTrail();

}

// ---- 球拍碰撞 (玩家2) ----

const p2 = player2;

if (ball.vx > 0 &&

ball.x + ball.r > p2.x &&

ball.x - ball.r < p2.x + p2.w &&

ball.y > p2.y && ball.y < p2.y + p2.h) {

const relY = (ball.y - (p2.y + p2.h / 2)) / (p2.h / 2);

const angle = relY * 0.75;

const speed = Math.min(ball.speed + 0.3, BALL_SPEED_MAX);

ball.speed = speed;

ball.vx = -Math.cos(angle) * speed;

ball.vy = Math.sin(angle) * speed;

ball.x = p2.x - ball.r;

addTrail();

}

// ---- 左右边界 (得分) ----

if (ball.x - ball.r < -20) {

// 玩家2得分

player2.score++;

updateScoreDisplay();

if (player2.score >= WIN_SCORE) {

gameState = STATE.GAMEOVER;

showGameOver(2);

} else {

gameState = STATE.SCORED;

scoredTimer = 60; // 约1秒 (60fps)

showScoredMessage(2);

resetBall(-1);

}

return;

}

if (ball.x + ball.r > W + 20) {

// 玩家1得分

player1.score++;

updateScoreDisplay();

if (player1.score >= WIN_SCORE) {

gameState = STATE.GAMEOVER;

showGameOver(1);

} else {

gameState = STATE.SCORED;

scoredTimer = 60;

showScoredMessage(1);

resetBall(1);

}

return;

}

// ---- 轨迹更新 ----

if (gameState === STATE.PLAYING) {

ball.trail.push({ x: ball.x, y: ball.y });

if (ball.trail.length > 14) ball.trail.shift();

}

}

function addTrail() {

for (let i = 0; i < 6; i++) {

ball.trail.push({

x: ball.x + (Math.random() - 0.5) * 6,

y: ball.y + (Math.random() - 0.5) * 6

});

}

if (ball.trail.length > 20) ball.trail.splice(0, ball.trail.length - 20);

}

// -------- 绘制函数 --------

function drawBackground() {

// 深色背景

const grad = ctx.createRadialGradient(400, 250, 100, 400, 250, 500);

grad.addColorStop(0, '#14142e');

grad.addColorStop(1, '#0a0a18');

ctx.fillStyle = grad;

ctx.fillRect(0, 0, W, H);

// 球场网格微光

ctx.strokeStyle = 'rgba(255,255,255,0.02)';

ctx.lineWidth = 1;

for (let i = 0; i < W; i += 40) {

ctx.beginPath();

ctx.moveTo(i, 0);

ctx.lineTo(i, H);

ctx.stroke();

}

for (let i = 0; i < H; i += 40) {

ctx.beginPath();

ctx.moveTo(0, i);

ctx.lineTo(W, i);

ctx.stroke();

}

// 中线 (虚线)

ctx.setLineDash(10, 14);

ctx.strokeStyle = 'rgba(255,255,255,0.06)';

ctx.lineWidth = 2;

ctx.beginPath();

ctx.moveTo(W / 2, 0);

ctx.lineTo(W / 2, H);

ctx.stroke();

ctx.setLineDash(\[\]);

// 中心圆

ctx.strokeStyle = 'rgba(255,255,255,0.04)';

ctx.lineWidth = 1.5;

ctx.beginPath();

ctx.arc(W / 2, H / 2, 70, 0, Math.PI * 2);

ctx.stroke();

// 中心点

ctx.fillStyle = 'rgba(255,255,255,0.05)';

ctx.beginPath();

ctx.arc(W / 2, H / 2, 4, 0, Math.PI * 2);

ctx.fill();

}

function drawPaddle(p) {

// 发光

const grad = ctx.createRadialGradient(

p.x + p.w / 2, p.y + p.h / 2, 2,

p.x + p.w / 2, p.y + p.h / 2, p.h * 0.7

);

grad.addColorStop(0, p.glowColor);

grad.addColorStop(1, 'transparent');

ctx.fillStyle = grad;

ctx.fillRect(p.x - p.h * 0.3, p.y - p.h * 0.2, p.w + p.h * 0.6, p.h * 1.4);

// 主体

const r = 4;

ctx.shadowColor = p.color;

ctx.shadowBlur = 20;

ctx.fillStyle = p.color;

ctx.beginPath();

ctx.roundRect(p.x, p.y, p.w, p.h, r);

ctx.fill();

ctx.shadowBlur = 0;

// 高光

const hlGrad = ctx.createLinearGradient(p.x, p.y, p.x + p.w, p.y);

hlGrad.addColorStop(0, 'rgba(255,255,255,0.25)');

hlGrad.addColorStop(0.4, 'rgba(255,255,255,0.05)');

hlGrad.addColorStop(1, 'rgba(255,255,255,0)');

ctx.fillStyle = hlGrad;

ctx.beginPath();

ctx.roundRect(p.x + 2, p.y + 4, p.w - 4, p.h - 8, 2);

ctx.fill();

// 边框光晕

ctx.strokeStyle = 'rgba(255,255,255,0.08)';

ctx.lineWidth = 1;

ctx.beginPath();

ctx.roundRect(p.x, p.y, p.w, p.h, r);

ctx.stroke();

// 中间装饰线

ctx.fillStyle = 'rgba(255,255,255,0.06)';

ctx.fillRect(p.x + 3, p.y + 10, 2, p.h - 20);

}

function drawBall() {

// 轨迹

for (let i = 0; i < ball.trail.length; i++) {

const alpha = (i / ball.trail.length) * 0.4;

const radius = ball.r * (0.3 + 0.7 * (i / ball.trail.length));

ctx.fillStyle = `rgba(255,255,255,${alpha})`;

ctx.beginPath();

ctx.arc(ball.traili.x, ball.traili.y, radius, 0, Math.PI * 2);

ctx.fill();

}

// 球体发光

const glow = ctx.createRadialGradient(

ball.x - 2, ball.y - 2, 1,

ball.x, ball.y, ball.r * 3

);

glow.addColorStop(0, 'rgba(255,255,255,0.3)');

glow.addColorStop(0.5, 'rgba(255,255,255,0.08)');

glow.addColorStop(1, 'transparent');

ctx.fillStyle = glow;

ctx.beginPath();

ctx.arc(ball.x, ball.y, ball.r * 3, 0, Math.PI * 2);

ctx.fill();

// 球体

const grad = ctx.createRadialGradient(

ball.x - 3, ball.y - 4, 2,

ball.x, ball.y, ball.r

);

grad.addColorStop(0, '#ffffff');

grad.addColorStop(0.7, '#f0f0f0');

grad.addColorStop(1, '#c8c8c8');

ctx.shadowColor = 'rgba(255,255,255,0.5)';

ctx.shadowBlur = 25;

ctx.fillStyle = grad;

ctx.begin

相关推荐
ly768922 分钟前
前端测试完整指南:从单元测试、组件测试到端到端测试与 CI/CD
前端·ci/cd·单元测试
青 春 记 忆9 小时前
Dify Docker Compose 通用无损升级指南:从备份、双版本预演到切换与回滚
运维·人工智能·python·docker·容器
GlueNa2SiO39 小时前
03-Flask模板引擎Jinja2详解
笔记·python·flask
钛态9 小时前
Vite 中的 CSS 工程化:从 CSS Modules 到 UnoCSS 的渐进式迁移
前端·vue·react·web
Csvn11 小时前
原型链、this 与闭包内存:三座深水区一次打通(含内存泄漏排查实战)
前端·javascript
临沂GEO11 小时前
GEO搜索优化科普|正规地理位置流量运营入门指南
大数据·人工智能·python·流量运营
葡萄城技术团队11 小时前
如何通过 JSON 数据源 / Web API 快速构建轻量级报表(Report)?
前端·json·原型模式
大模型码小白11 小时前
Spring AI Tool 实现自然语言操作 MySQL 数据库详解
服务器·开发语言·数据库·人工智能·python·mysql·spring