Bổ sung Think Python: trò chơi bài Old Maid

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

Nội dung trong bài này phục vụ Chương 18: Thừa kế của cuốn sách Think Python, nhưng thay vì trò chơi Poker là một trò chơi có tên Old Maid.

Trong chương này ta sẽ giới thiệu cách dùng thừa kế với vai trò là một phần của chương trình chơi bài Old Maid. Một trong những mục tiêu đặt ra là viết mã lệnh để có thể sử dụng lại khi xây dựng các chương trình chơi bài khác.

Một phần bài

Hầu như khi lập trò chơi nào cũng cần phải biểu diễn phần bài thuộc về một người chơi. Phần bài này cũng giống cỗ bài ở chỗ: chúng đều là tập hợp từ những lá bài, và chúng đều cần các thao tác như thêm và bớt các lá bài. Ngoài ra, ta còn có thể cần những tính năng như trộn cỗ bài và trộn phần bài của từng người chơi.

Một phần bài khác với một bộ bài. Tùy từng trò chơi, có những thao tác chúng ta muốn áp dụng cho phần bài nhưng lại không dùng cho cỗ bài. Chẳng hạn, trong trò chơi poker ta có thể đánh giá một phần bài (có chứa bộ gì không) hoặc so sánh hai phần bài xem ai thắng. Trong trò chơi bridge ta muốn tính điểm cho một phần bài để đặt cược.

Trường hợp này đã nảy sinh nhu cầu áp dụng thừa kế. Nếu Hand (phần bài) là lớp con của Deck (cỗ bài), thì nó sẽ có tất cả những phương thức của Deck, và có thể bổ sung những phương thức mới.

Trong lời định nghĩa hàm, tên của lớp cha mẹ được đặt trong cặp ngoặc đơn:

class Hand(Deck):
  pass

Câu lệnh này cho thấy rằng lớp Hand mới được thừa kế từ lớp Deck hiện có.

Constructor có tên Hand thực hiện khởi tạo những thuộc tính của phần bài, bao gồm name và cards. Chuỗi name để phân biệt các phần bài, thường đặt theo tên người cầm chúng. Cái tên này là một tham biến tùy chọn và có giá trị mặc định là một chuỗi rỗng. cards là danh sách các lá bài trong phần bài, được khởi tạo là một danh sách rỗng:

class Hand(Deck): 
  def __init__(self, name=""): 
    self.cards = [] 
    self.name = name 

Cũng như bất kì trò chơi bài nào khác, cần phải lấy thêm và bỏ bớt lá bài. Việc lấy thêm bài thì đơn giản, vì Hand thừa hưởng phương thức removeCard từ Deck, cụ thể như sau:

class Deck: 
  ... 
  def removeCard(self, card): 
    if card in self.cards: 
      self.cards.remove(card) 
      return True 
    else: 
      return False

Nhưng ta phải viết addCard:

class Hand(Deck):
  ...
  def addCard(self,card) :
    self.cards.append(card) 

Ở đây, ta lại dùng dấu ba chấm để cho thấy rằng đã bỏ qua những phương thức khác. Phương thức append bổ sung một lá bài mới vào cuối danh sách các lá bài.

Chia bài

Bây giờ khi đã có lớp Hand, ta muốn chia các lá bài từ Deck vào từng phần bài. Thật không dễ thấy rằng liệu nên cho phương thức này vào lớp Hand hay lớp Deck, nhưng vì phương thức hoạt động với cả một cỗ bài lẫn (có thể là) nhiều phần bài, nên để nó ở Deck sẽ tự nhiên hơn.

deal cần phải khá tổng quát, vì những trò chơi bài khác nhau sẽ có yêu cầu khác nhau. Có thể ta muốn chia hết cả cỗ bài cùng lúc hoặc chỉ cho mỗi phần bài thêm một lá bài.

