【新春创造营】新春经典桌游-五子棋

体验地址 : https://emetofo.qzz.io/


:horse_face: 马年五子棋

新春主题
技术栈
游戏类型
状态

:firecracker: 丙午马年 · 一码当先 · 码到成功 :firecracker:

一个精美的春节主题五子棋游戏,支持双人对战和人机对战,采用中国传统红金配色,充满节日氛围。

:man_technologist: 开发信息

本项目由 iFlow CLIglm-4.7 模型全程开发,从需求分析、代码实现到功能测试,全程由 AI 独立完成。


:open_book: 项目简介

马年五子棋是一个基于 Web 的五子棋游戏,采用纯前端技术实现,无需后端服务器。游戏界面设计精美,融合了中国传统文化元素(灯笼、鞭炮、马年主题等),为玩家提供沉浸式的游戏体验。

:sparkles: 核心功能

:video_game: 游戏模式

  • 双人对战模式:两名玩家轮流在同一设备上对弈
  • 人机对战模式:玩家与 AI 对弈,AI 具有三种难度级别

:robot: AI 难度级别

  • :seedling: 简单:随机落子,适合新手练习
  • :glowing_star: 中等:基于位置评估的策略性落子
  • :fire: 困难:使用 Minimax 算法 + Alpha-Beta 剪枝,具有较高博弈水平

:bullseye: 游戏操作

  • 落子:点击棋盘空白处即可落子
  • 悔棋:双人对战悔一步,人机对战悔两步
  • 重新开始:重置棋盘,保留胜负记录

:bar_chart: 胜负判定

  • 获胜条件:任意方向(横、竖、斜、反斜)连续五子连线
  • 获胜提示:显示获胜弹窗,绘制金光连线,计分板更新
  • 平局判定:棋盘填满且无玩家获胜时判定为平局

:artist_palette: 视觉效果

  • 动态灯笼:页面顶部两侧有摇曳的灯笼动画
  • 落子动画:棋子下落时有缩放动画效果
  • 获胜连线:获胜时显示金光闪闪的连线
  • 悬停效果:鼠标悬停在棋盘格子上有高亮提示
  • 马年图标:标题处有跳跃的马匹图标动画

:hammer_and_wrench: 技术栈

前端技术

  • HTML5:页面结构和语义化标签
  • CSS3
    • 使用 Tailwind CSS 进行快速样式开发
    • 自定义 CSS 动画(灯笼摇摆、棋子下落、马匹跳跃等)
    • 响应式布局(适配桌面端和移动端)
    • 渐变背景和阴影效果
  • JavaScript (ES6+)
    • 模块化代码结构
    • 游戏逻辑实现
    • AI 算法实现

开发工具

  • VS Code:代码编辑器
  • Chrome DevTools:调试工具
  • Playwright:自动化测试工具

:file_folder: 项目结构

horse-gomoku/
├── index.html          # 主页面文件
├── game.js             # 游戏逻辑脚本
└── README.md           # 项目说明文档

:rocket: 快速开始

方法一:直接打开

  1. 下载项目文件到本地
  2. 使用浏览器直接打开 index.html 文件即可运行

方法二:使用本地服务器(推荐)

# 使用 Python 3 启动简单的 HTTP 服务器
cd horse-gomoku
python3 -m http.server 8000

# 然后在浏览器中访问
# http://localhost:8000
# 使用 Node.js 的 http-server
cd horse-gomoku
npx http-server -p 8000

# 然后在浏览器中访问
# http://localhost:8000

:open_book: 实现细节

游戏核心逻辑

1. 数据结构

// 棋盘状态:15x15 二维数组
// 0 = 空, 1 = 黑棋, 2 = 白棋
board = Array(15).fill(null).map(() => Array(15).fill(0))

// 移动历史:记录每一步落子位置
moveHistory = [
    { row: 7, col: 7, player: 1 },
    { row: 7, col: 8, player: 2 },
    // ...
]

2. 落子逻辑

function makeMove(row, col) {
    // 1. 在棋盘对应位置放置棋子
    board[row][col] = currentPlayer;
    
    // 2. 记录移动历史
    moveHistory.push({ row, col, player: currentPlayer });
    
    // 3. 检查是否获胜
    if (checkWin(row, col)) {
        gameOver = true;
        showWinModal(currentPlayer);
        return;
    }
    
    // 4. 检查是否平局
    if (moveHistory.length === 225) {
        gameOver = true;
        showDrawModal();
        return;
    }
    
    // 5. 切换玩家
    currentPlayer = currentPlayer === BLACK ? WHITE : BLACK;
}

