☰
Android Studio开发五子棋:从自定义View到AI对手的完整实践
2026/10/21 8:16:54 网站建设 项目流程

简介:这是一份面向Android初学者与进阶开发者的五子棋实战项目资源,聚焦Android Studio环境下的UI布局、事件响应与游戏逻辑实现,帮助学习者系统掌握移动端交互式游戏开发全流程。压缩包共49个文件,含17个XML布局与配置文件(定义界面结构与资源引用)、15个PNG/JPG图像资源(棋子、按钮等UI元素)、4个Kotlin源码文件(核心的MainActivity、ChessBoard、GameRule等逻辑类),以及Gradle构建脚本、Git版本配置等工程必需文件,整体仅877KB,轻量易导入。已有2304人学习下载,资源结构清晰,完整呈现标准Android模块化组织方式——从app/src主代码目录、gradle构建体系到.idea开发配置一应俱全,附带可直接运行的棋盘渲染、回合切换、五连珠胜负判定及触控落子逻辑,是理解Android生命周期、自定义View与状态管理的优质练手案例。

1. 项目缘起:为什么用Android Studio做五子棋?

几年前我刚接触Android开发时,总想找个能贯穿UI、逻辑、交互的完整项目来练手。那些“Hello World”式的教程总觉得差点意思,而一个稍微复杂点的电商或社交App,对新手来说又过于庞大,容易在复杂的业务逻辑里迷失。后来我发现,像五子棋、扫雷、贪吃蛇这类经典小游戏,简直是绝佳的练手项目。它们规则明确,功能边界清晰,但麻雀虽小五脏俱全——你需要设计界面、处理用户交互、编写核心游戏逻辑、甚至还能引入AI对手。这不,最近又有朋友问起Android开发入门项目,我第一个想到的就是五子棋。今天,我就把自己从零开始,在Android Studio里实现一个带简单AI的五子棋应用的全过程,包括那些新手最容易踩的坑,掰开揉碎了分享给你。无论你是想巩固Java/Kotlin基础,还是想理解Android的基础组件(如View、Canvas绘制、触摸事件),这个项目都能给你带来实实在在的收获。

2. 环境搭建与项目初始化:避开那些“坑爹”的配置

万事开头难,对于Android开发,这个“难”往往就卡在环境配置上。网上的教程五花八门,但很多都过时了,或者忽略了关键细节,导致你跟着做却跑不起来。这里,我结合最新的Android Studio版本(以当前稳定的版本为例),给你梳理一条最稳妥的路径。

2.1 Android Studio安装与“第一道坎”

首先,去官网下载Android Studio。这里第一个坑就来了:安装路径千万不要有中文或空格。很多新手喜欢把软件装在“D:\编程软件\”这类目录下,这可能导致后续Gradle构建时出现各种诡异错误。我的建议是,路径简单点,比如D:\Android\AndroidStudio。

安装过程中,会提示你安装Android Virtual Device (AVD) 组件,也就是安卓模拟器。这里务必勾选。但即使勾选了,安装完成后首次运行,你可能还是会遇到“Android Virtual Device Unavailable”的提示。这通常是因为你的电脑没有开启CPU虚拟化技术(Intel的VT-x或AMD的SVM)。你需要重启电脑,进入BIOS(开机按F2、Del等键,因电脑品牌而异),找到“Virtualization Technology”或类似选项,将其设置为“Enabled”。

对于使用Windows且不带Hyper-V功能的电脑(比如一些家庭版系统),运行模拟器可能会更麻烦。这种情况下,我强烈建议你直接使用真机调试。用USB线连接手机,打开手机的“开发者选项”和“USB调试”,在Android Studio中运行项目时选择你的真机设备,比折腾模拟器快得多,也稳定得多。

2.2 创建新项目与Gradle“慢”的问题

打开Android Studio,选择“New Project”。这里我们选择“Empty Views Activity”,语言选择Java(为了照顾更广泛的读者,本文以Java为例,Kotlin的实现逻辑类似)。给项目起个名字,比如“Gomoku”,包名按自己喜欢来,保存位置再次确保无中文和空格。

点击“Finish”后,你会进入漫长的等待——Gradle正在下载项目依赖。这是第二个大坑:默认的仓库地址在国外,下载速度极慢,甚至可能失败。解决方法就是更换国内镜像源。