deal nhận vào ba tham số: cỗ bài, một danh sách (hay bộ) các phần bài, và tổng số lá bài cần chia. Nếu không có đủ bài trong cỗ bài thì chỉ cần chia dần đến vừa hết cỗ bài là dừng lại:

class Deck : 
  ... 
  def deal(self, hands, nCards=999): 
    nHands = len(hands) 
    for i in range(nCards): 
      if self.isEmpty(): break    # break, nếu hết lá bài 
      card = self.popCard()       # nhặt lá bài trên cùng 
      hand = hands[i % nHands]    # tiếp theo đến lượt ai? 
      hand.addCard(card)          # thêm lá bài này vào phần bài người đó  

Tham số cuối cùng, nCards, là tùy chọn; giá trị mặc định là một số rất lớn, có nghĩa rằng phải chia hết lá bài từ cỗ.

Biến vòng lặp i chạy từ 0 đến nCards-1. Mỗi lần lặp, một lá bài được lấy khỏi cỗ bằng cách dùng phương thức pop của danh sách, để lấy ra giá trị cuối của danh sách đồng thời trả lại giá trị này.

Toán tử phần dư (%) cho phép ta chia bài theo vòng tròn (mỗi lần chỉ đưa một lá bài cho từng phần bài). Khi i bằng với số phần bài trong danh sách, biểu thức i % nHands sẽ trở về đầu danh sách (chỉ số 0).

In một phần bài

Để in một phần bài, ta có thể tận dụng các phương thức printDeck và __str__ đã thừa kế từ Deck. Chẳng hạn:

>>> deck = Deck() 
>>> deck.shuffle() 
>>> hand = Hand("frank") 
>>> deck.deal([hand], 5) 
>>> print hand 
Hand frank contains 
2 of Spades 
3 of Spades 
  4 of Spades 
   Ace of Hearts 
    9 of Clubs 

Phần bài này chưa đẹp, nhưng nó đang chờ để lập thành một dây bài.

Mặc dù việc thừa kế từ những phương thức sẵn có thì rất tiện lợi, song còn một thông tin nữa ở đối tượng Hand mà ta muốn biểu diễn khi in ra. Để làm điều này, ta có thể khai báo một phương thức __str__ trong lớp Hand để đè lên  phương thức tương ứng từ lớp Deck:

class Hand(Deck) 
  ... 
  def __str__(self): 
    s = "Hand " + self.name 
    if self.isEmpty(): 
      return s + " is empty\n" 
    else: 
      return s + " contains\n" + Deck.__str__(self) 

Ban đầu, s là một chuỗi để đặt tên cho phần bài. Nếu phần bài không chứa gì, thì chương trình sẽ in ra hai chữ is empty theo sau tên, rồi trả lại kết quả.

Còn nếu không, chương trình sẽ thêm chữ contains cùng với chuỗi biểu diễn cho Deck, được tạo thành qua việc gọi phương thức __str__ trong lớp Deck đối với self.

Có vẻ kì quặc trong việc gửi self, vốn chỉ đến Hand hiện tại, đến cho một phương thức Deck, song điều này hoàn toàn bình thường nếu bạn còn nhớ rằng Hand là một dạng của Deck. Các đối tượng Hand có thể làm mọi việc mà đối tượng Deck có thể. Vì vậy gửi một Hand đến cho phương thức Deck là hợp lệ.

Nói chung, sẽ luôn hợp lệ trong việc dùng một thực thể của lớp con thay vì một thực thể của lớp cha mẹ.

Lớp CardGame

Lớp CardGame đảm nhiệm một số việc nhỏ nhặt cơ bản thường gặp ở các trò chơi, như lập cỗ bài và trộn nó:

class CardGame: 
  def __init__(self): 
    self.deck = Deck() 
    self.deck.shuffle() 

Đây là lần đầu tiên ta thấy phương thức khởi tạo đã thực hiện việc tính toán thực sự, thay vì đơn thuần là khởi tạo giá trị thuộc tính.

