Phần lớn web app coi màn hình như một canvas vẽ lại vô hạn lần — animate, transition, redraw 60 lần/giây, không ai để ý. Màn hình e-ink phá vỡ hoàn toàn giả định đó. Một lần refresh toàn màn hình mất vài trăm mili-giây và để lại một cú chớp rõ ràng; một lần refresh một phần nếu không xử lý cẩn thận sẽ để lại “ghosting” — vệt mờ của khung hình trước còn sót lại trên khung hình sau. EinkChess, một app cờ vua mã nguồn mở được thiết kế riêng cho Kindle và các máy đọc sách e-ink, là một case study hay về việc thiết kế phần mềm xoay quanh một ràng buộc phần cứng mà phần lớn chúng ta chưa bao giờ nghĩ tới. Mình đã lấy source, review kiến trúc, host lên một server nhỏ, và muốn đi qua những phần mình thấy thú vị nhất: engine, AI, và bộ luyện puzzle.

Không backend, và đó chính là điểm hay

EinkChess là một app thuần client-side: HTML, CSS, JavaScript, không có trang render phía server, không gọi API nào trong lúc chơi. Toàn bộ chess engine, đối thủ AI, và logic ghép puzzle chạy ngay trong tab trình duyệt. Cuộc gọi mạng duy nhất trong cả app là một beacon telemetry tùy chọn, rất nhỏ (vài trăm byte) để đếm người dùng đang hoạt động — và ngay cả cuộc gọi đó cũng lặng lẽ trở thành no-op nếu endpoint chưa được cấu hình. Mọi thứ người chơi thực sự tương tác — kiểm tra nước đi hợp lệ, phát hiện chiếu tướng, chọn nước đi của bot, điều chỉnh độ khó puzzle — đều chạy hoàn toàn offline, trong bộ nhớ, ngay trên thiết bị.

Đây không phải chi tiết triển khai nhỏ nhặt; đó là kiến trúc đúng cho thiết bị mục tiêu. Trình duyệt trên Kindle là một tính năng phụ, kết nối mạng không thường xuyên, gắn thêm vào một máy đọc sách — bạn không muốn một ván cờ bị chặn vì độ trễ hay tình trạng kết nối mạng. Một khi tài nguyên tĩnh đã được cache, cả app chạy được cả trên máy bay.

Bàn cờ: một mảng 8x8 đơn giản, và vì sao đó là lựa chọn đúng

Engine biểu diễn bàn cờ bằng một mảng “mailbox” phẳng — mỗi ô một phần tử, không dùng bitboard, không dùng 0x88. Với một chess engine nhắm tới trình duyệt WebKit đời cũ trên Kindle, đây chính là đánh đổi hợp lý. Bitboard engine lấy tốc độ từ các phép toán số nguyên 64-bit và các thủ thuật bit mà JIT có thể vector hóa; một trình duyệt nhúng đời cũ chỉ chạy JavaScript ES5 sẽ không tưởng thưởng cho độ phức tạp đó, và cái giá phải trả cho khả năng đọc hiểu code không đáng để đổi lấy cho một bot chỉ đạt tối đa khoảng 1600 ELO. Engine theo dõi vị trí bằng FEN (Forsyth-Edwards Notation) để import/export, sinh ra các nước đi “pseudo-legal” cho từng quân, rồi lọc xuống thành nước đi hợp lệ bằng cách mô phỏng từng nước ứng viên và kiểm tra xem nó có để vua bị chiếu hay không — cách tiếp cận chuẩn mực, đơn giản, và hoàn toàn đủ dùng ở quy mô này.

Lịch sử nước đi không chỉ để undo. Engine giữ một bản ghi liên tục các “position key” riêng để phát hiện lặp lại vị trí ba lần (threefold repetition), cùng với các kiểm tra riêng cho luật 50 nước và trường hợp không đủ quân để chiếu hết — ba điều kiện hòa “nhàm chán” mà người viết engine rất dễ quên khi đang mải tập trung làm cho việc phát hiện chiếu hết hoạt động đúng, nhưng một đối thủ thực sự chắc chắn sẽ tìm cách khai thác.

AI: minimax, alpha-beta, và một giới hạn trần rất có chủ đích

Bot dùng minimax search kèm cắt tỉa alpha-beta theo đúng sách giáo khoa, và có thêm quiescence search ở các nút lá — mở rộng độ sâu tìm kiếm thêm khi vị trí đang “ồn ào” (có một nước ăn quân khả dụng) để engine không đánh giá sai một vị trí chỉ vì dừng tìm kiếm giữa chừng một cuộc đổi quân. Đây là cách khắc phục kinh điển cho “horizon effect”: nếu không có nó, một tìm kiếm độ sâu cố định có thể để mất một quân ngay sau điểm dừng của tầm nhìn, mà vẫn tự tin cho rằng đó là nước đi tốt.