找到项目根目录下的build.gradle文件(注意是Project级别的那个,不是Module级别的)。在buildscript和allprojects的repositories块里,将google()和mavenCentral()前面加上阿里云的镜像地址。修改后大致如下:

// 项目根目录 build.gradle buildscript { repositories { maven { url 'https://maven.aliyun.com/repository/google' } maven { url 'https://maven.aliyun.com/repository/public' } maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } // 原来的 google() 和 mavenCentral() 可以注释掉或保留,但阿里的源要在前面 // google() // mavenCentral() } ... } allprojects { repositories { maven { url 'https://maven.aliyun.com/repository/google' } maven { url 'https://maven.aliyun.com/repository/public' } maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } // google() // mavenCentral() } }

修改后,点击Android Studio右上角的“Sync Now”同步项目。速度应该会有质的提升。如果还遇到某个特定Gradle版本下载慢(比如gradle-6.5-bin.zip),你可以去Gradle官网手动下载对应版本,然后将其放入C:\Users\你的用户名\.gradle\wrapper\dists\目录下对应的文件夹内,再重新同步。

2.3 项目结构初窥与基础设置

项目同步成功后,我们简单看一下目录结构。app/src/main/java/下是我们的Java代码目录,app/src/main/res/下是资源文件目录,包括布局(layout)、图片(drawable)、常量(values)等。

为了让开发过程更舒适,我建议做两个设置:

  1. 代码字体:在File -> Settings -> Editor -> Font中,调整一个适合编程的等宽字体和大小。
  2. Logcat字体:调试时看日志是家常便饭,默认的Logcat字体太小。在Logcat窗口右键,选择“Use Soft Wraps”并调整字体大小。

做完这些,我们的开发环境就算准备好了。虽然步骤看起来有点多,但这些都是“一劳永逸”的投入,配置好之后,后续的开发会顺畅很多。

3. 游戏界面绘制:自定义View与Canvas的运用

五子棋的棋盘和棋子,显然不能用普通的按钮(Button)或文本框(TextView)来堆砌。我们需要一个能自由绘制的画布,这就是Android中的自定义View。我们将创建一个继承自View的类,在它的onDraw方法里,用Canvas(画布)和Paint(画笔)画出一切。

3.1 创建棋盘View类

首先,在java目录下你的包名里,新建一个Java类,命名为ChessBoardView。

package com.yourpackage.gomoku; // 替换成你的包名 import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import androidx.annotation.Nullable; public class ChessBoardView extends View { // 棋盘参数 private int boardRows = 15; // 15路棋盘,标准五子棋 private float cellWidth; // 每个格子的宽度 private float offset; // 棋盘边距 private Paint gridPaint; // 画网格的画笔 private Paint blackPiecePaint; // 画黑棋的画笔 private Paint whitePiecePaint; // 画白棋的画笔 // 棋盘数据,0空,1黑,2白 private int[][] boardMatrix = new int[boardRows][boardRows]; // 构造方法 public ChessBoardView(Context context) { this(context, null); } public ChessBoardView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } private void init() { // 初始化网格画笔 gridPaint = new Paint(Paint.ANTI_ALIAS_FLAG); gridPaint.setColor(0xFF8B4513); // 深棕色 gridPaint.setStrokeWidth(3); // 初始化黑棋画笔(实心圆) blackPiecePaint = new Paint(Paint.ANTI_ALIAS_FLAG); blackPiecePaint.setColor(0xFF000000); // 黑色 blackPiecePaint.setStyle(Paint.Style.FILL); // 初始化白棋画笔(实心圆) whitePiecePaint = new Paint(Paint.ANTI_ALIAS_FLAG); whitePiecePaint.setColor(0xFFFFFFFF); // 白色 whitePiecePaint.setStyle(Paint.Style.FILL); // 为白棋加一个黑色的边框,更清晰 whitePiecePaint.setStrokeWidth(2); // 注意:Fill样式下Stroke不生效,我们需要另一个画笔来画边框,这里先简化 } }

这里有几个关键点:

  • boardRows=15:标准五子棋是15x15的棋盘。
  • boardMatrix:一个二维数组,用来记录棋盘上每个点的状态。这是游戏逻辑的核心数据源。
  • Paint.ANTI_ALIAS_FLAG:抗锯齿标志,让画出来的线条和图形边缘更平滑。
  • 构造方法:自定义View通常需要两个构造方法,以支持在XML布局中引用。