Để thiết lập các trò chơi cụ thể, ta có thể thừa kế từ CardGame và thêm những đặc điểm vào trò chơi mới. Lấy ví dụ, ta sẽ viết chương trình mô phỏng trò chơi Old Maid.

Mục đích của trò chơi này là phải hạ hết lá bài trên tay xuống. Để làm vậy, cần phải hợp thành các bộ đôi. Chẳng hạn, là 4 pích hợp với 4 nhép vì có cùng màu đen. J cơ hợp với J rô vì cùng màu đỏ.

Bắt đầu trò chơi, lá Q pích được loại khỏi cỗ bài để Q nhép không có đôi. Năm mươi mốt lá bài còn lại được chia cho người chơi theo vòng tròn. Sau khi chia, những người chơi tìm những bộ đôi trên tay mình và hạ xuống, càng nhiều càng tốt.

Khi không tìm thêm được đôi nào nữa, cuộc chơi bắt đầu. Theo lượt, từng người lấy một lá bài bất kì (không nhìn) từ người ngồi cạnh phía trái, nếu họ còn bài. Trong trường hợp lá bài được chọn hợp đôi với một lá bài trên tay, thì hạ đôi này xuống. Còn nếu không thì phải giữ lá bài đó. Sau cùng khi tất cả đôi lá bài được hạ xuống, người nào vẫn còn giữ lá Q nhép thì thua cuộc.

Trong chương trình mô phỏng sau đây, máy tính thực hiện thay cho tất cả những người chơi. Tuy nhiên, sẽ mất đi một số nét đẹp của trò chơi ngoài đời. Thực tế, người đang giữ Q nhép (Old Maid) cần làm cách nào đó để người ngồi cạnh nhặt phải lá bài này. Có thể cố tình để cho lá bài này dễ lấy, hay không cho nó dễ lấy, hoặc giả như vừa lỡ tay sửa cách cầm bài để không cho nó dễ lấy được. Còn máy tính thì chỉ việc chọn bài từ phần cạnh đó một cách ngẫu nhiên.

Lớp OldMaidHand

Một phần bài để chơi Old Maid sẽ cần những tính năng vượt ngoài khuôn khổ của Hand. Ta sẽ định nghĩa một lớp mới, OldMaidHand; lớp này thừa kế từ Hand đồng thời có thêm một phương thức mang tên removeMatches:

class OldMaidHand(Hand):
  def removeMatches(self):
    count = 0
    originalCards = self.cards[:]
    for card in originalCards:
      match = Card(3 - card.suit, card.rank)
      if match in self.cards:
        self.cards.remove(card)
        self.cards.remove(match)
        print "Hand %s: %s matches %s" % (self.name,card,match)
        count = count + 1
    return count 

Ta bắt đầu bằng việc sao chép một danh sách các lá bài, để có thể duyệt theo bản sao này trong quá trình lấy bỏ bài từ bản gốc. Vì self.cards được sửa lại trong vòng lặp, ta không muốn dùng nó để điều khiển việc duyệt này. Python có thể sẽ lẫn lộn nếu nó phải duyệt một dãy đang bị thay đổi!

Với từng lá bài trên tay, ta có thể hình dung được lá bài hợp đôi với nó là gì và đi tìm lá bài này. Lá bài hợp sẽ có cùng bậc và có chất cùng màu. Biểu thức 3 - card.suit sẽ biến chất Nhép (chất số 0) thành chất Pích (số 3) và một chất Rô (số 1) thành chất Cơ (số 2). Bạn có thể tự kiểm tra hai trường hợp ngược lại. Nếu có cặp đôi như vậy trong phần bài, hai lá bài đó sẽ bị bỏ đi.

Ví dụ sau đây sẽ giới thiệu cách dùng removeMatches:

