十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

五子棋Android项目实战:Room本地存储与Flask后台同步方案

五子棋Android项目实战:Room本地存储与Flask后台同步方案 简介Android课程设计或期末大作业可参考一套完整可运行的五子棋项目资源项目基于Android Studio开发整合了Java源码、XML界面布局、PNG素材、Gradle构建配置、第三方依赖库、后台数据库以及APK安装包共121个文件压缩后约11.47MB。源码注释详尽逻辑模块划分清晰涵盖棋盘绘制、落子判定、胜负检测、人机对战与双人对战等核心功能界面简洁美观操作流畅后台数据库可存储用户信息与对局记录配套说明文档从环境搭建到代码解析逐层说明方便新手理解项目全貌与答辩要点。资源中还提供可直接运行的APK文件和构建脚本导入Android Studio即可部署也可作为模板二次开发。目前已有94人学习下载是完成高分大作业或入门Android开发的实用参考。1. 一个 Android Studio 五子棋大作业分数最容易丢在“后台数据库”这四个字上五子棋本身不是这个项目的难点15×15 的棋盘就是一个二维数组落子就是改一个元素胜负判断就是横、竖、两条斜线四个方向各自数连续同色棋子的个数。真正让同一批大作业拉开分数差距的是标题里那句“后台数据库”是否名副其实。评阅老师看项目时代码能跑是基础分棋局记录能不能写进数据库、App 杀掉重开之后数据还在不在、说明文档里能不能把数据流动讲清楚才是高分段的区分项。下面这条路线适合正在用 Android Studio 做课程设计、又不想只交一个“双人对战小游戏”的开发者先立住棋盘逻辑再用自定义 View 画出界面接着用 Room 做本地库最后补一个可选的局域网后台文档按可复现的顺序写。2. 五子棋项目核心棋盘数据结构、连五判定与人机 AI 的最小实现2.1 棋盘数据结构与落子校验棋盘我一般直接用int[N][N]不用集合类。原因很简单数组下标天然对应棋盘坐标访问和赋值都是 O(1)判断越界也直观。五子棋标准盘是 15×15所以常量定义成public class Board { public static final int N 15; public static final int EMPTY 0; public static final int BLACK 1; public static final int WHITE 2; private final int[][] grid new int[N][N]; public boolean move(int row, int col, int player) { if (row 0 || row N || col 0 || col N || grid[row][col] ! EMPTY) { return false; } grid[row][col] player; return true; } public int get(int row, int col) { return grid[row][col]; } }move方法返回boolean落子不合法时 UI 层可以直接弹提示不用再单独查一次状态。把“越界检查”和“空格检查”放在同一个条件里是这道题最不容易漏的写法。不要把这个判断分散到 Activity 里否则多人对战、人机对战、悔棋三个入口要各自维护一套规则后面改 AI 时很容易把逻辑改出不一致。2.2 横竖斜四个方向的五连判定胜利判定只需要从刚落子的位置向四个方向扩展水平、垂直、主对角线、副对角线。每个方向都从落子点向正反两边数连续同色总数大于等于 5 就赢。public boolean isWin(int row, int col, int player) { int[][] directions {{1, 0}, {0, 1}, {1, 1}, {1, -1}}; for (int[] d : directions) { int count 1; count countDirection(row, col, player, d[0], d[1]); count countDirection(row, col, player, -d[0], -d[1]); if (count 5) { return true; } } return false; } private int countDirection(int row, int col, int player, int dr, int dc) { int count 0; int r row dr; int c col dc; while (r 0 r N c 0 c N grid[r][c] player) { count; r dr; c dc; } return count; }这个实现的优点是只扫描棋盘上极少一部分格子落子后单次判定时间复杂度是 O(1) 量级。有一个容易写错的地方方向数组里{1, -1}表示副对角线配合它的反向{-1, 1}才是一条完整的斜线。很多作业在这里只写了{1, 1}和{1, -1}两个方向忘记各自的反方向长连就会判断漏掉。默认不做禁手规则课程设计只要“成五即胜”就够了。2.3 人机对战评估函数与候选点裁剪如果作业要求人机对战AI 部分不要一上来就写完整搜索树。先做评估函数给每个空位打分分数由“我方的潜在连子”和“对方的潜在连子”共同决定。private int evaluatePoint(int row, int col, int player) { int attack lineScore(row, col, player); int defend (int) (lineScore(row, col, player BLACK ? WHITE : BLACK) * 0.9); return attack defend; } private Listint[] candidates() { Listint[] result new ArrayList(); for (int r 0; r N; r) { for (int c 0; c N; c) { if (grid[r][c] ! EMPTY || !hasNeighbor(r, c)) { continue; } result.add(new int[]{r, c}); } } result.sort((a, b) - evaluatePoint(b[0], b[1], currentPlayer) - evaluatePoint(a[0], a[1], currentPlayer)); return result.subList(0, Math.min(10, result.size())); }lineScore统计某个位置在四个方向上能形成的活二、活三、活四。分数表可以按这个量级给活二 10 分活三 100 分活四 1000 分成五直接返回极大值。防守时乘 0.9 是为了让 AI 在敌方威胁极大时能优先堵又在局面均衡时倾向进攻。这个权重不是唯一的答辩时能说清“为什么防守要略低于进攻”就足够。如果需要再上一个台阶可以在这个评估函数外套一层极小化极大搜索深度两层加 Alpha-Beta 剪枝对五子棋足够应付课程演示。搜索的模板比评估函数容易写真正决定棋力的是评估函数本身。3. Android Studio 界面层自定义 View 画棋盘、触摸落子与悔棋栈3.1 为什么大作业优先选自定义 View 而不是 GridLayout很多教程会把棋盘写成GridLayout加 225 个ImageView但这样做有两个问题一是 225 个 View 对象的创建和布局开销不小二是不方便做悔棋和动画。自定义 View 是更稳的做法一张Canvas画全部内容onDraw里画网格和棋子onTouchEvent里换算坐标逻辑集中代码量反而更少。在文档里写“棋盘采用自定义 View 绘制减少 View 层级”也能让评阅老师看出你没有只做界面堆叠。3.2 用 onSizeChanged 计算格子尺寸在 onDraw 里画棋盘自定义 View 的核心是坐标系换算。先拿到 View 的实际大小再算出每个格子的边长。public class BoardView extends View { private int gridSize; private int cellSize; private Board board; private Paint linePaint; public BoardView(Context context, AttributeSet attrs) { super(context, attrs); linePaint new Paint(); linePaint.setColor(Color.BLACK); linePaint.setStrokeWidth(2); } Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); gridSize Math.min(w, h); cellSize gridSize / (Board.N - 1); } Override protected void onDraw(Canvas canvas) { for (int i 0; i Board.N; i) { canvas.drawLine(0, i * cellSize, (Board.N - 1) * cellSize, i * cellSize, linePaint); canvas.drawLine(i * cellSize, 0, i * cellSize, (Board.N - 1) * cellSize, linePaint); } for (int r 0; r Board.N; r) { for (int c 0; c Board.N; c) { if (board.get(r, c) ! Board.EMPTY) { canvas.drawCircle(c * cellSize, r * cellSize, cellSize * 0.4f, chessPaint); } } } } }这里最容易踩的坑是cellSize的计算棋盘有 15 条线网格间距只有 14 份所以要用(Board.N - 1)做除数。如果用 15 除最右边和最下边的线会超出棋盘点位触摸坐标也会整体偏移。另外onDraw里要遍历整个棋盘数组而不是维护一个单独的“棋子列表”这样悔棋和重开时只需要改数组再invalidate()状态永远不会和界面不同步。3.3 触摸落子、线程更新与悔棋栈落子事件在onTouchEvent里获取点击抬起时把像素坐标换算成行列坐标然后回调给 Activity 或 ViewModel。坐标换算用Math.round()而不是强转int否则点击靠近格线右侧时容易落到错误的列上。Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() MotionEvent.ACTION_UP) { int col Math.round(event.getX() / cellSize); int row Math.round(event.getY() / cellSize); if (listener ! null) { listener.onCellClicked(row, col); } } return true; }Activity 里用一个Stackint[]记录每次落子的行列坐标Stackint[] history new Stack(); public void onCellClicked(int row, int col) { if (!board.move(row, col, currentPlayer)) { return; } history.push(new int[]{row, col}); boardView.invalidate(); if (board.isWin(row, col, currentPlayer)) { showWinner(currentPlayer); return; } currentPlayer currentPlayer Board.BLACK ? Board.WHITE : Board.BLACK; } public void undo() { if (history.isEmpty()) { return; } int[] last history.pop(); board.set(last[0], last[1], Board.EMPTY); boardView.invalidate(); currentPlayer currentPlayer Board.BLACK ? Board.WHITE : Board.BLACK; }invalidate()只负责请求重绘不会立刻执行onDraw所以连续快速落子也不会有并发问题。悔棋时先pop再恢复当前位置为空然后同样调用invalidate()。注意悔棋之后要切换当前玩家否则会出现同一方连续走两步的状态错乱。4. 后台数据库落地Room 本地存储与局域网 HTTP 后台的两级方案4.1 先分清“后台数据库”在 Android 项目里的真实含义“后台数据库”这四个字在课程作业里有两种理解。第一种是本地持久化也就是把对局记录存在手机自带的 SQLite 里Android 平台上通常用 Room 封装第二种是真正的客户端-服务器架构手机通过 HTTP 接口把数据提交给电脑上跑的一个后台服务由它写入数据库。只做第一种老师可能会说“这不叫后台”只做第二种演示时网络一断就白屏。高分做法是两层都做本地用 Room 保底电脑上用 Flask 起一个轻量服务接收同步数据两边都写进文档。维度Room 本地存储局域网 HTTP 后台演示稳定性高离线可用中依赖模拟器与电脑连通实现量1 个实体 1 个 DAO额外 1 个 Python 工程文档加分点数据持久化设计C/S 架构与网络编程答辩风险“不够后台”连接失败、IP 配置错误如果时间不够Room 是必须完成的底线HTTP 后台可以作为进阶章节放文档末尾评阅老师通常会把这两个层级都看到。4.2 Room 三件套Entity、DAO、Database 与调用姿势Room 是把 SQLite 操作封装成注解的库。先在build.gradle里加依赖implementation androidx.room:room-runtime:2.6.1 annotationProcessor androidx.room:room-compiler:2.6.1用 Java 写项目就用annotationProcessor用 Kotlin 写则要换成kapt并先应用kotlin-kapt插件。这个位置是 Android Studio 初学者最容易卡住的地方报错通常是编译时找不到Room相关注解处理器。实体类对应一张表字段对应列。对局记录最少要有玩家、结果、落子序列和时间Entity(tableName game_record) public class GameRecord { PrimaryKey(autoGenerate true) public long id; ColumnInfo(name black_player) public String blackPlayer; ColumnInfo(name white_player) public String whitePlayer; public String result; public String moves; public long timestamp; }moves字段我用7,7;7,8;8,7;8,8这样的字符串保存落子序列解析时按分号和逗号切分即可不需要额外引入对象序列化库。DAO 接口只写两个方法一个插入一个查询Dao public interface GameRecordDao { Insert void insert(GameRecord record); Query(SELECT * FROM game_record ORDER BY timestamp DESC) ListGameRecord getAll(); }数据库类用单例避免每次访问都重新创建实例Database(entities {GameRecord.class}, version 1, exportSchema false) public abstract class AppDatabase extends RoomDatabase { public abstract GameRecordDao gameRecordDao(); private static volatile AppDatabase INSTANCE; public static AppDatabase getInstance(Context context) { if (INSTANCE null) { synchronized (AppDatabase.class) { if (INSTANCE null) { INSTANCE Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, gobang.db).build(); } } } return INSTANCE; } }Room 不允许在主线程执行数据库操作所以插入和查询都要放到子线程。简单做法是维护一个全局ExecutorServiceAppExecutors.getInstance().getDiskIO().execute(() - { db.gameRecordDao().insert(record); });不要在execute里直接操作 UI。如果对局结束后要从数据库刷新列表用runOnUiThread或者LiveData都行课程作业里前者更直白。4.3 把对局同步到 Flask SQLite注意 10.0.2.2 和明文 HTTP后台服务我用 Python 的 Flask因为它文件少、依赖少答辩时能现场跑起来。数据库直接复用 SQLite字段和 Room 表保持一致import sqlite3 import time from flask import Flask, request, jsonify conn sqlite3.connect(gobang.db, check_same_threadFalse) conn.execute( create table if not exists game_record ( id integer primary key autoincrement, black text, white text, result text, moves text, ts integer ) ) app Flask(__name__) app.post(/api/game) def save_game(): data request.get_json() conn.execute( insert into game_record(black, white, result, moves, ts) values(?, ?, ?, ?, ?), (data[black], data[white], data[result], data[moves], int(time.time())) ) conn.commit() return jsonify({code: 0})Android 端用系统自带的HttpURLConnection不额外引入 OkHttp减少一个依赖变量private void postGameToServer(final GameRecord record) { new Thread(() - { try { URL url new URL(http://10.0.2.2:5000/api/game); HttpURLConnection conn (HttpURLConnection) url.openConnection(); conn.setRequestMethod(POST); conn.setRequestProperty(Content-Type, application/json); conn.setDoOutput(true); String json buildJson(record); conn.getOutputStream().write(json.getBytes(UTF-8)); conn.getResponseCode(); conn.disconnect(); } catch (Exception e) { e.printStackTrace(); } }).start(); }模拟器里访问电脑本机不能用localhost要用10.0.2.2这是 Android 模拟器专门映射到宿主机回环地址的固定规则。真机调试时改成电脑的局域网 IP例如http://192.168.1.8:5000/api/game并且要保证电脑防火墙放行 5000 端口。Android 9 开始默认禁止明文 HTTP 流量如果请求失败并报Cleartext HTTP traffic not permitted需要在AndroidManifest.xml的application上加一行android:usesCleartextTraffictrue。5. 让“详细说明文档”拿高分结构组织、截图与答辩话术5.1 文档按评审视角组织说明文档不是记录“每天做了什么事”而是让一个没看过你代码的人能顺着目录看懂项目。我一般把一个完整的 Android Studio 课设文档压成四章需求分析、总体设计、核心实现、测试验证。需求分析写清楚“支持双人对战、人机对战、对局记录查询”三个功能点即可不用长篇大论。总体设计放一张类关系图把 Board、BoardView、GameRecord、AppDatabase 的关系画清楚。核心实现里用一个“文件路径 职责 答辩一句话”的表格评阅老师能按图索骥你答辩时也不会忘词。文件职责答辩一句话Board.java棋盘数据结构与胜负判定15×15 数组四个方向扫描连子BoardView.java自定义绘制与触摸单张 Canvas 画完所有棋子GameRecord.java对局记录实体Room 注解映射 SQLite 表GameRecordDao.java数据访问层插入和按时间倒序查询5.2 答辩会被追问的技术细节答辩时老师不会照着文档念他只会挑几个点确认你是不是真的写明白了。最常被问到的是这几个为什么棋盘是 15×15AI 评估函数里的攻击分和防守分是怎么调的房间的moves字段为什么不拆成多张表如果 App 在对局中突然被杀已走的步骤会不会丢。最后一个问题最容易暴露问题因为答案是“只有一局结束时才写库”从按下落子到写库之间断电这局就丢了。遇到追问就老实说明再补一句“可以改成每步落子后自治”老师更认可这种对边界的理解。5.3 演示录像的录制节奏演示视频控制在 3 到 5 分钟。先展示棋盘 UI下一局完整的棋过程中快速点到“对局记录”页面把 Room 里的记录列表露出来。再切到电脑端用数据库工具打开gobang.db把同一局的数据指给镜头看。录制时打开 Android Studio 自带的 Database Inspector让落子序列和game_record表同框出现这比任何口头解释都有效。本文还有配套的精品资源点击获取
返回列表