3.2 计算尺寸与绘制网格

View的大小不是固定的,我们需要在onSizeChanged方法中,根据View的实际宽高,计算出每个格子的宽度(cellWidth)和边距(offset)。

@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // 取宽高中的较小值作为棋盘的有效区域,保证是正方形 int boardSize = Math.min(w, h); offset = 50; // 留出50像素的边距,为了美观 cellWidth = (boardSize - 2 * offset) / (boardRows - 1); // 格子数比交叉点数少1 } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); drawGrid(canvas); drawPieces(canvas); } // 绘制棋盘网格 private void drawGrid(Canvas canvas) { // 画横线 for (int i = 0; i < boardRows; i++) { float startX = offset; float startY = offset + i * cellWidth; float stopX = offset + (boardRows - 1) * cellWidth; float stopY = startY; canvas.drawLine(startX, startY, stopX, stopY, gridPaint); } // 画竖线 for (int i = 0; i < boardRows; i++) { float startX = offset + i * cellWidth; float startY = offset; float stopX = startX; float stopY = offset + (boardRows - 1) * cellWidth; canvas.drawLine(startX, startY, stopX, stopY, gridPaint); } } // 绘制棋子(根据boardMatrix数据) private void drawPieces(Canvas canvas) { for (int i = 0; i < boardRows; i++) { for (int j = 0; j < boardRows; j++) { if (boardMatrix[i][j] != 0) { float centerX = offset + j * cellWidth; float centerY = offset + i * cellWidth; float radius = cellWidth / 2 * 0.8f; // 棋子半径是格子宽度的0.8倍,留点空隙 if (boardMatrix[i][j] == 1) { canvas.drawCircle(centerX, centerY, radius, blackPiecePaint); } else if (boardMatrix[i][j] == 2) { // 画白棋,先画一个白色实心圆 canvas.drawCircle(centerX, centerY, radius, whitePiecePaint); // 再画一个黑色的圆环作为边框(简化版,可以用另一个画笔) Paint borderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); borderPaint.setColor(0xFF000000); borderPaint.setStyle(Paint.Style.STROKE); borderPaint.setStrokeWidth(2); canvas.drawCircle(centerX, centerY, radius, borderPaint); } } } } }

onSizeChanged是关键,它确保了无论我们的View被布局成什么尺寸,棋盘都能正确居中并缩放。drawGrid方法就是简单的画线循环。drawPieces方法遍历boardMatrix数组,在非空的位置画出对应的棋子。这里给白棋加边框是为了在白色背景上能看清。

3.3 将棋盘View加入布局

打开res/layout/activity_main.xml,将默认的TextView替换成我们的自定义View。

<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <com.yourpackage.gomoku.ChessBoardView android:id="@+id/chessBoard" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOfParent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>

注意com.yourpackage.gomoku要替换成你实际的包名。现在运行项目,你应该能看到一个棕色的15x15棋盘绘制在屏幕中央了。

4. 落子逻辑与触摸交互:从点击到棋盘数据更新

光有棋盘还不够,我们需要让用户能下棋。这需要处理触摸事件(onTouchEvent),将触摸点的坐标转换成棋盘的网格坐标,然后更新boardMatrix数据并重绘。

4.1 处理触摸事件

在ChessBoardView类中添加一个变量记录当前该谁下棋,并重写onTouchEvent方法。

