Android Java

Cờ tướng Android người đấu máy: AI, Canvas và vòng đời ứng dụng

Tích hợp tìm kiếm alpha–beta với bàn cờ 9×10, ba mức khó, đi lại, lưu ván và kiểm tra callback từ lượt cũ.

Thực hànhDự án thực hành nâng cao25 phút đọcJava · Android · Canvas

Thích và đánh giá Bình luận và hỏi đáp
Engine quản lý luật; luồng AI tính nước; main thread chỉ áp dụng kết quả còn đúng ván.
Engine quản lý luật; luồng AI tính nước; main thread chỉ áp dụng kết quả còn đúng ván. Mở hình lớn ↗

Tích hợp tìm kiếm alpha–beta với bàn cờ 9×10, ba mức khó, đi lại, lưu ván và kiểm tra callback từ lượt cũ. Các tệp nguồn, Gradle wrapper, hướng dẫn chạy và kiểm thử đi kèm trong ZIP.

Xem trọn bộ 5 dự án Android · Tải dự án Android đầy đủ (.zip) · Tải APK debug 1.1 để cài thử

1. Một máy đánh, hai bộ luật

Bài này ghép XiangqiEngine với AI negamax và giao diện Canvas để người cầm Đỏ đấu với máy Đen. Lõi tìm kiếm gần như giống cờ vua vì nó chỉ cần hỏi bên đi, các nước hợp lệ, vị trí sau nước, trạng thái cuối ván và điểm đánh giá. Những gì khác nhau như bí nước, sông hay ngòi Pháo phải nằm trong engine. Đây là một ví dụ thiết kế hướng đối tượng có thể đo được mức tái sử dụng.

Tải ZIP ở đầu bài, giải nén rồi mở thư mục có settings.gradle bằng Android Studio. Trong SDK Manager cài Android SDK Platform 35; chọn Gradle JDK 17. Dự án khóa Android Gradle Plugin 8.6.1 và Gradle 8.7, minSdk 26. Chờ Sync rồi chạy module app trên Android 8.0 trở lên. Lần đầu cần mạng để tải công cụ; bản ứng dụng sau khi cài hoạt động ngoại tuyến. Không cần khóa API, .NET hay dịch vụ AI trả phí.

Windows:       gradlew.bat assembleDebug
Linux/macOS:   chmod +x gradlew
               ./gradlew assembleDebug
APK: app/build/outputs/apk/debug/app-debug.apk

Nếu báo không tìm thấy SDK, mở lại dự án bằng Android Studio để tạo local.properties theo máy; không chép đường dẫn SDK từ máy khác. Nếu Gradle báo sai phiên bản Java, kiểm tra Gradle JDK thay vì chỉ kiểm tra lệnh java -version. Cấu hình này là một bộ phiên bản tương thích để học, không khẳng định là phiên bản Android mới nhất.

2. Các thao tác thực tế trên màn hình

Chạm vào quân Đỏ để chọn, sau đó chạm một dấu vàng để đi. Bàn vẽ 9 cột, 10 hàng giao điểm, hai cung và một dải sông. Các quân dùng ký hiệu Việt: TG, S, T, M, X, P, B; màu Đỏ/Đen phân biệt hai bên. Ván mới có xác nhận, Đi lại đưa về lượt người, và bộ chọn độ khó thay đổi độ sâu tối đa 1/2/3 ply.

Trước khi viết nét bàn, cần tính cell bằng giá trị nhỏ hơn giữa giới hạn chiều rộng và chiều cao. Tâm ô đầu được căn giữa toàn vùng vẽ; cùng công thức ấy đổi tọa độ chạm về hàng/cột. Nếu vẽ theo một tỷ lệ nhưng bắt chạm bằng tỷ lệ khác, game trông đúng ở một máy và chọn lệch ở máy khác. Canvas chỉ trình bày; trạng thái thực không nằm ở màu pixel.

