Bot 2048 biết tự học: Tinh chỉnh hàm đánh giá bằng self-play

Ở phần 1, ta xây một con bot 2048 dùng expectimax + một hàm đánh giá (evaluation function) được “tự tay” thiết kế: nhiều ô trống, mượt, đơn điệu, ô to nằm góc, v.v. Bot chơi ổn, nhưng vẫn có cảm giác… thiếu gì đó: tất cả trọng số đều do ta phán cảm tính. Phần 2 này sẽ cho bot một chút “khả năng tự học”: nó sẽ tự tinh chỉnh các trọng số trong hàm evaluate bằng cách… tự chơi với chính mình (self-play), đo hiệu năng và update dần dần. Chưa phải reinforcement learning full bài bản, nhưng đủ để bước từ “AI cố định” sang “AI biết tự điều chỉnh”.

Bot 2048 biết tự học: Tinh chỉnh hàm đánh giá bằng self-play

1. Vấn đề của việc chỉnh tay hàm đánh giá

Phần 1 ta có dạng hàm:

score(board) =
    w_empty   * emptyCells(board)
  + w_mono    * monotonicity(board)
  + w_smooth  * smoothness(board)
  + w_corner  * maxTileCorner(board)
  + w_max     * maxTile(board)

Vấn đề:

  • Chọn w_* hoàn toàn bằng cảm giác: thử – sai – reload game.
  • Trọng số hợp với depth = 4, có thể dở với depth = 3 hoặc 5.
  • Khi thêm feature mới (ví dụ “penalty cho tile lớn ở giữa”), ta lại phải ngồi chỉnh lại cả bộ.

Nói cách khác, ta đang code như năm 2000: “AI” mà không hề học từ dữ liệu. Bây giờ ta cho nó tự chơi, đo kết quả, rồi tự chỉnh trọng số.

2. Biến hàm evaluate thành vector đặc trưng + vector trọng số

Trước tiên, ta tách rõ featuresweights:

  • Features: các đặc trưng đo đạc từ bàn cờ (empty, smoothness, monotonicity, corner, maxTile).
  • Weights: một vector số thực tương ứng với từng feature.

Ví dụ, ta viết lại evaluate thành:

function extractFeatures(board) {
  const emptyCount   = getEmptyCells(board).length;
  const smoothness   = computeSmoothness(board);   // số âm
  const monotonicity = computeMonotonicity(board); // số dương
  const cornerBonus  = maxTileInCorner(board) ? 1 : 0;
  const maxTile      = getMaxTile(board);

  return {
    emptyCount,
    smoothness,
    monotonicity,
    cornerBonus,
    maxTile,
  };
}

function evaluateWithWeights(board, w) {
  const f = extractFeatures(board);

  return (
    w.wEmpty  * f.emptyCount   +
    w.wSmooth * f.smoothness   +
    w.wMono   * f.monotonicity +
    w.wCorner * f.cornerBonus  +
    w.wMax    * f.maxTile
  );
}

Sau đó, trong expectimax, ta chỉ cần truyền thêm w vào:

function expectimax(board, depth, isPlayerTurn, w) {
  if (depth === 0 || isGameOver(board)) {
    return evaluateWithWeights(board, w);
  }
  // ... phần còn lại giống phần 1, chỉ sửa chỗ gọi evaluate(...)
}

Như vậy, ta có một “mô hình” đơn giản: score = w · f(board) (tích vô hướng của trọng số và vector feature).
Việc “học” bây giờ chính là tìm vector w tốt.

3. Self-play: để bot tự chấm điểm trọng số của mình

Ý tưởng: với mỗi bộ trọng số w, ta cho bot:

  • Chơi vài ván 2048 hoàn chỉnh (hoặc giới hạn số bước).
  • Ghi lại điểm trung bình (hoặc max tile trung bình).
  • Coi đó là “fitness” của bộ trọng số.

Như vậy ta có một hàm fitness:

function evaluateWeights(w) {
  const NUM_GAMES = 3;
  let totalScore = 0;

  for (let g = 0; g < NUM_GAMES; g++) {
    const score = playOneGameWithWeights(w);
    totalScore += score;
  }

  return totalScore / NUM_GAMES;
}

Trong đó playOneGameWithWeights(w):

  • Khởi tạo bàn cờ.
  • Dùng expectimax với cùng bộ w cho đến khi hết nước đi hoặc đạt ngưỡng.
  • Trả về điểm.

Điểm càng cao → trọng số càng “ngon”.