public class ChessBoardView extends View { // ... 之前的变量 ... private boolean isBlackTurn = true; // 当前回合,true为黑方,false为白方 private GameCallback gameCallback; // 回调接口,用于通知Activity游戏状态变化 // 定义回调接口 public interface GameCallback { void onPiecePlaced(int player, int row, int col); void onGameOver(int winner); // winner: 0-继续,1-黑胜,2-白胜 } public void setGameCallback(GameCallback callback) { this.gameCallback = callback; } @Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { // 1. 获取触摸点坐标 float touchX = event.getX(); float touchY = event.getY(); // 2. 将坐标转换为棋盘网格索引 int col = (int) ((touchX - offset + cellWidth / 2) / cellWidth); int row = (int) ((touchY - offset + cellWidth / 2) / cellWidth); // 3. 检查点击是否在棋盘范围内,并且该位置为空 if (row >= 0 && row < boardRows && col >= 0 && col < boardRows && boardMatrix[row][col] == 0) { // 4. 更新棋盘数据 boardMatrix[row][col] = isBlackTurn ? 1 : 2; // 5. 通知外部(如Activity)棋子已落下 if (gameCallback != null) { gameCallback.onPiecePlaced(isBlackTurn ? 1 : 2, row, col); } // 6. 检查游戏是否结束 if (checkWin(row, col)) { if (gameCallback != null) { gameCallback.onGameOver(isBlackTurn ? 1 : 2); } // 游戏结束,可以禁用触摸或显示结果 return true; } // 7. 切换回合 isBlackTurn = !isBlackTurn; // 8. 重绘View invalidate(); return true; } } return super.onTouchEvent(event); } }

坐标转换的公式(touchX - offset + cellWidth / 2) / cellWidth是关键。touchX - offset得到相对于棋盘左上角的坐标,加上cellWidth/2是为了实现“点击格子附近即落子在该交叉点”的效果,然后除以格子宽度得到索引。强制转换为int即完成了从连续坐标到离散网格索引的映射。

4.2 胜负判定算法实现

checkWin(int row, int col)是五子棋的核心逻辑。我们需要检查刚落子的位置(row, col),在横、竖、左斜、右斜四个方向上,是否存在连续五个同色棋子。

private boolean checkWin(int row, int col) { int currentPlayer = boardMatrix[row][col]; if (currentPlayer == 0) return false; // 定义八个方向向量:横(1,0)、竖(0,1)、左斜(1,1)、右斜(1,-1) // 每个方向检查两次(正向和反向) int[][] directions = {{1, 0}, {0, 1}, {1, 1}, {1, -1}}; for (int[] dir : directions) { int count = 1; // 当前位置已经有一颗棋子 // 正向检查 for (int step = 1; step < 5; step++) { int newRow = row + step * dir[0]; int newCol = col + step * dir[1]; if (newRow >= 0 && newRow < boardRows && newCol >= 0 && newCol < boardRows && boardMatrix[newRow][newCol] == currentPlayer) { count++; } else { break; } } // 反向检查 for (int step = 1; step < 5; step++) { int newRow = row - step * dir[0]; int newCol = col - step * dir[1]; if (newRow >= 0 && newRow < boardRows && newCol >= 0 && newCol < boardRows && boardMatrix[newRow][newCol] == currentPlayer) { count++; } else { break; } } // 如果某个方向连续棋子数达到5,则获胜 if (count >= 5) { return true; } } return false; }

这个算法效率很高,只检查了落子点周围有限的范围。它的思路是:以落子点为中心,向每个方向的两边延伸计数,只要有一个方向上的连续同色棋子数(包括中心点本身)达到5,就判定胜利。注意边界检查newRow >= 0 && newRow < boardRows是必不可少的,防止数组越界。

4.3 在Activity中连接回调

现在,我们需要在MainActivity中实例化ChessBoardView并设置回调,以响应游戏状态的变化,比如更新当前回合提示、显示胜负对话框等。

public class MainActivity extends AppCompatActivity implements ChessBoardView.GameCallback { private ChessBoardView chessBoardView; private TextView tvStatus; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); chessBoardView = findViewById(R.id.chessBoard); tvStatus = findViewById(R.id.tv_status); // 需要在布局中添加一个TextView来显示状态 chessBoardView.setGameCallback(this); updateStatusText(); } private void updateStatusText() { // 这里可以获取chessBoardView的当前回合状态,简单示例 tvStatus.setText(chessBoardView.isBlackTurn() ? "黑方回合" : "白方回合"); } @Override public void onPiecePlaced(int player, int row, int col) { // 棋子落下后的处理,比如播放音效、记录棋谱等 runOnUiThread(this::updateStatusText); } @Override public void onGameOver(int winner) { runOnUiThread(() -> { String message = winner == 1 ? "黑方获胜!" : "白方获胜!"; new AlertDialog.Builder(MainActivity.this) .setTitle("游戏结束") .setMessage(message) .setPositiveButton("再来一局", (dialog, which) -> resetGame()) .setNegativeButton("退出", null) .show(); }); } private void resetGame() { chessBoardView.resetGame(); // 需要在ChessBoardView中实现重置方法 updateStatusText(); } }

在ChessBoardView中需要添加resetGame()方法和一个获取当前回合的公共方法isBlackTurn()。

// 在 ChessBoardView 类中添加 public void resetGame() { // 清空棋盘数据 for (int i = 0; i < boardRows; i++) { for (int j = 0; j < boardRows; j++) { boardMatrix[i][j] = 0; } } isBlackTurn = true; invalidate(); // 重绘空棋盘 } public boolean isBlackTurn() { return isBlackTurn; }

至此,一个双人对战的五子棋应用就完成了核心功能。运行后,两个玩家可以轮流在屏幕上点击落子,当有一方连成五子时,游戏会弹出提示。

5. 实现简单AI对手:从随机到基础策略

让人机对战,是让这个小游戏变得更有趣的关键。我们来实现一个简单的AI。最基础的AI是随机落子,但体验太差。我们稍微升级一下,实现一个“攻防兼备”的初级AI。

5.1 评估函数与棋型定义

AI下棋的核心是评估每个空位的“价值”,然后选择价值最高的点落子。我们定义一个简单的评估函数,为每种棋型打分。棋型是指一条线上连续的几个位置构成的模式。

public class GomokuAI { private int[][] board; private int boardSize; private int aiPlayer; // AI的棋子颜色,2(白) private int humanPlayer; // 玩家的棋子颜色,1(黑) // 棋型评分表(简化版) private static final int SCORE_FIVE = 100000; // 连五 private static final int SCORE_FOUR = 10000; // 活四 private static final int SCORE_SFOUR = 1000; // 冲四(单边被堵) private static final int SCORE_THREE = 100; // 活三 private static final int SCORE_STHREE = 10; // 眠三 private static final int SCORE_TWO = 1; // 活二 public GomokuAI(int[][] board, int aiPlayer) { this.board = board; this.boardSize = board.length; this.aiPlayer = aiPlayer; this.humanPlayer = (aiPlayer == 1) ? 2 : 1; } // 获取AI的最佳落子位置 public int[] getBestMove() { int bestScore = Integer.MIN_VALUE; int bestRow = -1, bestCol = -1; // 遍历所有空位 for (int i = 0; i < boardSize; i++) { for (int j = 0; j < boardSize; j++) { if (board[i][j] == 0) { // 模拟AI在此落子 board[i][j] = aiPlayer; int score = evaluatePosition(i, j, aiPlayer); // 再模拟玩家在此落子(防守视角) board[i][j] = humanPlayer; score += evaluatePosition(i, j, humanPlayer) * 0.8; // 防守权重略低于进攻 board[i][j] = 0; // 恢复空位 if (score > bestScore) { bestScore = score; bestRow = i; bestCol = j; } } } } // 如果没找到(比如第一步),返回棋盘中心附近 if (bestRow == -1) { bestRow = boardSize / 2; bestCol = boardSize / 2; } return new int[]{bestRow, bestCol}; } // 评估某个位置对指定玩家的价值 private int evaluatePosition(int row, int col, int player) { int score = 0; int[][] dirs = {{1,0},{0,1},{1,1},{1,-1}}; for (int[] dir : dirs) { score += evaluateDirection(row, col, dir[0], dir[1], player); } return score; } // 评估一个方向上的棋型 private int evaluateDirection(int row, int col, int dx, int dy, int player) { // 这个函数需要分析以(row,col)为中心,在(dx,dy)方向上的连续棋子情况 // 由于实现稍长,这里简述逻辑:分别向两个方向扫描,统计连续的同色棋子数、空位数、被堵情况。 // 根据扫描结果判断是“活四”、“冲四”、“活三”等棋型,并返回对应分数。 // 这是一个简化实现,实际完整的棋型判断代码有几十行。 // 为了项目可运行,我们可以先实现一个超简版:只统计连续棋子数。 int count = 1; // 中心点 // 正向 for (int i = 1; i < 5; i++) { int r = row + dx * i; int c = col + dy * i; if (r<0 || r>=boardSize || c<0 || c>=boardSize || board[r][c] != player) break; count++; } // 反向 for (int i = 1; i < 5; i++) { int r = row - dx * i; int c = col - dy * i; if (r<0 || r>=boardSize || c<0 || c>=boardSize || board[r][c] != player) break; count++; } // 超简版评分:连续棋子数越多,分数越高 switch (count) { case 5: return SCORE_FIVE; case 4: return SCORE_FOUR; case 3: return SCORE_THREE; case 2: return SCORE_TWO; default: return 0; } } }

这个AI类接收当前棋盘状态和AI的棋子颜色。getBestMove方法遍历所有空位,对每个空位进行“攻防双向评估”:先假设AI自己下在这里能得多少分(进攻),再假设对手下在这里会对自己造成多大威胁(防守),将两者加权求和作为该点的总价值。最后选择价值最高的点落子。

evaluateDirection函数是核心,完整的实现需要判断“活三”(两头无阻挡)、“冲四”(一头被堵)等复杂棋型。上面是一个极度简化的版本,只统计了连续棋子数。一个真正可用的评估函数代码量会大很多,你可以将其作为后续的优化方向。

5.2 整合AI到游戏循环

在ChessBoardView中,我们需要修改游戏模式,并添加AI落子的逻辑。

public class ChessBoardView extends View { // ... 之前变量 ... private boolean isAIMode = false; // 是否为人机对战模式 private GomokuAI ai; private int aiPlayer = 2; // 假设AI执白 // 设置游戏模式 public void setGameMode(boolean isAIMode) { this.isAIMode = isAIMode; if (isAIMode) { ai = new GomokuAI(boardMatrix, aiPlayer); // 如果AI先手,则AI先下一步 if (!isBlackTurn && aiPlayer == 2) { // 白棋先手且是AI postDelayed(this::aiMove, 500); // 延迟500毫秒,模拟思考 } } } // AI走棋 private void aiMove() { if (!isAIMode || (isBlackTurn && aiPlayer == 1) || (!isBlackTurn && aiPlayer == 2)) { // 不是AI模式,或者当前不是AI的回合 return; } int[] move = ai.getBestMove(); if (move != null && boardMatrix[move[0]][move[1]] == 0) { boardMatrix[move[0]][move[1]] = aiPlayer; if (gameCallback != null) { gameCallback.onPiecePlaced(aiPlayer, move[0], move[1]); } if (checkWin(move[0], move[1])) { if (gameCallback != null) { gameCallback.onGameOver(aiPlayer); } return; } isBlackTurn = !isBlackTurn; invalidate(); } } // 修改 onTouchEvent,在人机模式下,玩家落子后触发AI思考 @Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { // ... 坐标转换和有效性检查 ... if (isAIMode && ((isBlackTurn && aiPlayer == 2) || (!isBlackTurn && aiPlayer == 1))) { // 人机模式下,且当前是AI的回合,忽略玩家触摸 return false; } // ... 玩家落子逻辑 ... // 玩家落子后,重绘,然后如果是人机模式且游戏未结束,则触发AI invalidate(); if (isAIMode && !checkWin(row, col)) { postDelayed(this::aiMove, 500); // 延迟一下,让玩家看到自己的棋子 } return true; } return super.onTouchEvent(event); } }

在MainActivity中,我们可以通过一个按钮来切换游戏模式。

// 在MainActivity的布局中添加一个按钮 // 在onCreate中 Button btnMode = findViewById(R.id.btn_mode); btnMode.setOnClickListener(v -> { // 切换模式 chessBoardView.setGameMode(true); // 切换为人机模式 chessBoardView.resetGame(); // 更新按钮文字等 });

现在,运行应用,切换到人机模式,你就可以和这个“初级AI”对战了。虽然它的棋力很弱(因为评估函数太简单),但已经具备了最基本的攻防意识,不会乱下。

6. 功能完善与体验优化:让游戏更完整

基础功能完成后,我们可以添加一些特性让游戏更像一个真正的产品。

6.1 悔棋功能

悔棋是棋类游戏的标配。我们需要一个栈(Stack)来记录每一步的落子位置。

public class ChessBoardView extends View { // ... 之前变量 ... private Stack<int[]> moveHistory = new Stack<>(); // 记录每一步 [row, col, player] // 在落子成功的地方(onTouchEvent和aiMove中),将步骤存入历史 // boardMatrix[row][col] = player; // moveHistory.push(new int[]{row, col, player}); // 悔棋方法 public boolean undo() { if (moveHistory.isEmpty()) { return false; } int[] lastMove = moveHistory.pop(); int row = lastMove[0]; int col = lastMove[1]; boardMatrix[row][col] = 0; // 清空该位置 isBlackTurn = !isBlackTurn; // 回合回退 invalidate(); if (gameCallback != null) { gameCallback.onUndo(); // 通知Activity更新状态 } return true; } }

在Activity中添加一个悔棋按钮,调用chessBoardView.undo()即可。注意,在人机模式下,悔棋可能需要连续调用两次(撤销AI的一步和玩家的一步),逻辑会更复杂一些。

6.2 游戏状态持久化

防止游戏被意外中断,我们可以简单保存棋盘状态。使用SharedPreferences或直接序列化boardMatrix数组。

// 在ChessBoardView中 public void saveGameState(Context context) { SharedPreferences prefs = context.getSharedPreferences("GomokuPrefs", Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); // 将二维数组扁平化为字符串存储,例如“1,2,0,1,...” StringBuilder sb = new StringBuilder(); for (int i = 0; i < boardRows; i++) { for (int j = 0; j < boardRows; j++) { sb.append(boardMatrix[i][j]).append(","); } } editor.putString("board_state", sb.toString()); editor.putBoolean("is_black_turn", isBlackTurn); editor.putBoolean("is_ai_mode", isAIMode); editor.apply(); } public void loadGameState(Context context) { SharedPreferences prefs = context.getSharedPreferences("GomokuPrefs", Context.MODE_PRIVATE); String savedState = prefs.getString("board_state", ""); if (!savedState.isEmpty()) { String[] values = savedState.split(","); int index = 0; for (int i = 0; i < boardRows; i++) { for (int j = 0; j < boardRows; j++) { if (index < values.length) { boardMatrix[i][j] = Integer.parseInt(values[index++]); } } } isBlackTurn = prefs.getBoolean("is_black_turn", true); isAIMode = prefs.getBoolean("is_ai_mode", false); invalidate(); } }

在MainActivity的onPause()中调用saveGameState,在onCreate()中调用loadGameState,就可以实现简单的断点续玩。

6.3 音效与视觉反馈

为落子和胜负添加音效,能让体验提升一个档次。在res/raw/目录下放入音效文件(如place.mp3,win.mp3)。

// 在ChessBoardView或Activity中 private void playSound(Context context, int soundResId) { MediaPlayer mediaPlayer = MediaPlayer.create(context, soundResId); mediaPlayer.setOnCompletionListener(MediaPlayer::release); mediaPlayer.start(); } // 在落子和游戏结束时调用

视觉上,可以在落子时添加一个简单的缩放动画,或者在获胜时高亮显示连成五子的那排棋子。这需要更复杂的自定义绘制逻辑,比如在onDraw中根据获胜坐标数组,用高亮颜色的画笔再画一遍线。

7. 项目总结与扩展思考

走完以上所有步骤,一个功能相对完整的Android五子棋应用就诞生了。回顾这个项目,它几乎涵盖了Android应用开发入门阶段需要掌握的大部分核心技能:从环境搭建、UI自定义绘制、事件处理、到基础算法(胜负判断、AI评估)、数据持久化、多媒体交互等。

在这个过程中,最容易出问题的往往是环境配置和坐标转换这些“脏活累活”。比如Gradle下载慢、模拟器启动失败、触摸坐标计算偏差导致落子不准。我的经验是,环境问题优先考虑换源和真机调试;坐标问题一定要在onDraw里把计算用的关键坐标点(如offset,cellWidth)用canvas.drawCircle画出来,看看它们到底在哪,这是最直接的调试方法。

这个项目还有很多可以深入和扩展的方向:

  1. AI强化:目前的AI非常弱。你可以研究Minimax算法(极大极小值算法)配合Alpha-Beta剪枝,这是棋类AI的经典方法。网上有很多五子棋AI的开源实现,可以参考其评估函数。
  2. 网络对战:使用Socket或WebSocket实现双人在线对战。这涉及到网络编程、数据同步、房间管理等更复杂的知识。
  3. UI美化:使用更精美的棋盘和棋子图片替代纯色绘制,加入更流畅的动画效果。
  4. 游戏功能:加入禁手规则(针对专业五子棋)、棋谱记录与回放、难度选择等。

对于初学者来说,不要一开始就追求把所有功能做全。我的建议是,先把这个基础版本完完整整地自己敲一遍,确保每一步都理解。遇到问题,善用Logcat输出调试信息,善用搜索引擎和官方文档。当你看到自己写的程序能跑起来,并且能和你“对战”时,那种成就感是看再多教程都无法替代的。这个项目就像一个骨架,后续的血肉(更优的AI、更好的UI、更多功能)可以由你根据自己的兴趣和时间,一点点添加上去。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询