Điều thú vị hơn cả lựa chọn thuật toán là cách hiệu chỉnh độ khó. Năm cấp độ khó tương ứng khoảng 800 đến 1600 ELO, được tinh chỉnh chủ yếu qua độ sâu tìm kiếm và trọng số đánh giá vị trí, chứ không phải bằng cách cố tình chèn thêm nước đi sai. Đây là một quyết định thiết kế khác hẳn so với “thêm nhiễu ngẫu nhiên để dễ thắng hơn” — nó giữ cho sai lầm của bot trông giống sai lầm của một người chơi yếu hơn thật (đánh giá sai giữa đổi quân về vật chất và đổi quân về thế trận, bỏ lỡ một chiến thuật sâu hơn) thay vì trông như nhiễu ngẫu nhiên vô nghĩa — điều này quan trọng nếu app muốn định vị mình là một đối tác luyện tập thật sự chứ không phải một món đồ chơi.

Bộ puzzle: ghép cặp độ khó với chính rating của bạn

Phần có kiến trúc thú vị nhất không phải đối thủ AI — mà là chế độ puzzle. Nó được lấy dữ liệu từ Lichess Open Database, chia thành 31 khoảng ELO từ 400 đến 3400, và chạy một hệ thống ELO tỷ lệ (proportional ELO) riêng, độc lập với rating chơi cờ thường: giải được một puzzle, rating puzzle của bạn tăng theo tỷ lệ với khoảng cách giữa rating của bạn và độ khó của puzzle; giải sai thì giảm theo cùng cách đó. Đây chính là kiểu cập nhật rating mà các bộ luyện puzzle thật sự dùng (cả Lichess và chess.com đều dùng biến thể của cách này), và nó là thứ khiến “độ khó thích ứng” có ý nghĩa hơn một thanh trượt độ khó đơn thuần — puzzle tiếp theo được chọn để nằm đúng ở ranh giới khả năng giải hiện tại của bạn.

Chi tiết chống gian lận đáng nhắc tới: trạng thái puzzle đang làm dở được lưu lại, nên việc reload trang không cho phép bạn thử lại một puzzle đang bí với trạng thái sạch từ đầu, và việc bỏ qua một puzzle sẽ tốn rating chứ không miễn phí. Một cơ chế nhỏ, nhưng đó là khác biệt giữa một con số rating phản ánh đúng trình độ thật của bạn và một con số phản ánh việc bạn sẵn sàng refresh trang bao nhiêu lần.

Thiết kế cho một màn hình “chống trả”

Hướng dẫn đóng góp của dự án nói rất rõ ràng về các ràng buộc e-ink, và điều đó định hình gần như mọi quyết định giao diện: không có transition hay animation opacity nào trong CSS, vì chúng gây ra hiện tượng chớp nháy rõ rệt trên màn hình có giới hạn refresh; ô vừa đi nước cuối được đánh dấu bằng viền nét đứt thay vì tô nền màu đặc, vì tô nền là vẽ lại toàn vùng còn viền chỉ là vẽ lại một dải mỏng; một layout “zero-scroll” có chủ đích, nơi bàn cờ, header và các nút điều khiển đều vừa trong một viewport, vì bản thân việc cuộn trang buộc phải vẽ lại theo tầng trên e-ink; và các cập nhật DOM chỉ vá đúng những ô thực sự thay đổi sau mỗi nước đi thay vì render lại toàn bộ bàn cờ.

Ngoài ra còn có một giới hạn ES5 cứng — không const, không arrow function, không async/await, không WebAssembly — vì trình duyệt mục tiêu là một bản WebKit nhúng đời cũ không hỗ trợ bất kỳ thứ nào trong số đó. Đây là một lời nhắc hữu ích rằng “trình duyệt” không phải một mục tiêu duy nhất; nếu người dùng thực tế của bạn đang chạy trên một runtime nhúng năm năm tuổi, những giả định kiểu “trình duyệt hiện đại” sẽ âm thầm biến thành bug.

Triển khai lên hosting

Việc host cái này khá đơn giản, và đó thực ra là hệ quả trực tiếp của quyết định kiến trúc ở trên: vì không có backend, không cần cấp phát database, npm run build tạo ra một thư mục dist/ tĩnh, và toàn bộ việc triển khai chỉ là “đưa một thư mục file cho một web server”. Mình chạy nó trong một container cô lập nhỏ để không cạnh tranh tài nguyên với bất kỳ thứ gì khác, trỏ nginx vào thư mục build, và đó là toàn bộ quá trình deploy — không cần process manager, không cần runtime, không cần môi trường nào ngoài một nơi để serve file tĩnh.

Rút ra được gì

Không có mảnh ghép riêng lẻ nào ở đây — bàn cờ dạng mailbox, minimax alpha-beta, ELO tỷ lệ — là thuật toán mới cả. Điều khiến EinkChess đáng đọc source là mỗi lựa chọn tiêu chuẩn đó đều được lọc qua đúng một ràng buộc cứng (một màn hình trừng phạt mỗi lần vẽ lại) và ra đời với hình dạng khác hẳn so với khi bạn viết cùng một app cho trình duyệt bình thường: cấu trúc dữ liệu đơn giản hơn vì runtime không tưởng thưởng cho độ phức tạp, giao diện không animation vì animation chính là bug trên phần cứng này, và một câu chuyện triển khai đơn giản đến mức nhàm chán — theo nghĩa tốt nhất. Đây là một lời nhắc hay rằng “hiểu rõ thiết bị mục tiêu” không phải một ô cần tick — đó là một quyết định kiến trúc cần thể hiện trong cấu trúc dữ liệu của bạn, chứ không chỉ trong media query của CSS.

Xuất nội dung

Bình luận