Chương 16: GridWorld, phần 3

Trở về Mục lục cuốn sách

Nếu bạn chưa làm bài tập trong các Chương 5 và 10, bạn hãy nên làm đi trước khi đọc chương này. Xin được nhắc lại, bạn có thể tìm tài liệu về các lớp GridWorld ở http://www.greenteapress.com/thinkapjava/javadoc/gridworld/.

Phần 3 của cuốn Hướng dẫn sinh viên về GridWorld trình bày những lớp cấu thành GridWorld và các mối tương tác giữa chúng. Đây là một ví dụ về thiết kế hương đối tượng và làm một cơ hổi để ta bàn luận những vấn đề thiết kế hướng đối tượng.

Nhưng trước khi bạn đọc cuốn Hướng dẫn sinh viên, sau đây có thêm một số điều mà bạn cần biết.

16.1  ArrayList

GridWorld sử dụng java.util.ArrayList, một đối tượng gần giống với mảng. Đó là một tập hợp, tức là đối tượng để chứa những đối tượng khác. Java cung cấp những tập hợp khác với nhiều tính năng khác nhau, nhưng để dùng GridWorld ta chỉ cần đến các ArrayList.

Để thấy một ví dụ, hãy tải về http://thinkapjava.com/code/BlueBug.java và http://thinkapjava.com/code/BlueBugRunner.javaBlueBug là một con bọ di chuyển ngẫu nhiên và đi tìm các tảng đá. Nếu nó thấy một tảng đá, con bọ sẽ làm tảng đá hóa màu xanh.

Sau đây là cách hoạt động của BlueBug. Khi act được kích hoạt, BlueBug lấy vị trí của nó cùng một tham chiếu đến lưới:

    Location loc = getLocation(); 
    Grid<Actor> grid = getGrid();

Kiểu dữ liệu nằm trong cặp ngoặc góc (<>) là một tham số kiểu để quy định nội dung của grid. Nói cách khác, grid không chỉ là một Grid, mà nó là Grid có chứa những Actor.

Bước tiếp theo là thu lấy những vị trí lân cận với chỗ hiện tại. Grid cung cấp một phương thức chỉ để làm việc này:

    ArrayList<Actor> neighbors = grid.getNeighbors(loc);

Kết quả trả lại từ getNeighbors là một ArrayList gồm các Actor. Phương thức size trả lại chiều dài của ArrayList, và get thì chọn lấy một phần tử. Bởi vậy ta có thể in ra những vị trí lân cận như sau.

    for (int i = 0; i < neighbors.size(); i++) { 
      Actor actor = neighbors.get(i); 
      System.out.println(actor); 
    }

Việc duyệt một ArrayList là thao tác thông dụng đến nỗi có một cú pháp đặc biệt dành cho nó: vòng lặp for-each. Bởi vậy ta có thể viết:

    for (Actor actor : neighbors) { 
      System.out.println(actor); 
    }

Ta biết rằng các lân cận đều là những Actor, song lại không biết kiểu của chúng là gì: từng lân cận có thể là BugRock, v.v. Để tìm tảng đá (Rock), ta sử dụng toán tử instanceof, vốn để kiểm tra xem liệu một đối tượng có là thực thể của một lớp hay không.

    for (Actor actor : neighbors) { 
      if (actor instanceof Rock) { 
        actor.setColor(Color.blue); 
      } 
    }

Để làm cho toàn bộ hoạt động được, ta cần phải nhập những lớp cần dùng đến:

import info.gridworld.actor.Actor; 
import info.gridworld.actor.Bug; 
import info.gridworld.actor.Rock; 
import info.gridworld.grid.Grid; 
import info.gridworld.grid.Location; 
import java.awt.Color; 
import java.util.ArrayList;

16.2  Giao diện

GridWorld cũng sử dụng các giao diện của Java, bởi vậy tôi muốn giải thích ý nghĩa của chúng. “Giao diện” có nhiều nghĩa trong những ngữ cảnh khác nhau, nhưng trong Java, thuật ngữ này dùng để chỉ một đặc điểm cụ thể của ngôn ngữ: giao diện là một lời định nghĩa lớp mà trong đó các phương thức không có phần thân.

Trong lời định nghĩa lớp thông thường, mỗi phương thức có một nguyên mẫu và một phần thân (xem Mục 8.5). Nguyễn mẫu còn được gọi là phần quy định bởi nó quy định tên, các thông số, và kiểu trả lại của phương thức đó. Phần thân được gọi là phần thực hiện bởi nó thực hiện phần quy định trên.