3. Hợp đồng tìm kiếm và giá trị quân

GamePosition cung cấp legalMoves, after, inCheck, drawn, lossOnStalemate và evaluate. Một nước sau khi sinh luôn được kiểm tra lại với engine trước lúc UI commit. Điểm evaluate đo theo bên đến lượt. Trọng số quân, tiến Binh và trung tâm là heuristic minh bạch của bản mẫu; chúng không phải định giá chính xác cho mọi thế cờ. Đặc biệt, giá Pháo ở khai cuộc và tàn cuộc có thể khác nhau nếu mở rộng theo pha ván.

Một vị trí không có nước đi được chấm thua nếu bị chiếu hoặc engine nói bí nước là thua. Chỉ sau bước này mới xét drawn và độ sâu bằng 0. Đảo thứ tự một cách tùy tiện có thể biến một thế bí nước cờ tướng thành hòa. Bộ kiểm thử vì vậy có riêng vị trí Tướng chưa bị chiếu nhưng mọi cửa đi đều bị khóa.

if (moves.isEmpty()) {
    return p.inCheck() || p.lossOnStalemate()
            ? -MATE + ply : 0;
}
if (p.drawn()) return 0;
if (depth == 0) return p.evaluate();

4. Alpha–beta, tìm sâu dần và giới hạn

Ở mỗi nút, thuật toán thử các nước theo thứ tự ưu tiên, gọi đệ quy với dấu điểm đảo và cửa sổ [−beta,−alpha]. Khi alpha đạt beta, các nước còn lại của nút không cần mở rộng. Trên bàn cờ tướng, số nước Pháo và Xe có thể lớn, vì vậy sinh nước và thứ tự duyệt ảnh hưởng đáng kể đến tổng thời gian. Cắt nhánh chỉ có ích nếu legalMoves và after đúng.

Mỗi lần chọn nước giải lần lượt các mức 1 đến maxDepth. Độ sâu được công bố khi toàn bộ vòng đã xong. Ngân sách 300/700/1200 ms dùng nanoTime; hủy tìm bằng interruption khi người đổi ván hoặc ứng dụng tạm dừng. Bản này chưa có transposition table, quiescence hay sách khai cuộc, nên chữ Khó chỉ là tương quan trong ba mức của bài thực hành, không phải cam kết đạt trình độ thi đấu.

Một thí nghiệm có ý nghĩa là giữ nguyên 10 vị trí, chạy mỗi cấu hình nhiều lần, lưu nước chọn, độ sâu hoàn tất, số nút và thời gian. Dùng các thế bắt quân bắt buộc, tránh chiếu hết và bí nước; không chỉ chọn khai cuộc yên tĩnh. Cùng một nước đúng có thể được tìm với số nút khác nhau trên các máy, vì vòng sâu hơn có thể bắt đầu rồi hết giờ.

5. Tổ chức luồng Android để không đóng băng bàn cờ

Main thread xử lý chạm và vẽ; một ExecutorService duy nhất chạy AI. Trước khi gửi tìm kiếm, Activity chụp position và generation. Khi kết quả về, kiểm tra Activity còn sống/đang resume, token còn khớp và position chưa thay đổi. Bấm Đi lại hay Ván mới vừa hủy Future vừa tăng generation, nên một kết quả cũ không thể đặt quân vào ván khác.

Không truyền mảng bàn có thể bị sửa trực tiếp cho luồng AI. Engine tạo ảnh chụp mới sau mỗi nước, nhờ đó AI không nhìn thấy nửa bàn cũ và nửa bàn mới khi người dùng tương tác. Khi lưu trong onPause, dùng cùng hàm encode như lúc đánh, không tuần tự hóa các đối tượng View. View có thể được dựng lại từ trạng thái; nó không phải nguồn dữ liệu chính.

6. Mã AI và đường tích hợp

