micropython开发esp32sWeb小游戏

所使用的开发板:

Web端效果

代码如下

import network
import usocket as socket
import uasyncio as asyncio
import ujson
import machine
import random
import gc
import sys
import struct

# 内存监控函数
def mem_info():
    gc.collect()
    print("Free mem:", gc.mem_free())

# 初始化时释放内存
mem_info()

# ================ AP热点设置 ================
def setup_ap():
    ap = network.WLAN(network.AP_IF)
    ap.active(True)
    ap.config(essid='SnakeGame', password='12345678', authmode=3)  # WPA2加密
    ap.config(max_clients=5)
    print('AP模式已启动')
    print('SSID: SnakeGame, 密码: 12345678')
    ip = ap.ifconfig()[0]
    print('IP地址:', ip)
    return ip

# ================ DNS服务器(用于劫持所有域名) ================
class DNSServer:
    def __init__(self, ip):
        self.ip = ip
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.sock.setblocking(False)
        self.sock.bind(('0.0.0.0', 53))
        print("DNS服务器已启动 (端口 53)")
    
    async def run(self):
        while True:
            try:
                data, addr = self.sock.recvfrom(512)
                if data:
                    # 解析DNS查询
                    transaction_id = data[0:2]
                    flags = b'\x81\x80'  # 标准响应标志
                    questions = 1
                    answers = 1
                    authority_rrs = 0
                    additional_rrs = 0
                    
                    # 构建响应头
                    response = transaction_id + flags
                    response += struct.pack('>HHHH', questions, answers, authority_rrs, additional_rrs)
                    
                    # 添加查询部分
                    response += data[12:]
                    
                    # 添加答案部分
                    response += b'\xc0\x0c'  # 指向查询名的指针
                    response += b'\x00\x01'  # 类型A
                    response += b'\x00\x01'  # 类IN
                    response += b'\x00\x00\x00\x3c'  # TTL 60秒
                    response += b'\x00\x04'  # 数据长度
                    response += socket.inet_aton(self.ip)  # 我们的IP地址
                    
                    self.sock.sendto(response, addr)
                    print(f"DNS劫持请求来自: {addr[0]}")
            except Exception as e:
                # print("DNS错误:", e)
                pass
            await asyncio.sleep_ms(100)

# ================ 游戏状态 ================
class GameState:
    def __init__(self):
        self.reset()
        self.clients = []
        self.high_score = 0
        self.base_speed = 300  # 基础速度(ms)
        self.speed_factor = 1.0  # 速度因子
        
    def reset(self):
        self.snake = [(5, 5), (4, 5), (3, 5)]  # 蛇初始位置
        self.direction = 'right'
        self.food = self.generate_food()
        self.score = 0
        self.game_over = False
        
    def generate_food(self):
        # 15x15网格
        while True:
            food = (random.randint(0, 14), random.randint(0, 14))
            if food not in self.snake:
                return food
                
    def move(self):
        if self.game_over:
            return

        head_x, head_y = self.snake[0]
        if self.direction == 'up':
            new_head = (head_x, head_y - 1)
        elif self.direction == 'down':
            new_head = (head_x, head_y + 1)
        elif self.direction == 'left':
            new_head = (head_x - 1, head_y)
        elif self.direction == 'right':
            new_head = (head_x + 1, head_y)

        # 检查碰撞
        if (new_head[0] < 0 or new_head[0] > 14 or 
            new_head[1] < 0 or new_head[1] > 14 or 
            new_head in self.snake):
            self.game_over = True
            if self.score > self.high_score:
                self.high_score = self.score
            return

        # 移动蛇
        self.snake.insert(0, new_head)
        
        # 检查食物
        if new_head == self.food:
            self.score += 1
            self.food = self.generate_food()
        else:
            self.snake.pop()
                
    def get_speed(self):
        """计算当前游戏速度,考虑基础速度和速度因子"""
        return max(100, int(self.base_speed / self.speed_factor))