>>> game = CardGame()
 >>> hand = OldMaidHand("frank")
 >>> game.deck.deal([hand], 13)
 >>> print hand
 Hand frank contains
 Ace of Spades
 2 of Diamonds
 7 of Spades
 8 of Clubs
 6 of Hearts
 8 of Spades
 7 of Clubs
 Queen of Clubs
 7 of Diamonds
 5 of Clubs
 Jack of Diamonds
 10 of Diamonds
 10 of Hearts
>>> hand.removeMatches()
 Hand frank: 7 of Spades matches 7 of Clubs
 Hand frank: 8 of Spades matches 8 of Clubs
 Hand frank: 10 of Diamonds matches 10 of Hearts
 >>> print hand
 Hand frank contains
 Ace of Spades
 2 of Diamonds
 6 of Hearts
 Queen of Clubs
 7 of Diamonds
 5 of Clubs
 Jack of Diamonds

Lưu ý rằng không có phương thức __init__ nào cho lớp OldMaidHand. Nó được thừa kế từ Hand.

Lớp OldMaidGame

Bây giờ ta đã có thể hướng sự chú ý vào bản thân trò chơiOldMaidGame là một lớp con của CardGame với một phương thức mới có tên là play trong đó nhận một danh sách các người chơi làm tham biến.

Vì __init__ được kế thừa từ CardGame, một đối tượng OldMaidGame mới sẽ chứa một cỗ bài mới đã được trộn:

class OldMaidGame(CardGame):
  def play(self, names):
    # bỏ lá Q nhép 
    self.deck.removeCard(Card(0,12))
    # chia phần bài cho từng người chơi 
    self.hands = []
    for name in names :
      self.hands.append(OldMaidHand(name)) 
    # chia bài 
    self.deck.deal(self.hands)
    print "---------- Đã chia xong bài"
    self.printHands()
    # bỏ những bộ đôi ban đầu 
    matches = self.removeAllMatches()
    print "---------- Đã bỏ hết đôi, bắt đầu ván bài"
    self.printHands()
    # chơi đến tận khi hết hợp đôi hết 50 lá bài
    turn = 0
    numHands = len(self.hands)
    while matches < 25:
      matches = matches + self.playOneTurn(turn)
      turn = (turn + 1) % numHands
    print "---------- Xong ván"
    self.printHands()

Một số bước trong trò chơi đã được phân chia thành những phương thứcremoveAllMatches duyệt danh sách các phần bài rồi kích hoạt removeMatches đối với mỗi phần:

class OldMaidGame(CardGame):
  ...
  def removeAllMatches(self):
    count = 0
    for hand in self.hands:
      count = count + hand.removeMatches()
    return count

Bài tập: hãy viết printHands với nhiệm vụ duyệt self.hands rồi in ra từng phần bài.

count là một biến tích lũy để cộng dồn số cặp đôi trong mỗi phần bài rồi trả lại tổng số.

Khi tổng số cặp đôi đạt tới 25 thì đã có 50 lá bài được hạ. Như vậy chỉ còn một lá bài và ván chơi kết thúc.

Biến turn theo dõi xem đến lượt người nào. Nó bắt đầu bằng 0 rồi tăng thêm một sau mỗi lần; khi đạt tới numHands, toán tử phần dư sẽ khiến nó quay trở về 0.

Phương thức playOneTurn nhận một đối số chỉ định xem đến lượt người nào. Giá trị trả về là số cặp đôi mà người đó hạ trong lượt chơi:

class OldMaidGame(CardGame):
  ...
  def playOneTurn(self, i):
    if self.hands[i].isEmpty():
      return 0
    neighbor = self.findNeighbor(i)
    pickedCard = self.hands[neighbor].popCard()
    self.hands[i].addCard(pickedCard)
    print "Hand", self.hands[i].name, "picked", pickedCard
    count = self.hands[i].removeMatches()
    self.hands[i].shuffle()
    return count

Nếu phần bài của người chơi trống không, thì người đó đã chơi xong, và không phải thực hiện nhiệm vụ gì, phương thức trả lại giá trị bằng 0.