3. 获胜判定算法

function checkWin(row, col) {
    // 检查四个方向:水平、垂直、对角线、反对角线
    const directions = [
        [0, 1],   // 水平
        [1, 0],   // 垂直
        [1, 1],   // 对角线
        [1, -1]   // 反对角线
    ];
    
    for (const [dx, dy] of directions) {
        // 向两个方向延伸计数
        let count = 1; // 当前棋子算1个
        
        // 正方向计数
        for (let i = 1; i < 5; i++) {
            if (isValidPosition(row + dx*i, col + dy*i) && 
                board[row + dx*i][col + dy*i] === currentPlayer) {
                count++;
            } else {
                break;
            }
        }
        
        // 反方向计数
        for (let i = 1; i < 5; i++) {
            if (isValidPosition(row - dx*i, col - dy*i) && 
                board[row - dx*i][col - dy*i] === currentPlayer) {
                count++;
            } else {
                break;
            }
        }
        
        // 如果计数≥5,判定获胜
        if (count >= 5) {
            drawWinningLine(row, col, dx, dy);
            return true;
        }
    }
    return false;
}

AI 算法实现

1. 简单 AI(随机策略)

function getRandomMove() {
    // 获取所有空白位置
    const availableMoves = [];
    for (let row = 0; row < 15; row++) {
        for (let col = 0; col < 15; col++) {
            if (board[row][col] === EMPTY) {
                availableMoves.push({ row, col });
            }
        }
    }
    // 随机选择一个位置
    return availableMoves[Math.floor(Math.random() * availableMoves.length)];
}

2. 中等 AI(评估策略)

function getMediumMove() {
    // 1. 优先检查是否能直接获胜
    const winningMove = findWinningMove(WHITE);
    if (winningMove) return winningMove;
    
    // 2. 阻止对方获胜
    const blockingMove = findWinningMove(BLACK);
    if (blockingMove) return blockingMove;
    
    // 3. 评估中心区域,选择最优位置
    const centerMoves = [];
    for (let row = 5; row < 10; row++) {
        for (let col = 5; col < 10; col++) {
            if (board[row][col] === EMPTY && hasNeighbor(row, col)) {
                centerMoves.push({ 
                    row, col, 
                    score: evaluatePosition(row, col, WHITE) 
                });
            }
        }
    }
    
    if (centerMoves.length > 0) {
        centerMoves.sort((a, b) => b.score - a.score);
        return centerMoves[0];
    }
    
    return getRandomMove();
}

3. 困难 AI(Minimax 算法)

function getHardMove() {
    // 1. 优先检查是否能直接获胜
    const winningMove = findWinningMove(WHITE);
    if (winningMove) return winningMove;
    
    // 2. 阻止对方获胜
    const blockingMove = findWinningMove(BLACK);
    if (blockingMove) return blockingMove;
    
    // 3. 使用 Minimax 算法选择最优移动
    let bestScore = -Infinity;
    let bestMove = null;
    
    // 只考虑有邻居的候选位置(优化性能)
    const candidates = getCandidateMoves();
    
    for (const move of candidates) {
        board[move.row][move.col] = WHITE;  // 尝试落子
        const score = minimax(3, -Infinity, Infinity, false);
        board[move.row][move.col] = EMPTY;   // 撤销落子
        
        if (score > bestScore) {
            bestScore = score;
            bestMove = move;
        }
    }
    
    return bestMove || getRandomMove();
}

// Minimax 算法(带 Alpha-Beta 剪枝)
function minimax(depth, alpha, beta, isMaximizing) {
    if (depth === 0) {
        return evaluateBoard();
    }
    
    const candidates = getCandidateMoves();
    if (candidates.length === 0) return 0;
    
    if (isMaximizing) {
        // AI 回合,选择最大得分
        let maxScore = -Infinity;
        for (const move of candidates) {
            board[move.row][move.col] = WHITE;
            const score = minimax(depth - 1, alpha, beta, false);
            board[move.row][move.col] = EMPTY;
            maxScore = Math.max(maxScore, score);
            alpha = Math.max(alpha, score);
            if (beta <= alpha) break;  // 剪枝
        }
        return maxScore;
    } else {
        // 对手回合,选择最小得分
        let minScore = Infinity;
        for (const move of candidates) {
            board[move.row][move.col] = BLACK;
            const score = minimax(depth - 1, alpha, beta, true);
            board[move.row][move.col] = EMPTY;
            minScore = Math.min(minScore, score);
            beta = Math.min(beta, score);
            if (beta <= alpha) break;  // 剪枝
        }
        return minScore;
    }
}

