已开启
20261401340 环节二蜡笔小新绘图完成,包含ipynb与png结果图;环节三网页马里奥游戏 #33
20261401340 环节二蜡笔小新绘图完成,包含ipynb与png结果图;环节三网页马里奥游戏 #33
已开启
zhouch创建于 9月19日
共 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>空格 / ↑ 跳跃 &nbsp;·&nbsp; ← → 移动</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+}