Giới thiệu

Tic Tac Toe là một trò chơi khá phổ biến dùng viết trên bàn cờ giấy có 9 ô. Hai người chơi, một người dùng ký hiệu O, người kia dùng ký hiệu X, lần lượt điền ký hiệu của mình vào các ô. Người thắng cuộc là người đầu tiên tạo được một dãy 3 ký hiệu của mình theo các chiều ngang, dọc hay chéo đều được. Nếu sau khi đã lấp đầy các ô trống mà vẫn không có ai đạt được một dãy 3 ô thẳng hàng thì sẽ là hòa. Hình sau mô tả 3 trường hợp ví dụ:


(a) Người chơi X thắng           (b) Cờ hòa                             (c) Người chơi O thắng
Bài viết này sẽ giới thiệu với các bạn giải pháp để thiết kế và triển khai trò chơi này trong Java. Đây là giải pháp đã được giới thiệu bởi Daniel Liang trong cuốn “Introduction to Java Programming“.

Phân tích và thiết kế

Những ví dụ mà bạn vừa nhìn thấy ở trên chỉ thể hiện những hành vi đơn giản và có thể dễ dàng mô hình hóa bằng các lớp, nhưng ngoài ra thì hành vi của trò chơi Tic Tac Toe còn có một số điểm khác phức tạp hơn thế. Để có thể tạo ra được các lớp mô phỏng các hành vi này, chúng ta sẽ cần phải nghiên cứu và hiểu kỹ hơn về trò chơi này.
Giả sử rằng ban đầu thì tất cả các ô (cell) đều trống, và người chơi thứ nhất dùng ký hiệu X, người chơi thứ hai dùng ký hiệu O. Để thực hiện một bước đi thì người chơi sẽ trỏ chuột đến một ô và nhấn chuột vào đó, nếu ô đó còn trống thì ký hiệu O (hoặc X) sẽ được điền vào đó, nếu ô đó không còn trống thì nước đi này sẽ không được ghi nhận.
Với những mô tả như trên, ta thấy rằng mỗi ô cờ là một đối tượng GUI (Graphic User Interface – Giao diện Đồ họa Người dùng) mà có thể xử lý sự kiện nhấn chuột và hiển thị các ký hiệu. Đối tượng này có thể là một nút (button) hoặc là một bảng (panel). Việc vẽ trên một panel sẽ là linh hoạt hơn nhiều so với việc vẽ trên một button, bởi vì trên một panel thì ta có thể vẽ các ký hiện XO với bất kỳ kích thước nào mà ta mong muốn, còn trên một button thì ta chỉ có thể hiển thị các ký hiệu đó như là một nhãn ký tự (text label). Vì lí do đó chúng ta sẽ sử dụng một panel để mô phỏng một ô của bàn cờ. Làm thế nào chúng ta có thể biết được trạng thái của mỗi ô (trống, X, hay O)? Trong lớp Cell chúng ta sẽ sử dụng một thuộc tính có tên token với kiểu dữ liệu là char. Lớp Cell có nhiệm vụ vẽ ký hiệu khi một ô trống được nhấn chuột. Vì vậy chúng ta cần phải viết một đoạn mã để lắng nghe sự kiện MouseEvent và một đoạn mã khác để vẽ các ký hiệu X hoặc O. Lớp Cell này có thể được định nghĩa như hình sau:

Lớp Cell vẽ ký hiệu trong một ô

token: Ký hiệu được dùng trong các ô, mặc định là ”
getToken(): Trả về ký hiệu hiện tại của ô.
setToken(token: char): Nhập ký hiệu mới cho ô.
paintComponent(g: Graphics): Vẽ ký hiệu trong ô.
mouseClicked(e: MouseEvent): Xử lý sự kiện nhấn chuột của ô.