Môi trường thực hành. Dự án Android Java đầy đủ được tải bằng liên kết ZIP ở đầu bài. Android Studio, JDK 17, AGP 8.6.1, Gradle 8.7, compileSdk 35 và thiết bị API 26 trở lên. Dùng Activity/Canvas/SQLite gốc; xem README để chạy và kiểm tra. Tệp dưới đây là một thành phần của dự án.

Java · Android · Canvas · XiangqiAi.javaTải mã nguồn
package vn.kienutt.xiangqi;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

/** Negamax + alpha-beta, iterative deepening, time budget and cancellation. */
public final class XiangqiAi {
  public static final class Result {
    public final Move move;
    public final int depth, score;
    public final long nodes;

    Result(Move m, int d, int s, long n) {
      move = m;
      depth = d;
      score = s;
      nodes = n;
    }
  }

  private static final int MATE = 100000, INF = 1000000;
  private long deadline, nodes;

  private static final class Stop extends RuntimeException {
    Stop() {
      super(null, null, false, false);
    }
  }

  private void budget() {
    if (Thread.currentThread().isInterrupted() || System.nanoTime() - deadline >= 0)
      throw new Stop();
  }

  private int order(GamePosition p, Move m) {
    return Math.abs(p.pieceAt(m.to)) * 100 + m.promotion * 80;
  }

  private List<Move> ordered(GamePosition p, List<Move> moves) {
    List<Move> copy = new ArrayList<>(moves);
    copy.sort(Comparator.comparingInt((Move m) -> order(p, m)).reversed());
    return copy;
  }

  private int negamax(GamePosition p, int depth, int alpha, int beta, int ply) {
    budget();
    nodes++;
    List<Move> moves = p.legalMoves();
    if (moves.isEmpty()) return p.inCheck() || p.lossOnStalemate() ? -MATE + ply : 0;
    if (p.drawn()) return 0;
    if (depth == 0) return p.evaluate();
    int best = -INF;
    for (Move m : ordered(p, moves)) {
      int value = -negamax(p.after(m), depth - 1, -beta, -alpha, ply + 1);
      if (value > best) best = value;
      if (value > alpha) alpha = value;
      if (alpha >= beta) break;
    }
    return best;
  }

  public Result choose(GamePosition p, int maxDepth, int millis) {
    if (maxDepth < 1 || millis < 1) throw new IllegalArgumentException("Ngân sách không hợp lệ");
    deadline = System.nanoTime() + millis * 1000000L;
    nodes = 0;
    List<Move> roots = p.legalMoves();
    if (roots.isEmpty() || p.drawn()) return new Result(null, 0, 0, nodes);
    Move completedMove = roots.get(0);
    int completedDepth = 0, completedScore = 0;
    for (int depth = 1; depth <= maxDepth; depth++) {
      Move best = null;
      int value = -INF, alpha = -INF;
      try {
        for (Move m : ordered(p, roots)) {
          budget();
          int v = -negamax(p.after(m), depth - 1, -INF, -alpha, 1);
          if (v > value) {
            value = v;
            best = m;
          }
          alpha = Math.max(alpha, v);
        }
        completedMove = best;
        completedDepth = depth;
        completedScore = value;
      } catch (Stop stop) {
        break;
      }
    }
    return new Result(completedMove, completedDepth, completedScore, nodes);
  }
}

Trong ZIP, XiangqiActivity.startAi là điểm nối worker với UI; chooseMove nhận thao tác; commit thêm lịch sử rồi tạo vị trí mới. Đọc theo tuyến này giúp thấy một hành động của người chuyển thành trạng thái thế nào. Nếu muốn cho người cầm Đen, phải sửa cả điều kiện nhận thao tác, lượt mở ván và cách trình bày tọa độ, không chỉ đổi nhãn “Bạn”.

7. Bộ ca để phân biệt lỗi luật và lỗi đánh giá