Trong một giao diện Java, các phương thức không có phần thân, bởi vậy giao diện chỉ quy định các phương thức mà không thực hiện chúng.

Chẳng hạn, java.awt.Shape là một giao diện với các nguyên mẫu cho containsintersects, cùng một số phương thức khác. java.awt.Rectangle cung cấp phần thực hiện của những phương thức này, bởi vậy ta nói rằng “Rectangle thực hiện Shape.” Thực ra, dòng đầu tiên của lời định nghĩa lớp Rectangle là:

public class Rectangle extends Rectangle2D implements Shape, Serializable

Rectangle thừa kế các phương thức từ Rectangle2D và cung cấp phần thực hiện cho các phương thức trong Shape và Serializable.

Trong GridWorld, lớp Location thực hiện giao diện java.lang.Comparable bằng cách cung cấp compareTo, vốn tương tự với compareCards ở Mục 13.5. GridWorld cũng định nghĩa một giao diện mới, Grid, để quy định các phương thức mà một Grid cần phải cung cấp. Đồng thời, GridWorld cũng bao gồm hai phần thực hiện, BoundedGrid và UnboundedGrid.

Quyển hướng dẫn có dùng chữ viết tắt API, mà chữ đầy đủ là “application programming interface” (giao diện lập trình ứng dụng). API là một tập hợp các phương thwusc dành sẵn cho bạn, người lập trình ứng dụng, để sử dụng. Hãy xem http://en.wikipedia.org/wiki/Application_programming_interface.

16.3  public và private

Hãy nhớ lại từ Chương 1, tôi đã nói rằng tôi sẽ giải thích tại sao phương thức main lại có từ khóa public chứ? Rốt cuộc, đã đến lúc cần giải thích rồi.

public nghĩa là phương thức được xét có thể được kích hoạt từ những phương thức khác. Lựa chọn còn lại là private, có nghĩa là phương thức đang xét chỉ có thể kích hoạt được trong lớp mà nó được định nghĩa.

Các biến thực thể cũng có thể là public hoặc private, với kết quả tương tự: một biến thực thể private chỉ có thể truy cập được từ bên trong lớp mà nó được định nghĩa.

Lý do cơ bản cho việc đặt những phương thức và biến thực thể dưới dạng private là nhằm hạn chế sự tương tác giữa các lớp để có thể giữ mức độ phức tạp ở mức chấp nhận được.

Chẳng hạn, lớp Location giữ các biến thực thể dưới dạng private. Nó có các phương thức truy cập getRow là getCol, nhưng lại không cung cấp phương thức nào để sửa đổi các biến thực thể. Hệ quả là, các đối tượng Location đều không thể biến đổi, theo nghĩa rằng chúng đều có thể được chia sẻ mà ta không lo chúng bộc lộ động thái không mong đợi do xuất hiện bí danh (alias).

Việc đặt các phương thức dưới dạng private giúp ta giữ cho API được đơn giản. Các lớp thường kèm theo những phương thức trợ giúp vốn được dùng để thực hiện các phương thức khác, song nếu để cho những phương thức này tham gia vào trong API public có thể sẽ không cần thiết và dễ gây lỗi.

Các phương thức và biến thực thể private là đặc điểm ngôn ngữ giúp cho lập trình viên đảm bảo được sự bao bọc dữ liệu, theo nghĩa là các đối tượng thuộc lớp này thì được cô lập khỏi những lớp khác.

16.4  Trò chơi Life

Nhà toán học John Conway đã phát minh ra “Trò chơi Life,” mà ông gọi là một “trò chơi không người” vì chẳng cần có người chơi để lựa chọn chiến thuật hay ra quyết định. Sau khi thiết lập điều kiện ban đầu, bạn chỉ việc xem trò chơi tự nó phát triển. Nhưng điều này hóa ra còn hay hơn so với thoạt nghe; bạn có thể đọc thêm ở http://en.wikipedia.org/wiki/Conways_Game_of_Life.

Mục đích của bài tập này là thực hiện trò chơi Life trong GridWorld. “Bàn cờ” chính là là lưới ô, và những “quân cờ” chính là đối tượng Rock (viên đá).