4. 局面评估函数

function evaluatePosition(row, col, player) {
    let score = 0;
    const directions = [[0, 1], [1, 0], [1, 1], [1, -1]];
    
    for (const [dx, dy] of directions) {
        let count = 1;
        let openEnds = 0;
        
        // 正方向延伸
        for (let i = 1; i < 5; i++) {
            const newRow = row + dx * i;
            const newCol = col + dy * i;
            if (!isValidPosition(newRow, newCol)) break;
            
            if (board[newRow][newCol] === player) {
                count++;
            } else if (board[newRow][newCol] === EMPTY) {
                openEnds++;
                break;
            } else {
                break;
            }
        }
        
        // 反方向延伸
        for (let i = 1; i < 5; i++) {
            const newRow = row - dx * i;
            const newCol = col - dy * i;
            if (!isValidPosition(newRow, newCol)) break;
            
            if (board[newRow][newCol] === player) {
                count++;
            } else if (board[newRow][newCol] === EMPTY) {
                openEnds++;
                break;
            } else {
                break;
            }
        }
        
        // 根据连子数和开放端评分
        if (count >= 5) score += 100000;           // 五连
        else if (count === 4 && openEnds === 2) score += 10000;   // 活四
        else if (count === 4 && openEnds === 1) score += 1000;    // 冲四
        else if (count === 3 && openEnds === 2) score += 500;     // 活三
        else if (count === 3 && openEnds === 1) score += 100;     // 冲三
        else if (count === 2 && openEnds === 2) score += 50;      // 活二
        else if (count === 2 && openEnds === 1) score += 10;      // 冲二
    }
    
    return score;
}

界面设计

1. 配色方案

  • 背景:红金渐变(中国红 → 火红 → 金黄)
  • 棋盘:木纹纹理(棕色渐变)
  • 装饰:金色边框和文字
  • 棋子:黑色和白色(经典配色)

2. 动画效果

/* 灯笼摇摆动画 */
@keyframes swing {
    0%, 100% { transform: rotate(-5deg); }
    50% { transform: rotate(5deg); }
}

/* 棋子下落动画 */
@keyframes dropPiece {
    0% { transform: scale(0); opacity: 0; }
    50% { transform: scale(1.2); }
    100% { transform: scale(1); opacity: 1; }
}

/* 马匹跳跃动画 */
@keyframes gallop {
    0%, 100% { transform: translateY(0); }
    50% { transform: translateY(-10px); }
}

/* 获胜连线闪光动画 */
@keyframes shine {
    0%, 100% { opacity: 0.8; }
    50% { opacity: 1; }
}

3. 响应式布局

  • 桌面端(≥1024px):左右布局,棋盘和控制面板并排显示
  • 平板端(768px-1023px):垂直布局,控制面板在棋盘下方
  • 移动端(<768px):紧凑布局,棋盘格子缩小为 32px

:test_tube: 测试

项目使用 Playwright 进行自动化测试,覆盖以下测试场景:

  • :white_check_mark: 游戏加载和显示
  • :white_check_mark: 棋盘渲染(15×15网格)
  • :white_check_mark: 玩家落子功能
  • :white_check_mark: 游戏模式切换
  • :white_check_mark: AI功能
  • :white_check_mark: 难度切换
  • :white_check_mark: 悔棋功能
  • :white_check_mark: 重新开始
  • :white_check_mark: 获胜检测
  • :white_check_mark: 获胜弹窗
  • :white_check_mark: 计分板更新
  • :white_check_mark: 响应式布局

:mobile_phone: 浏览器兼容性

  • :white_check_mark: Chrome/Edge (推荐)
  • :white_check_mark: Firefox
  • :white_check_mark: Safari
  • :white_check_mark: Opera
  • :white_check_mark: 移动端浏览器

:video_game: 游戏规则

  1. 黑棋先行,双方轮流落子
  2. 落子后不能移动或移动位置
  3. 任意方向(横、竖、斜、反斜)连续五子连线即获胜
  4. 棋盘填满且无玩家获胜时判定为平局
  5. 人机模式下,玩家执黑棋先行

:horse_face: 祝您马年大吉 · 骏码奔腾 · 人强码壮 :horse_face:

我这边似乎加载不出页面(挂了:ladder:) 蹲下大家的反馈吧

我这边是可以访问的哦,我同事也可以访问

换了个地方部署,试试这个 https://emetofo.qzz.io/

OK OK 被我们:spider_web:拦截了 看私信哦