Bàn cờ của trò chơi Tic Tac Toe bao gồm 9 ô, được tạo ra bằng câu lệnh new Cell[3][3]. Để xác định được lượt chơi của người chơi thì chúng ta sẽ sử dụng một biến có tên là whoseTurn với kiểu dữ liệu là char. Ban đầu thì whoseTurn sẽ có giá trị là ‘X’, sau đó chuyển thành ‘O’ và cứ thay đổi lần lượt giữa hai giá trị đó mỗi khi các ô mới được đánh. Khi trò chơi kết thúc thì giá trị của whoseTurn sẽ chuyển thành ‘ ‘.
Làm sao để biết được trò chơi đã kết thúc hay chưa? Có ai thắng cuộc hay không? Và ai là người thắng, nếu có? Chúng ta có thể tạo một phương thức có tên là isWon(char token) để kiểm tra xem một người chơi với ký hiệu token đã thắng cuộc hay chưa, và một phương thức khác là isFull() để kiểm tra xem liệu tất cả các ô đều đã được đánh hay chưa.
Với những phân tích như vậy, chúng ta thấy lộ rõ ra 2 lớp. Lớp đầu tiên là Cell, với chức năng là điều khiển các thao tác cho một ô. Lớp thứ hai là TicTacToe có chức năng là thực hiện toàn bộ trò chơi và xử lý tất cả các ô. Mối quan hệ giữa hai lớp này được thể hiện trong hình dưới đây:

Lớp TicTacToe chứa 9 ô

whoseTurn: Chỉ ra lượt chơi của người chơi, ban đầu là X.
cell: Một mảng 2 chiều 3 x 3 các ô.
jlblStatus: Một nhãn (label) để hiển thị trạng thái của game.
TicTacToe(): Khởi tạo giao diện người dùng.
isFull(): Trả về giá trị true nếu tất cả các ô đều đã được đánh.
isWon(token: char): Trả về giá trị true nếu người chơi có ký hiệu token đã thắng.

Với lí do là lớp Cell chỉ được dùng trong lớp TicTacToe nên nó có thể được định nghĩa bên trong lớp TicTacToe (inner class). Chi tiết của chương trình được thể hiện bên dưới:

[sourcecode lang=”java”]

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.LineBorder;

