已开启
20261401340 环节二蜡笔小新绘图完成,包含ipynb与png结果图;环节三网页马里奥游戏 #33
zhouch创建于 9月19日
20261401340 环节二蜡笔小新绘图完成,包含ipynb与png结果图;环节三网页马里奥游戏 #33
已开启
共 3 个文件变更+261-0
| @@ -0,0 +1,36 @@ | |||
| 1 | +# 环节三:网页版马里奥 | ||
| 2 | + | ||
| 3 | +一个零依赖的单文件网页小游戏:蓝天背景、棕色地面,红色方块代表马里奥,按空格跳跃、吃金币。 | ||
| 4 | + | ||
| 5 | +## 启动流程 | ||
| 6 | + | ||
| 7 | +不需要安装任何依赖,也不需要构建: | ||
| 8 | + | ||
| 9 | +1. 直接用浏览器打开 `index.html`(双击即可); | ||
| 10 | +2. 或在当前目录起一个静态服务: | ||
| 11 | + ```bash | ||
| 12 | + python3 -m http.server 8000 | ||
| 13 | + ``` | ||
| 14 | + 然后浏览器访问 <http://localhost:8000>。 | ||
| 15 | + | ||
| 16 | +## 玩法 | ||
| 17 | + | ||
| 18 | +| 操作 | 按键 | | ||
| 19 | +| --- | --- | | ||
| 20 | +| 跳跃 | `空格` 或 `↑` | | ||
| 21 | +| 左右移动 | `←` `→` | | ||
| 22 | + | ||
| 23 | +- 站在地面或棕色砖块平台上时才能起跳; | ||
| 24 | +- 碰到金币即得分,右上角实时显示 `金币: x / 5`; | ||
| 25 | +- 平台只有从上方落下时才能站住,从下方穿过不会碰撞。 | ||
| 26 | + | ||
| 27 | +## 技术说明 | ||
| 28 | + | ||
| 29 | +- 单文件 HTML,所有 HTML / CSS / JS 都写在 `index.html` 内,无外部文件、无第三方库; | ||
| 30 | +- 使用 `<canvas>` 绘制,`requestAnimationFrame` 驱动主循环; | ||
| 31 | +- 物理:每帧施加重力(`GRAVITY = 0.7`),跳跃初速度 `JUMP = -13`; | ||
| 32 | +- 碰撞:AABB 矩形判定(`hit()`)处理地面与平台,圆-矩形最近点判定处理金币拾取。 | ||
| 33 | + | ||
| 34 | +## 素材来源 | ||
| 35 | + | ||
| 36 | +未使用任何图片、音效或第三方模板,画面元素全部由 Canvas API 代码绘制。 | ||
| @@ -0,0 +1,165 @@ | |||
| 1 | +<!DOCTYPE html> | ||
| 2 | +<html lang="zh-CN"> | ||
| 3 | +<head> | ||
| 4 | +<meta charset="UTF-8"> | ||
| 5 | +<meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| 6 | +<title>简易马里奥</title> | ||
| 7 | +<style> | ||
| 8 | + html, body { height: 100%; margin: 0; } | ||
| 9 | + body { | ||
| 10 | + display: flex; flex-direction: column; | ||
| 11 | + align-items: center; justify-content: center; | ||
| 12 | + background: #1a1a1a; font-family: sans-serif; user-select: none; | ||
| 13 | + } | ||
| 14 | + canvas { | ||
| 15 | + background: #5c94fc; | ||
| 16 | + border-radius: 6px; | ||
| 17 | + box-shadow: 0 8px 30px rgba(0, 0, 0, .6); | ||
| 18 | + } | ||
| 19 | + p { color: #aaa; font-size: 14px; margin: 12px 0 0; } | ||
| 20 | +</style> | ||
| 21 | +</head> | ||
| 22 | +<body> | ||
| 23 | +<canvas id="game" width="800" height="400"></canvas> | ||
| 24 | +<p>空格 / ↑ 跳跃 · ← → 移动</p> | ||
| 25 | + | ||
| 26 | +<script> | ||
| 27 | +const ctx = document.getElementById('game').getContext('2d'); | ||
| 28 | +const W = 800, H = 400; | ||
| 29 | +const GROUND_Y = 340; // 地面顶部 y 坐标 | ||
| 30 | +const GRAVITY = 0.7, JUMP = -13, SPEED = 4; | ||
| 31 | + | ||
| 32 | +// 马里奥:红色方块 | ||
| 33 | +const player = { x: 60, y: GROUND_Y - 40, w: 30, h: 40, vx: 0, vy: 0, onGround: true }; | ||
| 34 | + | ||
| 35 | +// 棕色砖块平台 | ||
| 36 | +const platforms = [ | ||
| 37 | + { x: 220, y: 260, w: 130, h: 16 }, | ||
| 38 | + { x: 440, y: 195, w: 130, h: 16 }, | ||
| 39 | + { x: 660, y: 265, w: 110, h: 16 } | ||
| 40 | +]; | ||
| 41 | + | ||
| 42 | +// 金币(悬在平台上方) | ||
| 43 | +const coins = [ | ||
| 44 | + { x: 265, y: 225 }, { x: 305, y: 225 }, | ||
| 45 | + { x: 485, y: 160 }, { x: 525, y: 160 }, | ||
| 46 | + { x: 700, y: 230 } | ||
| 47 | +].map(c => ({ ...c, r: 9, got: false })); | ||
| 48 | + | ||
| 49 | +const keys = {}; | ||
| 50 | +let score = 0; | ||
| 51 | + | ||
| 52 | +addEventListener('keydown', e => { | ||
| 53 | + if ([' ', 'ArrowUp', 'ArrowLeft', 'ArrowRight'].includes(e.key)) e.preventDefault(); | ||
| 54 | + keys[e.key] = true; | ||
| 55 | +}); | ||
| 56 | +addEventListener('keyup', e => { keys[e.key] = false; }); | ||
| 57 | + | ||
| 58 | +const hit = (a, b) => | ||
| 59 | + a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y; | ||
| 60 | + | ||
| 61 | +function update() { | ||
| 62 | + // 左右移动 | ||
| 63 | + player.vx = (keys.ArrowRight ? SPEED : 0) - (keys.ArrowLeft ? SPEED : 0); | ||
| 64 | + player.x = Math.max(0, Math.min(W - player.w, player.x + player.vx)); | ||
| 65 | + | ||
| 66 | + // 重力 | ||
| 67 | + player.vy += GRAVITY; | ||
| 68 | + player.y += player.vy; | ||
| 69 | + player.onGround = false; | ||
| 70 | + | ||
| 71 | + // 地面碰撞 | ||
| 72 | + if (player.y + player.h >= GROUND_Y) { | ||
| 73 | + player.y = GROUND_Y - player.h; | ||
| 74 | + player.vy = 0; | ||
| 75 | + player.onGround = true; | ||
| 76 | + } | ||
| 77 | + | ||
| 78 | + // 平台碰撞:只有从上方落下时才能站住 | ||
| 79 | + for (const p of platforms) { | ||
| 80 | + if (hit(player, p) && player.vy >= 0 && player.y + player.h - player.vy <= p.y + 2) { | ||
| 81 | + player.y = p.y - player.h; | ||
| 82 | + player.vy = 0; | ||
| 83 | + player.onGround = true; | ||
| 84 | + } | ||
| 85 | + } | ||
| 86 | + | ||
| 87 | + // 空格跳跃:站在地面或平台上时才可起跳 | ||
| 88 | + if (player.onGround && (keys[' '] || keys.ArrowUp)) { | ||
| 89 | + player.vy = JUMP; | ||
| 90 | + player.onGround = false; | ||
| 91 | + } | ||
| 92 | + | ||
| 93 | + // 吃金币 | ||
| 94 | + for (const c of coins) { | ||
| 95 | + if (c.got) continue; | ||
| 96 | + const nx = Math.max(player.x, Math.min(c.x, player.x + player.w)); | ||
| 97 | + const ny = Math.max(player.y, Math.min(c.y, player.y + player.h)); | ||
| 98 | + if ((c.x - nx) ** 2 + (c.y - ny) ** 2 < c.r ** 2) { | ||
| 99 | + c.got = true; | ||
| 100 | + score++; | ||
| 101 | + } | ||
| 102 | + } | ||
| 103 | +} | ||
| 104 | + | ||
| 105 | +function draw() { | ||
| 106 | + // 蓝天 | ||
| 107 | + ctx.fillStyle = '#5c94fc'; | ||
| 108 | + ctx.fillRect(0, 0, W, H); | ||
| 109 | + | ||
| 110 | + // 白云 | ||
| 111 | + ctx.fillStyle = '#ffffff'; | ||
| 112 | + [[120, 70], [380, 50], [640, 90]].forEach(([x, y]) => { | ||
| 113 | + ctx.beginPath(); | ||
| 114 | + ctx.arc(x, y, 22, 0, Math.PI * 2); | ||
| 115 | + ctx.arc(x + 26, y + 6, 16, 0, Math.PI * 2); | ||
| 116 | + ctx.arc(x - 26, y + 6, 16, 0, Math.PI * 2); | ||
| 117 | + ctx.fill(); | ||
| 118 | + }); | ||
| 119 | + | ||
| 120 | + // 棕色地面 | ||
| 121 | + ctx.fillStyle = '#8b4513'; | ||
| 122 | + ctx.fillRect(0, GROUND_Y, W, H - GROUND_Y); | ||
| 123 | + ctx.fillStyle = '#a0522d'; | ||
| 124 | + ctx.fillRect(0, GROUND_Y, W, 8); | ||
| 125 | + | ||
| 126 | + // 砖块平台 | ||
| 127 | + for (const p of platforms) { | ||
| 128 | + ctx.fillStyle = '#8b4513'; | ||
| 129 | + ctx.fillRect(p.x, p.y, p.w, p.h); | ||
| 130 | + ctx.fillStyle = '#a0522d'; | ||
| 131 | + ctx.fillRect(p.x, p.y, p.w, 4); | ||
| 132 | + } | ||
| 133 | + | ||
| 134 | + // 金币 | ||
| 135 | + for (const c of coins) { | ||
| 136 | + if (c.got) continue; | ||
| 137 | + ctx.beginPath(); | ||
| 138 | + ctx.arc(c.x, c.y, c.r, 0, Math.PI * 2); | ||
| 139 | + ctx.fillStyle = '#ffd700'; | ||
| 140 | + ctx.fill(); | ||
| 141 | + ctx.strokeStyle = '#c99700'; | ||
| 142 | + ctx.stroke(); | ||
| 143 | + } | ||
| 144 | + | ||
| 145 | + // 马里奥 | ||
| 146 | + ctx.fillStyle = '#e52521'; | ||
| 147 | + ctx.fillRect(player.x, player.y, player.w, player.h); | ||
| 148 | + ctx.fillStyle = '#000000'; | ||
| 149 | + ctx.fillRect(player.x + 6, player.y + 9, 6, 6); | ||
| 150 | + ctx.fillRect(player.x + 18, player.y + 9, 6, 6); | ||
| 151 | + | ||
| 152 | + // 分数 | ||
| 153 | + ctx.fillStyle = '#ffffff'; | ||
| 154 | + ctx.font = 'bold 20px sans-serif'; | ||
| 155 | + ctx.fillText('金币: ' + score + ' / ' + coins.length, 16, 32); | ||
| 156 | +} | ||
| 157 | + | ||
| 158 | +(function loop() { | ||
| 159 | + update(); | ||
| 160 | + draw(); | ||
| 161 | + requestAnimationFrame(loop); | ||
| 162 | +})(); | ||
| 163 | +</script> | ||
| 164 | +</body> | ||
| 165 | +</html> | ||
| @@ -0,0 +1,60 @@ | |||
| 1 | +{ | ||
| 2 | + "cells": [ | ||
| 3 | + { | ||
| 4 | + "cell_type": "code", | ||
| 5 | + "execution_count": null, | ||
| 6 | + "metadata": {}, | ||
| 7 | + "outputs": [], | ||
| 8 | + "source": [ | ||
| 9 | + "%matplotlib inline\n", | ||
| 10 | + "import numpy as np\n", | ||
| 11 | + "import matplotlib.pyplot as plt\n", | ||
| 12 | + "\n", | ||
| 13 | + "# 蜡笔小新绘图代码\n", | ||
| 14 | + "fig, ax = plt.subplots(figsize=(6,6))\n", | ||
| 15 | + "ax.set_xlim(-10,10)\n", | ||
| 16 | + "ax.set_ylim(-10,10)\n", | ||
| 17 | + "ax.axis('off')\n", | ||
| 18 | + "\n", | ||
| 19 | + "# 脸轮廓\n", | ||
| 20 | + "face = plt.matplotlib.patches.Circle((0,0), 7, color='#ffddaa', ec='black',lw=2)\n", | ||
| 21 | + "ax.add_patch(face)\n", | ||
| 22 | + "# 眼睛\n", | ||
| 23 | + "ax.add_patch(plt.matplotlib.patches.Circle((-2.5,2),1,color='white',ec='black',lw=2))\n", | ||
| 24 | + "ax.add_patch(plt.matplotlib.patches.Circle((2.5,2),1,color='white',ec='black',lw=2))\n", | ||
| 25 | + "ax.add_patch(plt.matplotlib.patches.Circle((-2.5,2),0.4,color='black'))\n", | ||
| 26 | + "ax.add_patch(plt.matplotlib.patches.Circle((2.5,2),0.4,color='black'))\n", | ||
| 27 | + "# 眉毛\n", | ||
| 28 | + "ax.plot([-4,-1.5],[4,4.2],color='black',lw=3)\n", | ||
| 29 | + "ax.plot([1.5,4],[4.2,4],color='black',lw=3)\n", | ||
| 30 | + "# 嘴巴\n", | ||
| 31 | + "mouth = plt.matplotlib.patches.Arc((0,-2), 6,4, angle=0, theta1=20, theta2=160, color='black',lw=3)\n", | ||
| 32 | + "ax.add_patch(mouth)\n", | ||
| 33 | + "\n", | ||
| 34 | + "plt.savefig('蜡笔小新结果图.png',dpi=150,bbox_inches='tight')\n", | ||
| 35 | + "plt.show()" | ||
| 36 | + ] | ||
| 37 | + } | ||
| 38 | + ], | ||
| 39 | + "metadata": { | ||
| 40 | + "kernelspec": { | ||
| 41 | + "display_name": "Python 3", | ||
| 42 | + "language": "python", | ||
| 43 | + "name": "python3" | ||
| 44 | + }, | ||
| 45 | + "language_info": { | ||
| 46 | + "codemirror_mode": { | ||
| 47 | + "name": "ipython", | ||
| 48 | + "version": 3 | ||
| 49 | + }, | ||
| 50 | + "file_extension": ".py", | ||
| 51 | + "mimetype": "text/x-python", | ||
| 52 | + "name": "python", | ||
| 53 | + "nbconvert_exporter": "python", | ||
| 54 | + "pygments_lexer": "ipython3", | ||
| 55 | + "version": "3.10" | ||
| 56 | + } | ||
| 57 | + }, | ||
| 58 | + "nbformat": 4, | ||
| 59 | + "nbformat_minor": 5 | ||
| 60 | +} | ||