# ================ Web服务器 ================
class WebServer:
    def __init__(self, game_state):
        self.game_state = game_state
        self.server = None
        self.html = self.get_html()
        
    def get_html(self):
        # 包含速度滑杆的HTML界面
        return """<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ESP32 Snake</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
            background-color: #f0f0f0;
        }
        .game-container {
            text-align: center;
            background-color: white;
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0 0 10px rgba(0,0,0,0.1);
        }
        canvas {
            border: 1px solid #000;
            background-color: #000;
        }
        .game-over {
            display: none;
            color: red;
            font-size: 24px;
            margin: 10px 0;
        }
        .settings {
            margin: 15px 0;
        }
        .speed-control {
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 10px;
        }
        button {
            padding: 10px 20px;
            font-size: 16px;
            cursor: pointer;
        }
    </style>
</head>
<body>
    <div class="game-container">
        <h1>ESP32 Snake</h1>
        <div class="score">得分: <span id="score">0</span> | 最高分: <span id="high-score">0</span></div>
        <canvas id="gameCanvas" width="300" height="300"></canvas>
        <div id="gameOver" class="game-over">游戏结束!</div>
        
        <div class="settings">
            <h3>游戏设置</h3>
            <div class="speed-control">
                <div class="speed-label">游戏速度:</div>
                <input type="range" min="1" max="3" value="1" step="0.5" class="speed-slider" id="speedSlider">
                <div class="speed-value" id="speedValue">1.0x</div>
            </div>
            <div class="instructions">(速度设置会在新游戏生效)</div>
        </div>
        
        <button id="restartBtn">开始新游戏</button>
    </div>

    <script>
        const canvas = document.getElementById('gameCanvas');
        const ctx = canvas.getContext('2d');
        const scoreElement = document.getElementById('score');
        const highScoreElement = document.getElementById('high-score');
        const gameOverElement = document.getElementById('gameOver');
        const restartBtn = document.getElementById('restartBtn');
        const speedSlider = document.getElementById('speedSlider');
        const speedValue = document.getElementById('speedValue');
        
        let ws;
        let gridSize = 15;
        let cellSize = canvas.width / gridSize;
        
        function connectWebSocket() {
            ws = new WebSocket('ws://' + window.location.host + '/ws');
            
            ws.onopen = () => {
                console.log('WebSocket连接已建立');
            };
            
            ws.onmessage = (event) => {
                const gameState = JSON.parse(event.data);
                drawGame(gameState);
            };
            
            ws.onclose = () => {
                console.log('WebSocket连接已关闭,尝试重新连接...');
                setTimeout(connectWebSocket, 2000);
            };
            
            ws.onerror = (error) => {
                console.error('WebSocket错误:', error);
            };
        }
        
        function drawGame(gameState) {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            
            // 绘制蛇
            ctx.fillStyle = '#00FF00';
            gameState.snake.forEach(segment => {
                ctx.fillRect(segment[0] * cellSize, segment[1] * cellSize, cellSize, cellSize);
            });
            
            // 绘制食物
            ctx.fillStyle = '#FF0000';
            ctx.fillRect(gameState.food[0] * cellSize, gameState.food[1] * cellSize, cellSize, cellSize);
            
            // 更新分数
            scoreElement.textContent = gameState.score;
            highScoreElement.textContent = gameState.high_score;
            
            // 显示游戏结束
            gameOverElement.style.display = gameState.game_over ? 'block' : 'none';
        }
        
        // 键盘控制
        window.addEventListener('keydown', (e) => {
            if (!ws) return;
            
            let direction;
            switch(e.key) {
                case 'ArrowUp': direction = 'up'; break;
                case 'ArrowDown': direction = 'down'; break;
                case 'ArrowLeft': direction = 'left'; break;
                case 'ArrowRight': direction = 'right'; break;
                default: return;
            }
            
            ws.send(JSON.stringify({ action: 'direction', data: direction }));
        });
        
        // 重新开始游戏
        restartBtn.addEventListener('click', () => {
            if (ws) {
                ws.send(JSON.stringify({ action: 'restart' }));
            }
        });
        
        // 速度设置
        speedSlider.addEventListener('input', () => {
            const value = parseFloat(speedSlider.value).toFixed(1);
            speedValue.textContent = value + 'x';
            
            if (ws) {
                ws.send(JSON.stringify({ action: 'set_speed', data: value }));
            }
        });
        
        // 初始连接
        connectWebSocket();
    </script>
</body>
</html>"""
    
    async def handle_client(self, reader, writer):
        try:
            request = await reader.read(512)
            request = request.decode('utf-8')
            
            if not request:
                return
            
            # 解析请求的第一行
            first_line = request.split('\r\n')[0]
            parts = first_line.split()
            if len(parts) < 2:
                return
                
            method = parts[0]
            path = parts[1]
            
            # 检查WebSocket升级请求
            if 'Upgrade: websocket' in request and path == '/ws':
                # 解析Sec-WebSocket-Key
                key_line = [line for line in request.split('\r\n') 
                            if line.startswith('Sec-WebSocket-Key:')][0]
                key = key_line.split(': ')[1].strip()
                
                # 计算响应key
                import ubinascii, uhashlib
                accept_key = ubinascii.b2a_base64(
                    uhashlib.sha1(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest()
                ).strip().decode()
                
                # 发送WebSocket握手响应
                response = (
                    "HTTP/1.1 101 Switching Protocols\r\n"
                    "Upgrade: websocket\r\n"
                    "Connection: Upgrade\r\n"
                    "Sec-WebSocket-Accept: " + accept_key + "\r\n\r\n"
                )
                await writer.awrite(response.encode())
                
                # 将客户端添加到列表
                self.game_state.clients.append(writer)
                print("新的WebSocket客户端连接")
                
                # 处理WebSocket消息
                while True:
                    try:
                        data = await reader.read(512)
                        if not data:
                            break
                            
                        # 解析WebSocket帧 (简化版)
                        opcode = data[0] & 0x0F
                        payload_len = data[1] & 0x7F
                        mask = data[2:6]
                        payload = data[6:6+payload_len]
                        
                        # 解掩码
                        decoded = bytearray()
                        for i in range(payload_len):
                            decoded.append(payload[i] ^ mask[i % 4])
                        
                        # 处理关闭帧
                        if opcode == 0x8:
                            break
                            
                        # 处理文本帧
                        if opcode == 0x1:
                            message = ujson.loads(decoded.decode())
                            if message['action'] == 'direction':
                                self.game_state.direction = message['data']
                            elif message['action'] == 'restart':
                                self.game_state.reset()
                            elif message['action'] == 'set_speed':
                                self.game_state.speed_factor = float(message['data'])
                    except Exception as e:
                        print("WebSocket错误:", e)
                        break
                
                # 客户端断开连接
                if writer in self.game_state.clients:
                    self.game_state.clients.remove(writer)
                print("WebSocket客户端断开")
                
            else:
                # 处理普通HTTP请求 - 重定向到游戏页面
                if path != '/':
                    # 如果是其他路径,重定向到首页
                    headers = "HTTP/1.1 302 Found\r\n"
                    headers += "Location: http://192.168.4.1/\r\n"
                    headers += "Connection: close\r\n\r\n"
                    await writer.awrite(headers.encode())
                else:
                    # 发送游戏页面
                    headers = "HTTP/1.1 200 OK\r\n"
                    headers += "Content-Type: text/html\r\n"
                    headers += "Connection: close\r\n\r\n"
                    await writer.awrite(headers.encode() + self.html.encode())
                
                await writer.drain()
                
        except Exception as e:
            print("处理客户端错误:", e)
        finally:
            writer.close()
            gc.collect()
    
    async def send_game_state(self, writer):
        try:
            # 准备游戏状态数据
            game_data = {
                'snake': self.game_state.snake,
                'food': self.game_state.food,
                'score': self.game_state.score,
                'high_score': self.game_state.high_score,
                'game_over': self.game_state.game_over
            }
            json_data = ujson.dumps(game_data)
            
            # 构建WebSocket帧
            frame = bytearray()
            frame.append(0x81)  # FIN + 文本帧
            if len(json_data) < 126:
                frame.append(len(json_data))
            else:
                frame.append(126)
                frame.extend(struct.pack('>H', len(json_data)))
            frame.extend(json_data.encode())
            
            # 发送数据
            await writer.awrite(frame)
        except Exception as e:
            print("发送游戏状态错误:", e)
            # 发生错误时移除客户端
            if writer in self.game_state.clients:
                self.game_state.clients.remove(writer)
    
    async def broadcast_game_state(self):
        if not self.game_state.clients:
            return
            
        # 准备游戏状态数据
        game_data = {
            'snake': self.game_state.snake,
            'food': self.game_state.food,
            'score': self.game_state.score,
            'high_score': self.game_state.high_score,
            'game_over': self.game_state.game_over
        }
        json_data = ujson.dumps(game_data)
        
        # 构建WebSocket帧
        frame = bytearray()
        frame.append(0x81)  # FIN + 文本帧
        if len(json_data) < 126:
            frame.append(len(json_data))
        else:
            frame.append(126)
            frame.extend(struct.pack('>H', len(json_data)))
        frame.extend(json_data.encode())
        
        # 向所有客户端广播
        for writer in self.game_state.clients[:]:  # 使用副本防止修改列表
            try:
                await writer.awrite(frame)
            except Exception as e:
                print("广播错误:", e)
                if writer in self.game_state.clients:
                    self.game_state.clients.remove(writer)
    
    async def run(self):
        # 修复:MicroPython的uasyncio没有serve_forever方法
        self.server = await asyncio.start_server(self.handle_client, "0.0.0.0", 80)
        print("Web服务器已启动 (端口 80)")
        
        # 创建一个任务来保持服务器运行
        while True:
            await asyncio.sleep(1)

# ================ 主循环 ================
async def main():
    try:
        # 设置AP
        ip = setup_ap()
        
        # 启动DNS服务器
        dns_server = DNSServer(ip)
        dns_task = asyncio.create_task(dns_server.run())
        
        # 初始化游戏状态
        game_state = GameState()
        
        # 启动Web服务器
        web_server = WebServer(game_state)
        server_task = asyncio.create_task(web_server.run())
        
        # 游戏主循环
        while True:
            # 更新游戏状态
            game_state.move()
            
            # 广播游戏状态
            if game_state.clients:
                await web_server.broadcast_game_state()
            
            # 控制游戏速度
            await asyncio.sleep_ms(game_state.get_speed())
            
            # 定期内存回收
            if game_state.get_speed() % 1000 == 0:
                mem_info()
                gc.collect()
                
    except Exception as e:
        sys.print_exception(e)
        print("主循环错误, 重启...")

# 启动程序
try:
    # 分配紧急异常缓冲区
    import micropython
    micropython.alloc_emergency_exception_buf(100)
    asyncio.run(main())
except Exception as e:
    sys.print_exception(e)
    print("致命错误, 重启设备...")

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值