public class TicTacToe extends JApplet {
// Chỉ ra lượt chơi của người chơi, giá trị ban đầu là X
private char whoseTurn = ‘X’;

// Khởi tạo các ô
private Cell[][] cells = new Cell[3][3];

// Khởi tạo label trạng thái của trò chơi
private JLabel jlblStatus = new JLabel("X’s turn to play");

/** Initialize UI */
public TicTacToe() {
// Panel p chứa các ô
JPanel p = new JPanel(new GridLayout(3, 3, 0, 0));
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
p.add(cells[i][j] = new Cell());

// Thay đổi đường viền của các ô và label trạng thái
p.setBorder(new LineBorder(Color.red, 1));
jlblStatus.setBorder(new LineBorder(Color.yellow, 1));

// Đưa panel và label vào trong applet
add(p, BorderLayout.CENTER);
add(jlblStatus, BorderLayout.SOUTH);
}

/**
Xác định xem liệu tất cả các ô đều đã được đánh hay chưa
*/
public boolean isFull() {
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
if (cells[i][j].getToken() == ‘ ‘)
return false;

return true;
}

/**
Xác định xem liệu người chơi với ký hiện token đã thắng hay chưa
*/
public boolean isWon(char token) {
for (int i = 0; i < 3; i++)
if ((cells[i][0].getToken() == token)
&& (cells[i][1].getToken() == token)
&& (cells[i][2].getToken() == token)) {
return true;
}

for (int j = 0; j < 3; j++)
if ((cells[0][j].getToken() == token)
&& (cells[1][j].getToken() == token)
&& (cells[2][j].getToken() == token)) {
return true;
}

if ((cells[0][0].getToken() == token)
&& (cells[1][1].getToken() == token)
&& (cells[2][2].getToken() == token)) {
return true;
}

if ((cells[0][2].getToken() == token)
&& (cells[1][1].getToken() == token)
&& (cells[2][0].getToken() == token)) {
return true;
}

return false;
}

// Một inner class đại diện cho một ô
public class Cell extends JPanel {
// Ký hiệu của ô này
private char token = ‘ ‘;

public Cell() {
setBorder(new LineBorder(Color.black, 1)); // Thay đổi đường viền của ô
addMouseListener(new MyMouseListener()); // Đăng ký listener
}

/**
Trả về ký hiệu của ô
*/
public char getToken() {
return token;
}

/**
Nhập một ký hiệu mới cho ô
*/
public void setToken(char c) {
token = c;
repaint();
}

/**
Vẽ ô
*/
protected void paintComponent(Graphics g) {
super.paintComponent(g);

if (token == ‘X’) {
g.drawLine(10, 10, getWidth() – 10, getHeight() – 10);
g.drawLine(getWidth() – 10, 10, 10, getHeight() – 10);
}
else if (token == ‘O’) {
g.drawOval(10, 10, getWidth() – 20, getHeight() – 20);
}
}

private class MyMouseListener extends MouseAdapter {
/** Xử lý sự kiện nhấn chuột trong một ô */
public void mouseClicked(MouseEvent e) {
// Nếu một ô còn trống và trò chơi vẫn chưa kết thúc
if (token == ‘ ‘ && whoseTurn != ‘ ‘) {
setToken(whoseTurn); // Thay đổi ký hiệu cho ô

// Kiểm tra trạng thái của trò chơi
if (isWon(whoseTurn)) {
jlblStatus.setText(whoseTurn + " won! The game is over");
whoseTurn = ‘ ‘; // Trò chơi kết thúc
}
else if (isFull()) {
jlblStatus.setText("Draw! The game is over");
whoseTurn = ‘ ‘; // Trò chơi kết thúc
}
else {
// Thay đổi lượt chơi
whoseTurn = (whoseTurn == ‘X’) ? ‘O’: ‘X’;
// Hiển thị lượt chơi
jlblStatus.setText(whoseTurn + "’s turn");
}
}
}
}
}

/**
Phương thức main() cho phép applet này chạy như là một ứng dụng
*/
public static void main(String[] args) {
// Tạo một frame
JFrame frame = new JFrame("TicTacToe");

// Tạo một thể hiện của applet
TicTacToe applet = new TicTacToe();

// Thêm thể hiện của applet vào trong frame
frame.add(applet, BorderLayout.CENTER);

// Hiển thị frame
frame.setSize(300, 300);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
[/sourcecode]

Kết luận

Lớp TicTacToe khởi tạo giao diện người dùng với 9 ô đặt trong một panel với GridLayout. Một label có tên là jlblStatus được sử dụng để hiển thị trạng thái của trò chơi. Biến whoeTurn được sử dụng để lưu ký hiệu tiếp theo sẽ được gán cho các ô. Phương thức isFull() và isWon() được sử dụng để kiểm tra trạng thái của trò chơi.

Bởi vì lớp Cell là inner class của lớp TicTacToe nên biến whoseTurn và các phương thức isFull() isWon() của lớp TicTacToe đều có thể sử dụng được trong lớp Cell. Inner class giúp cho chương trình trở nên đơn giản và ngắn gọn hơn. Nếu lớp Cell không được khai báo là inner class của lớp TicTacToe thì chúng ta cần phải truyền một đối tượng của TicTacToe vào trong lớp Cell để lớp này có thể sử dụng được các biến và phương thức của lớp TicTacToe. Bạn có thể thử viết lại chương trình này mà không sử dụng inner class.
Sự kiện MouseEvent được đăng ký cho từng ô, nếu một ô trống được nhấn chuột trong khi trò chơi vẫn chưa kết thúc thì ký hiệu hiện tại sẽ được đặt cho ô đó. Nếu trò chơi kết thúc, biến whoseTurn sẽ được gán giá trị ‘ ‘, còn nếu không thì whoseTurn sẽ chuyển sang giá trị của lượt chơi mới.

Mẹo:

Hãy sử dụng phương pháp tiếp cận tăng cường (incremental approach) trong việc phát triển và kiểm thử một dự án Java thuộc loại này. Chương trình ở trên có thể được chia thành 5 bước như sau:

  1. Dựng giao diện người dùng và hiển thị ký hiệu X trong một ô cố định.
  2. Tạo cho các ô khả năng hiển thị ký hiệu X mỗi khi một ô nào đó được nhấn chuột.
  3. Kết hợp giữa hai người chơi sao cho các ký hiệu XO được hiển thị thay phiên nhau.
  4. Kiểm tra xem một người chơi đã thắng hay chưa, hoặc là tất cả các ô đều đã được đánh.
  5. Hiển thị thông báo trên label trạng thái sau mỗi nước đi của người chơi.