Trường hợp ngược lại, một lượt chơi bao gồm: tìm người chơi gần nhất phía tay trái mà vẫn còn bài, lấy một lá bài từ tay người đó, rồi kiểm tra xem có hợp đôi không. Trước khi trả lại giá trị phương thức, phần bài được trộn lên để việc lựa chọn của người tiếp theo được tiến hành ngẫu nhiên.

Phương thức findNeighbor bắt đầu bằng người chơi ở ngay bên trái và tiếp tục theo chiều kim đồng hồ đến khi tìm thấy một người vẫn còn bài:

class OldMaidGame(CardGame):
  ...
  def findNeighbor(self, i):
    numHands = len(self.hands)
    for next in range(1,numHands):
      neighbor = (i + next) % numHands
      if not self.hands[neighbor].isEmpty():
        return neighbor

Nếu findNeighbor đi hết một vòng mà không tìm thấy bài, nó sẽ trả lại None rồi có khả năng gây ra lỗi ở chỗ nào đó trong chương trình. Thật may là ta có thể chứng tỏ rằng điều này không bao giờ xảy ra (miễn là chương trình phát hiện đúng thời điểm kết thúc ván bài).

Ta đã bỏ qua phương thức printHands. Bạn có thể tự tay viết phương thức này.

Kết quả dưới đây thu được một ván bài rút gọn chỉ với 15 quân bài (từ bậc 10 trở lên) được chia cho 3 người. Với cỗ bài nhỏ này, ván chơi sẽ kết thúc sau 7 vòng thay vì 25.

>>> import cards
>>> game = cards.OldMaidGame()
>>> game.play(["Allen","Jeff","Chris"])
 ---------- Cards have been dealt
 Hand Allen contains
 King of Hearts
 Jack of Clubs
 Queen of Spades
 King of Spades
 10 of Diamonds
Hand Jeff contains
 Queen of Hearts
 Jack of Spades
 Jack of Hearts
 King of Diamonds
 Queen of Diamonds
Hand Chris contains
 Jack of Diamonds
 King of Clubs
 10 of Spades
 10 of Hearts
 10 of Clubs
Hand Jeff: Queen of Hearts matches Queen of Diamonds
 Hand Chris: 10 of Spades matches 10 of Clubs
 ---------- Matches discarded, play begins
 Hand Allen contains
 King of Hearts
 Jack of Clubs
 Queen of Spades
 King of Spades
 10 of Diamonds
Hand Jeff contains
 Jack of Spades
 Jack of Hearts
 King of Diamonds
Hand Chris contains
 Jack of Diamonds
 King of Clubs
 10 of Hearts
Hand Allen picked King of Diamonds
 Hand Allen: King of Hearts matches King of Diamonds
 Hand Jeff picked 10 of Hearts
 Hand Chris picked Jack of Clubs
 Hand Allen picked Jack of Hearts
 Hand Jeff picked Jack of Diamonds
 Hand Chris picked Queen of Spades
 Hand Allen picked Jack of Diamonds
 Hand Allen: Jack of Hearts matches Jack of Diamonds
 Hand Jeff picked King of Clubs
 Hand Chris picked King of Spades
 Hand Allen picked 10 of Hearts
 Hand Allen: 10 of Diamonds matches 10 of Hearts
 Hand Jeff picked Queen of Spades
 Hand Chris picked Jack of Spades
 Hand Chris: Jack of Clubs matches Jack of Spades
 Hand Jeff picked King of Spades
 Hand Jeff: King of Clubs matches King of Spades
 ---------- Game is Over
 Hand Allen is empty
Hand Jeff contains
 Queen of Spades
Hand Chris is empty

Như vậy Jeff đã thua cuộc.

1 Phản hồi

Filed under Think Python

One response to “Bổ sung Think Python: trò chơi bài Old Maid

  1. Pingback: Cách 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