Dấu hiệuKiểm tra trướcHướng xử lý
Máy đi Mã qua chân bị chặnNước ấy có xuất hiện trong legalMoves không?Sửa engine trước AI
Máy không tránh mất Xe sau vài lượtTăng độ sâu trên cùng vị tríPhân tích đường chân trời và đánh giá
Bấm ván mới rồi quân tự nhảyToken kết quả và snapshotChặn callback cũ
Bí nước nhưng báo hòalossOnStalemate và thứ tự terminalSửa quy tắc kết thúc

Các kiểm tra hiện có xác nhận AI trả về nước hợp lệ và không thay đổi đầu vào, đồng thời chạy bộ luật với các chướng ngại đặc thù. Thực hành thêm kiểm thử hủy: chọn Khó, về Home ngay sau nước của mình rồi mở lại. Chỉ được có một kết quả áp dụng cho vị trí hiện tại. Chạy lại nhiều lần vì lỗi đồng thời không phải lúc nào cũng xuất hiện ở lần đầu.

8. Nâng cấp có kiểm soát

Bước tiếp theo có thể là bảng điểm theo ô, độ linh hoạt Xe/Pháo, tính an toàn Tướng và tìm kiếm tĩnh ở nhánh bắt quân. Mỗi thay đổi cần tập thế cố định cùng một baseline. Nếu thêm học máy, hãy ghi rõ dữ liệu, mục tiêu huấn luyện và vai trò của mạng trong tìm kiếm; không gọi một bảng if/else là mạng học sâu.

Quy tắc hòa lặp và 60 nước không bắt của bài chỉ là quy ước dạy học; chưa phân xử WXF trường chiếu/trường tróc. Việc AI tránh thua trong một ván ngắn không chứng minh nó tuân thủ toàn bộ luật thi đấu. Khi dùng bài cho đồ án, tách ba tiêu chí: đúng luật trong phạm vi công bố, giao diện ổn định và chất lượng chọn nước.

Bản 1.1 đã dựng APK debug bằng bộ công cụ Android đầy đủ; lint không có lỗi. Dự án này đã qua 2 ca JUnit/Robolectric, gồm mở màn hình, pause/resume và tái tạo Activity trên Android API 35 mô phỏng trong JVM. Hai dự án quản lý còn kiểm thử trực tiếp SQLiteOpenHelper với SQLite native. Chưa kiểm tra trên điện thoại thật; hãy chạy thêm checklist thiết bị trong README, nhất là thao tác cảm ứng, cỡ chữ và độ trễ AI.

Đọc thêm tài liệu gốc: bộ công cụ AGP 8.6, vẽ Custom View với Canvas và SQLite trên Android. Các lớp và dữ liệu trong bài được xây dựng cho thực hành, không phải mã của các tổ chức được dẫn nguồn.

Xem bộ luật cờ tướng và AI cờ vua để đối chiếu phần chung/phần riêng.

9. Chạy lại kiểm thử và dựng APK

Mở Terminal tại thư mục dự án. Trên Windows chạy lệnh dưới; trên Linux/macOS thay gradlew.bat bằng ./gradlew. assembleDebug tạo APK; lintDebug kiểm tra tĩnh; testDebugUnitTest chạy các ca JUnit và Robolectric. Ba việc này bổ sung cho nhau, không đồng nghĩa đã thử trên mọi điện thoại.

gradlew.bat assembleDebug lintDebug testDebugUnitTest
# APK: app/build/outputs/apk/debug/app-debug.apk
# Test: app/build/reports/tests/testDebugUnitTest/index.html

ZIP có đầy đủ mã kiểm thử trong app/src/test/java và kiểm tra logic/SQL ở thư mục tests. Chỉnh một luật hoặc một truy vấn, chạy lại bộ ca liên quan trước khi dựng APK mới. Thư viện kiểm thử chỉ dùng lúc phát triển, không được đóng vào ứng dụng. APK cung cấp trên trang là bản debug học tập.