Trò chơi được tiến hành theo từng lượt, hay từng bước thời gian. Ở lúc bắt đầu một bước thời gian, từng viên đá có trạng thái “sống” hoặc “chết”. Trên màn hình, màu sắc của viên đá này thể hiện trạng thái của nó. Trạng thái của từng viên đá lại phụ thuộc vào trạng thái của những viên lân cận với nó. Mỗi viên đá có 8 viên lân cận, trừ những viên nằm dọc theo cạnh của lưới ô. Sau đây là luật chơi:

  • Nếu một viên đá chết có đúng 3 viên lân cận, thì nó sẽ sống lại! Nếu không, thì nó vẫn chết.
  • Nếu một viên đá sống có 2 hoặc 3 viên lân cận, thì nó vẫn sống. Còn không thì nó chết đi.

Từ quy tắc này sẽ có một vài hệ quả: Nếu tất cả viên đá đều chết rồi, thì chẳng có viên nào sống lại. Nếu lúc đầu bạn có mỗi một viên đá sống, thì nó sẽ chết đi. Nhưng nếu có 4 viên cạnh nhau xếp thành hình vuông thì chúng sẽ giữ cho nhau còn sống, bởi vậy đây là một cấu trúc bền vững.

Đa số các cấu hình đơn giản lúc đầu sẽ nhanh chóng chế đi, hoặc đạt đến một cấu hình ổn định. Song cũng có một ít điều kiện ban đầu cho thấy độ phức tạp đáng kể. Một trong những điều kiện đầu như vậy là r-pentomino: bắt đầu chỉ với 5 viên đá, cấu hình này chạy suốt 1103 bước thời gian rồi kết thúc ở một cấu hình bền vững với 116 viên đá sống (xem http://www.conwaylife.com/wiki/R-pentomino).

Các mục tiếp sau đây là những gợi ý để thực hiện trò chơi Life trong GridWorld. Bạn có thể tải về lời giải của tôi tại http://thinkapjava.com/code/LifeRunner.java và http://thinkapjava.com/code/LifeRock.java.

16.5  LifeRunner

Hãy sao lại một bản file BugRunner.java rồi đặt tên thành LifeRunner.java, sau đó bổ sung những phương thức với nguyên mẫu như sau:

  /** 
   * Lập nên một lưới ô cho trò chơi Life, cùng cấu hình r-pentomino. */ 
  public static void makeLifeWorld(int rows, int cols) 

  /** 
   * Xếp các viên đá LifeRock lên lưới ô. */ 
  public static void makeRocks(ActorWorld world)

Phương thức makeLifeWorld cần phải tạo nên một Grid chứa các Actor cùng một ActorWorld, sau đó kích hoạt makeRocks, đến lượt phương thức này sẽ phải đặt một LifeRock vào mỗi ô trong Grid.

16.6  LifeRock

Hãy sao lại một bản của file BoxBug.java rồi đặt tên là LifeRock.java. Lớp LifeRock phải mở rộng từ Rock. Hãy bổ sung thêm một phương thức act chẳng để làm gì cả. Bây giờ mã lệnh phải chạy thông được và bạn sẽ thấy một Grid chứa đầy Rock.

Để theo dõi trạng thái của những viên đá này, bạn có thể bổ sung một biến thực thể mới, hoặc bạn có thể dùng màu sắc (Color) của Rock để biểu thị trạng thái. Bằng cách nào đi nữa, hay viết những phương thức có các nguyên mẫu sau đây:

  /** 
   * Trả về true nếu viên đá còn sống. */ 
  public boolean isAlive() 
  
  /** 
   * Làm cho viên đá sống lại. */ 
  public void setAlive() 

  /** 
   * Làm viên đá chết đi. */ 
  public void setDead()

Hãy viết một constructor để kích hoạt setDead rồi khẳng định chắc rằng tất cả viên đá đều chết.

16.7  Cập nhật đồng thời

Trong trò chơi Life, tất cả viên đá đều được cập nhật một cách đồng thời; nghĩa là từng viên đá đều kiểm tra trạng thái của các viên lân cận trước khi những viên lân cận này thay đổi trạng thái. Nếu không, động thái của hệ thống sẽ còn phụ thuộc vào thứ tự của phép cập nhật nữa.

Để thực hiện cập nhật đồng thời, tôi gợi ý rằng bạn nên viết một phương thức act gồm có hai khâu. Ở khâu thứ nhất, tất cả viên đá phải đếm số lân cận với nó rồi ghi lại kết quả. Và ở khâu thứ hai, tất cả viên đá cập nhật trạng thái của chúng.

Phương thức act tôi đã viết trông như sau:

  /** 
   * Kiểm tra xem ta đang ở khâu nào và gọi phương thức tương ứng. 
   * Chuyển đến khâu tiếp theo. */ 
  public void act() { 
    if (phase == 1) { 
      numNeighbors = countLiveNeighbors(); 
      phase = 2; 
    } else {
      updateStatus(); 
      phase = 1; 
    } 
  }

phase và numNeighbors là các biến thực thể. Và sau đây là những nguyên mẫu cho countLiveNeighbors và updateStatus:

  /** 
   * Đếm số viên đá lân cận còn sống. */ 
  public int countLiveNeighbors() 

  /** 
   * Cập nhật trạng thái viên đá (sống hoặc chết) dựa trên   
   * số các viên lân cận với nó. */ 
  public void updateStatus()

Hãy bắt đầu với một phiên bản đơn giản của updateStatus chỉ để chuyển viên đá sống thành chết và ngược lại. Bây giờ hãy chạy chương trình rồi khẳng định rằng viên đá đã đổi màu. Cứ hai bước trong World (môi trường) thì tương ứng với một bước thời gian trong trò chơi Life.

Bây giờ hãy điền nội dung vào phần thân của các phương thức countLiveNeighbors và updateStatus theo luật chơi và xem hệ thống có động thái giống như ta dự liệu hay không.

16.8  Điều kiện đầu

Để thay đổi điều kiện đầu, bạn có thể dùng các menu bật của GridWorld để đặt trạng thái của các viên đá bằng cách kích hoạt setAlive. Hoặc bạn cũng có thể viết các phương thức để tự động hóa quy trình.

Trong LifeRunner, hãy bổ sung một phương thức có tên makeRow để tạo nên cấu hình ban đầu với n viên đá sống liền nhau cùng một hàng ở giữa lưới ô. Điều gì sẽ xảy ra với các giá trị khác nhau của n?

Hãy bổ sung một phương thức có tên makePentomino để tạo nên một r-pentomino ở chính giữa lưới ô. Cấu hình ban đầu phải có dạng như sau:

Nếu bạn chạy cấu hình này trong nhiều bước, nó sẽ lan tỏa đến cuối lưới ô. Bốn đường biên của lưới ô đã làm thay đổi động thái của hệ thống. Để thấy được sự phát triển toàn vẹn của r-pentomino, thì lưới ô phải đủ lớn. Bạn có thể phải thử nghiệm để tìm ra kích cỡ lưới ô thích hợp; và tùy thuộc vào tốc độ máy tính đang dùng, công việc này có thể mất thời gian.

Trang web về trò chơi có mô tả những điều kiện đầu khác mà cho ta kết quả thú vị (http://www.conwaylife.com/). Hãy chọn lấy điều kiện đầu mà bạn ưa thích rồi thực hiện nó.

Còn có những biến thể của trò chơi Life với luật chơi khác nhau. Hãy thử chơi một biến thể trong số đó để xem có gì hay.

16.9  Bài tập

Bài tập 1   Khởi đầu bằng một bản sao của BlueBug.java, bạn hãy viết một lời định nghĩa lớp cho một kiểu đối tượng Bug mới có khả năng tìm kiếm và ăn những đóa hoa. Ở đây, “ăn” bông hoa có thể được thực hiện bằng kích hoạt removeSelfFromGrid lên nó.
Bài tập 2   Bây giờ bạn biết hết những gì cần biết để đọc Phần 3 của Cuốn hướng dẫn sinh viên về GridWorld rồi làm các bài tập.
Bài tập 3   Nếu bạn đã thiết lập trò chơi Life, bạn đã hoàn toàn sẵn sàng làm Phần 4 của cuốn Hướng dẫn sinh viên về GridWorld. Hãy đọc nó rồi làm các bài tập.

Chúc mừng, bạn đã học xong!

1 Phản hồi

Filed under Think Java

One response to “Chương 16: GridWorld, phần 3

  1. Pingback: Think Java: Cách suy nghĩ như nhà khoa học máy tính | Blog của Chiến

Gửi phản hồi

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Log Out / Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Log Out / Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Log Out / Thay đổi )

Google+ photo

Bạn đang bình luận bằng tài khoản Google+ Log Out / Thay đổi )

Connecting to %s