4. Một thuật toán hill-climbing nhẹ để tối ưu trọng số

Ta sẽ dùng một thuật toán cực kỳ đơn giản: stochastic hill-climbing.

Ý tưởng:

  1. Bắt đầu với một bộ trọng số “cảm tính” w_best.
  2. Tính fitness hiện tại fitness_best = evaluateWeights(w_best).
  3. Lặp lại nhiều lần:
    • Nhân nhẹ mỗi trọng số với 1 + noise ngẫu nhiên:
      w_candidate[i] = w_best[i] * (1 + uniform(-0.2, 0.2)).
    • Tính fitness_candidate.
    • Nếu fitness_candidate > fitness_best thì:
      • w_best = w_candidate
      • fitness_best = fitness_candidate

Pseudocode JS:

let bestWeights = {
  wEmpty: 350,
  wSmooth: 3,
  wMono: 10,
  wCorner: 300,
  wMax: 1,
};

let bestFitness = -Infinity;

function mutateWeights(w) {
  const factor = () => 1 + (Math.random() * 0.4 - 0.2); // [-0.2, +0.2]
  return {
    wEmpty:  w.wEmpty  * factor(),
    wSmooth: w.wSmooth * factor(),
    wMono:   w.wMono   * factor(),
    wCorner: w.wCorner * factor(),
    wMax:    w.wMax    * factor(),
  };
}

function trainOneStep() {
  const candidate = mutateWeights(bestWeights);
  const fitness   = evaluateWeights(candidate); // self-play vài ván

  if (fitness > bestFitness) {
    bestFitness = fitness;
    bestWeights = candidate;
    console.log("New best fitness =", fitness, "weights =", bestWeights);
  }
}

Đây chưa phải gradient descent, chưa phải RL chính thống, nhưng:

  • Dễ hiểu, dễ triển khai trong trình duyệt.
  • Có thể chạy trực tiếp trong bài viết, cho bot tự “trâu” vài chục ván rồi update kết quả.

5. Demo: Bot 2048 tự tinh chỉnh trọng số (self-play mini trainer)

Demo dưới đây gồm 2 phần:

  • Một board 2048 cho bạn xem bot chơi với trọng số hiện tại.
  • Một trainer nhỏ:
    • Nút “Train 10 bước (self-play)”: bot tự chơi nhiều ván, thử nghiệm trọng số mới, nếu tốt hơn thì cập nhật.
    • Hiển thị bộ trọng số hiện tại và “fitness” ước lượng.

Bạn có thể:

  • Ấn “Auto-play với weights hiện tại” để bot chơi một ván với trọng số mới nhất.
  • Ấn “Train 10 bước” vài lần, xem trọng số drift dần và bot có chơi khôn hơn không.

2048 Self-Learning AI Demo (Phần 2)

Expectimax + heuristic linear, weights được tinh chỉnh bằng self-play hill-climbing

Điểm hiện tại: 0 | Max tile: 0 | Trạng thái:

Weights hiện tại


Estimated fitness (avg score ước lượng):

6. Hạn chế & chuẩn bị cho phần 3

Cái ta vừa làm vẫn còn khá “thô”:

  • Hàm đánh giá vẫn là tuyến tính trên vài feature thủ công.
  • Chiến lược học chỉ là hill-climbing ngẫu nhiên, không dùng gradient, không có khái niệm “trạng thái – hành động – reward” rõ ràng như RL chuẩn.
  • Việc đánh giá fitness tốn khá nhiều thời gian (chơi nhiều ván), khó scale nếu depth cao.

Nhưng bù lại, có vài điểm rất thực tế:

  • Dễ cài ngay trong trình duyệt, không cần backend.
  • Trực quan: nhìn trọng số thay đổi, nhìn bot chơi tốt lên hoặc tệ đi.
  • Là bước đệm rất hợp lý trước khi sang phần 3 với các kỹ thuật “xịn sò” hơn:
    • Dùng mạng nơ-ron để học evaluation function.
    • TD-learning / Q-learning / các biến thể RL cho 2048.
    • MCTS kết hợp hàm value học được (kiểu AlphaZero phiên bản mini).

Ở phần 3, ta sẽ nâng cấp từ “tuning weights” sang “học hẳn một hàm value phi tuyến”, để bot 2048 tiến gần hơn phong cách các game AI hiện đại.

Bình luận


  • Không có bình luận.

Init Toolbox

Nhấn Ctrl + \ trên máy tính, hoặc vuốt sang trái ở bất kỳ đâu trên mobile.

Đăng nhập





Đang tải...