今日目標,顯示上一組牌、定義特殊牌型、檢驗出牌是合法的。
發現前幾天的 code 邏輯有點問題,所以稍微作一下修正,最近有點累...
sendMyHands()
,這是為了待會方便調用、修改 play()
,主要是 previousPlayer 從錯誤地方取得,原本是直接取 currentPlayer,但應該要從 gameStatus 取才對:
public void sendMyHands(String name) {
ArrayList<Card> myHands = this.getHandsByPlayerName(name);
this.simpMessagingTemplate.convertAndSendToUser(name, "/queue/my-hands", myHands);
}
public void play(String roomId) {
Player[] players = gameStatus.getPlayers(roomId);
int index = 0;
for (int i=0; i<4; i++) {
if (players[i].getName().equals(this.gameStatus.getCurrentPlayer(roomId))) {
index = i;
break;
}
}
String currentPlayer;
String previousPlayer;
GameTimer timer = new GameTimer();
this.gameTimerList.add(roomId, timer);
try {
Thread.sleep(5000);
}
catch(Exception e) {
System.out.println(e.getMessage());
}
while (true) {
previousPlayer = this.gameStatus.getPreviousPlayer(roomId);
currentPlayer = players[index].getName();
this.gameStatus.setCurrentPlayer(roomId, currentPlayer);
if (currentPlayer.equals(previousPlayer)) {
this.gameStatus.setPreviousPlayer(roomId, null);
this.gameStatus.setPreviousPlayedCards(roomId, null);
}
timer.init(20);
String finalCurrentPlayer = currentPlayer;
String finalPreviousPlayer = previousPlayer;
timer.countDown((n) -> {
Map<String, Object> status = new HashMap<>();
status.put("currentPlayer", finalCurrentPlayer);
PlayedCards tmp = this.gameStatus.getPreviousPlayedCards(roomId);
if (tmp == null) {
status.put("previousPlayedCards", null);
}
else {
status.put("previousPlayedCards", tmp.get());
}
status.put("timer", n);
roomService.sendMessageToRoom(roomId, "/queue/game", status);
});
timer.await(25);
if (gameStatus.getHandsByPlayerName(currentPlayer).size() == 0) {
// TODO: 定義結束遊戲的訊息
this.gameStatus.remove(roomId);
this.gameTimerList.remove(roomId);
return;
}
index = (index + 1) % 4;
}
}
getMyHands()
,其實這邊不是必要的,只是因為上面把發送手牌訊息函數化就順便改了:
@MessageMapping("/my-hands")
public void getMyHands(Principal principal) {
String name = principal.getName();
this.gameService.sendMyHands(name);
}
countDown()
,多一個錯誤捕獲的過程,避免被停止卻又剛好正在倒數:
public void countDown(Callable printer) {
try {
Thread.sleep(1000);
this.timer.scheduleAtFixedRate(()-> {
printer.call(time.toString());
time--;
if (time <= 0) {
stop();
}
}, 1, 1, TimeUnit.SECONDS);
}
catch (Exception ignored) {
}
}
game.js
的 compareByGeneral()
,這樣排列應該才是正常人會用的:
function compareByGeneral(a, b) {
if (GENERAL_LEVEL[a.number] > GENERAL_LEVEL[b.number]) {
return 1;
}
else if (GENERAL_LEVEL[a.number] < GENERAL_LEVEL[b.number]) {
return -1;
}
else {
return GENERAL_LEVEL[b.suit] - GENERAL_LEVEL[a.suit];
}
}
game.js
的 generateMyHands()
,要記得清除原先選擇的牌,不然會一直往上疊,最後什麼牌都出不了,修改後的函數完整內容為:
function generateMyHands(myHands) {
$(".my-hands").empty();
let index = 0;
for (let cards of myHands) {
let tmp = cards.split("-");
let color = (tmp[0] == SUITS.SPADE || tmp[0] == SUITS.CLUB) ? "#000000" : "#FF0000";
$(".my-hands").append(`
<div class="m-card text-center" style="color: ${color};" id="card-${tmp[0]}-${tmp[1]}" tabindex="${index++}">
<div>
${tmp[0]}
<br>
${tmp[1]}
</div>
</div>
`);
}
relocateMyHands();
// 清除選擇的
selectedCards.clear();
$(".my-hands .m-card").click((element) => {
let id = element.currentTarget.id;
if (selectedCards.has(id)) {
unselectCard(id);
}
else {
selectCard(id);
}
})
}
小弟昨天忘記處理這邊了...
game.js
加入 generatePlayedCards()
function generatePlayedCards(playedCards) {
$(".card-type").empty();
if (playedCards === null) {
return;
}
playedCards = playedCards.map(convertCard);
for (let card of playedCards) {
let tmp = card.split("-");
let color = (tmp[0] == SUITS.SPADE || tmp[0] == SUITS.CLUB) ? "#000000" : "#FF0000";
$(".card-type").append(`
<div class="m-card text-center" style="color: ${color};">
<div>
${tmp[0]}
<br>
${tmp[1]}
</div>
</div>
`);
}
relocatePlayedCards();
}
subscribeGame()
,加入剛才的 generatePlayedCards()
:
function subscribeGame() {
websocket.subscribe("/user/queue/game", (response) => {
response = JSON.parse(response.body);
$(".timer").text(response.timer);
if (response.currentPlayer === myUsername) {
$("#button-play").prop("disabled", false);
$("#button-pass").prop("disabled", false);
}
else {
$("#button-play").prop("disabled", true);
$("#button-pass").prop("disabled", true);
}
generatePlayedCards(response.previousPlayedCards);
});
}
game.html
,把原本的位置的靜態卡牌刪掉,修改後完整內容為:
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout.html}"
>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div layout:fragment="content" class="card game-window">
<input id="my-username" type="hidden" th:value="${username}">
<div class="timer text-center">..</div>
<div class="other-hands-90 other-hands-left" id="user-3"></div>
<div class="other-hands" id="user-4"></div>
<div class="my-hands" id="user-1">
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
</div>
<div class="other-hands-90 other-hands-right" id="user-2"></div>
<div class="card-type"></div>
<div class="action">
<button type="button" class="btn btn-outline-success btn-lg" id="button-play" onclick="play()" disabled>出牌</button>
<button type="button" class="btn btn-outline-danger btn-lg" id="button-pass" onclick="pass()" disabled>PASS</button>
</div>
</div>
<div layout:fragment="js-and-css">
<!-- websockets -->
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.js"
integrity="sha512-tL4PIUsPy+Rks1go4kQG8M8/ItpRMvKnbBjQm4d2DQnFwgcBYRRN00QdyQnWSCwNMsoY/MfJY8nHp2CzlNdtZA=="
crossorigin="anonymous"
referrerpolicy="no-referrer"></script>
<!-- custom -->
<script type="text/javascript" th:src="@{/js/WebSockets.js}"></script>
<script type="text/javascript" th:src="@{/js/game.js}"></script>
<link th:href="@{/css/game.css}" rel="stylesheet">
</div>
</body>
</html>
有玩過撲克牌的就知道,會有所謂的牌型,比如同花順、鐵支、葫蘆等,我們要先確定使用者打出的牌究竟是什麼,然後再根據上一個使用者打出的牌來檢驗能不能出這組牌。
不過各個地區的規則略有不同,所以我們這邊要先定義好牌型以及大小和規定:
package com.example.card;
public enum Type {
STRAIGHT_FLUSH(3), // 同花順
FOUR_OF_A_KIND(2), // 鐵支
FULL_HOUSE(1), // 葫蘆
STRAIGHT(1), // 順子
PAIR(1), // 一對
NONE(1); // 孤支
private final int level;
Type(int level) {
this.level = level;
}
public boolean levelGreaterThan(Type other) {
if(other == null) {
return true;
}
return this.level > other.level;
}
public boolean levelLessThan(Type other) {
if(other == null) {
return false;
}
return this.level < other.level;
}
public boolean levelEquals(Type other) {
if(other == null) {
return false;
}
return this.level == other.level;
}
}
getComparatorWithValue()
:
public static Comparator<Card> getComparatorWithValue() {
return new Comparator<Card>() {
@Override
public int compare(Card a, Card b) {
return a.getNumber().getValue() - b.getNumber().getValue();
}
};
}
getValue()
:
public int getValue() {
return (this.level + 1) % 13 + 1;
}
public static boolean isStraightFlush(ArrayList<Card> cards) {
if (cards.size() != 5) {
return false;
}
// 同時滿足同花和順子的條件
return Type.isStraight(cards) && Type.isFlush(cards);
}
public static boolean isFourOfAKing(ArrayList<Card> cards) {
if (cards.size() != 5) {
return false;
}
Number number = cards.get(0).getNumber();
// 計算不同數字出現幾次
Map<Number, Integer> map = new HashMap<>();
for (Card card : cards) {
if (map.containsKey(card.getNumber())) {
int tmp = map.get(card.getNumber());
map.put(card.getNumber(), tmp + 1);
}
else {
map.put(card.getNumber(), 1);
}
}
// 只有兩種數字
boolean onlyContainTwoNumbers = map.keySet().size() == 2;
// 數字的數量不是 1 張就是 4 張
boolean isCombinedByOneAndFourNumbers = map.get(number) == 1 || map.get(number) == 4;
// 同時滿足才是鐵支
return onlyContainTwoNumbers && isCombinedByOneAndFourNumbers;
}
public static boolean isFullHouse(ArrayList<Card> cards) {
if (cards.size() != 5) {
return false;
}
Number number = cards.get(0).getNumber();
Map<Number, Integer> map = new HashMap<>();
for (Card card : cards) {
if (map.containsKey(card.getNumber())) {
int tmp = map.get(card.getNumber());
map.put(card.getNumber(), tmp + 1);
}
else {
map.put(card.getNumber(), 1);
}
}
// 只有兩種數字
boolean onlyContainTwoNumbers = map.keySet().size() == 2;
// 數字的數量不是 2 張就是 3 張
boolean isCombinedByTwoAndThreeNumbers = map.get(number) == 2 || map.get(number) == 3;
// 同時符合才是葫蘆
return onlyContainTwoNumbers && isCombinedByTwoAndThreeNumbers;
}
public static boolean isStraight(ArrayList<Card> cards) {
if (cards.size() != 5) {
return false;
}
// 先根據數字做排序
cards.sort(Card.getComparatorWithValue());
int previous = cards.get(0).getNumber().getValue();
// 只要後一張數字是前一張數字 -1 就是連續的,連續 5 張就是順子
for (int i=1; i<5; i++) {
if (cards.get(i).getNumber().getValue() - previous != 1) {
return false;
}
previous = cards.get(i).getNumber().getValue();
}
return true;
}
public static boolean isFlush(ArrayList<Card> cards) {
if (cards.size() != 5) {
return false;
}
Suit suit = cards.get(0).getSuit();
// 全部花色相同
for (Card card : cards) {
if (!card.getSuit().equals(suit)) {
return false;
}
}
return true;
}
public static boolean isPair(ArrayList<Card> cards) {
if (cards.size() != 2) {
return false;
}
// 兩張數字一致
return cards.get(0).getNumber().equals(cards.get(1).getNumber());
}
getType()
:
public static Type getType(ArrayList<Card> cards) {
int size = cards.size();
if (size == 5) {
if (isStraightFlush(cards)) {
return Type.STRAIGHT_FLUSH;
}
else if (isFourOfAKing(cards)) {
return Type.FOUR_OF_A_KIND;
}
else if(isFullHouse(cards)) {
return Type.FULL_HOUSE;
}
else if (isStraight(cards)) {
return Type.STRAIGHT;
}
}
else if (size == 2) {
if (isPair(cards)) {
return Type.PAIR;
}
}
else if (size == 1) {
return Type.NONE;
}
return null;
}
再來,我們要檢驗玩家打出的這組牌是合法的,也就是辨別的出是哪種牌型,而且比上一組牌來的更大。
isValid()
:
public boolean isValid() {
return Type.getType(this.get()) != null;
}
greaterThan()
比較兩張牌的大小關係、getComparatorWithLevel()
根據 level 排序:
public boolean greaterThan(Card other) {
if (this.number.greaterThan(other.number)) {
return true;
}
if (this.number.lessThan(other.number)) {
return false;
}
return this.suit.greaterThan(other.suit);
}
public static Comparator<Card> getComparatorWithLevel() {
return new Comparator<Card>() {
@Override
public int compare(Card a, Card b) {
if (a.greaterThan(b)) {
return 1;
}
return -1;
}
};
}
getHighestLevelCard()
、compare()
:
public Card getHighestLevelCard() {
this.cards.sort(Card.getComparatorWithLevel());
return this.cards.get(this.cards.size() - 1);
}
public static int compare(PlayedCards a, PlayedCards b) {
if (a == null) {
return -1;
}
if (b == null) {
return 1;
}
Type aType = Type.getType(a.get());
Type bType = Type.getType(b.get());
if (bType.levelGreaterThan(aType)) {
return -1;
}
else if (bType.levelEquals(aType)) {
if (bType == aType) {
if (aType == Type.STRAIGHT || aType == Type.STRAIGHT_FLUSH) {
boolean aIsAceToFive = a.get().get(3).getNumber().equals(Number.ACE);
boolean bIsAceToFive = b.get().get(3).getNumber().equals(Number.ACE);
if (aIsAceToFive && bIsAceToFive) {
if (a.getHighestLevelCard().greaterThan(b.getHighestLevelCard())) {
return 1;
}
else {
return -1;
}
}
else {
if (aIsAceToFive) {
return -1;
}
else if (bIsAceToFive) {
return 1;
}
else {
if (a.getHighestLevelCard().greaterThan(b.getHighestLevelCard())) {
return 1;
}
else {
return -1;
}
}
}
}
else if (aType == Type.FULL_HOUSE || aType == Type.FOUR_OF_A_KIND) {
if (a.getHighestLevelCard().greaterThan(b.getHighestLevelCard())) {
return 1;
}
else {
return -1;
}
}
else {
if (a.getHighestLevelCard().greaterThan(b.getHighestLevelCard())) {
return 1;
}
else {
return -1;
}
}
}
else {
return 0;
}
}
return 0;
}
canBePlayedOn()
:
public boolean canBePlayedOn(PlayedCards previous) {
return PlayedCards.compare(this, previous) == 1;
}
playGame()
加入檢驗的過程,然後我們改一下函數名稱(改成 playCards()
),昨天取得不好...:
@MessageMapping("/play")
public void playCards(PlayerActionMessage playerActionMessage, Principal principal) {
String playerName = principal.getName();
String roomId = userStatus.getUserRoomId(playerName);
Player player = this.gameService.findPlayer(roomId, playerName);
Map<String, Object> response = new HashMap<>();
GameTimer timer = gameTimerList.get(roomId);
String action = playerActionMessage.getAction();
if (action.equals("play")) {
PlayedCards currentPlayedCards = playerActionMessage.getPlayedCards();
PlayedCards previousPlayedCards = this.gameStatus.getPreviousPlayedCards(roomId);
// 檢驗它
if (!currentPlayedCards.isValid() || !currentPlayedCards.canBePlayedOn(previousPlayedCards)) {
response.put("status", "fail");
simpMessagingTemplate.convertAndSendToUser(playerName, "/queue/play", response);
return;
}
player.play(currentPlayedCards.get());
this.gameStatus.setPreviousPlayer(roomId, playerName);
this.gameStatus.setPreviousPlayedCards(roomId, currentPlayedCards);
}
response.put("message", "success");
simpMessagingTemplate.convertAndSendToUser(playerName, "/queue/play", response);
timer.stop();
}
如果梅花 3 的那個人一直不出,那其實有違遊戲規則的,或者是大家都 pass 一輪完,結果原本那個人不出牌,這樣沒辦法玩下去,所以還需要自動出牌的手段,讓整個遊戲可以正常進行。
autoPlay()
:
public void autoPlay(String roomId, String playerName) {
Player player = this.findPlayer(roomId, playerName);
ArrayList<Card> cards = new ArrayList<>(player.getHands().subList(0, 1));
player.play(cards);
this.gameStatus.setPreviousPlayer(roomId, playerName);
this.gameStatus.setPreviousPlayedCards(roomId, new PlayedCards(cards));
}
public void autoPlay(String roomId, String playerName, ArrayList<Card> cards) {
Player player = this.findPlayer(roomId, playerName);
player.play(cards);
this.gameStatus.setPreviousPlayer(roomId, playerName);
this.gameStatus.setPreviousPlayedCards(roomId, new PlayedCards(cards));
}
play()
加入自動打出的動作,目前 play 完整內容為:
public void play(String roomId) {
Player[] players = gameStatus.getPlayers(roomId);
int index = 0;
for (int i=0; i<4; i++) {
if (players[i].getName().equals(this.gameStatus.getCurrentPlayer(roomId))) {
index = i;
break;
}
}
String currentPlayer;
String previousPlayer;
GameTimer timer = new GameTimer();
this.gameTimerList.add(roomId, timer);
try {
Thread.sleep(5000);
}
catch(Exception e) {
System.out.println(e.getMessage());
}
while (true) {
previousPlayer = this.gameStatus.getPreviousPlayer(roomId);
currentPlayer = players[index].getName();
this.gameStatus.setCurrentPlayer(roomId, currentPlayer);
if (currentPlayer.equals(previousPlayer)) {
this.gameStatus.setPreviousPlayer(roomId, null);
this.gameStatus.setPreviousPlayedCards(roomId, null);
}
timer.init(20);
String finalCurrentPlayer = currentPlayer;
String finalPreviousPlayer = previousPlayer;
timer.countDown((n) -> {
Map<String, Object> status = new HashMap<>();
status.put("currentPlayer", finalCurrentPlayer);
PlayedCards tmp = this.gameStatus.getPreviousPlayedCards(roomId);
if (tmp == null) {
status.put("previousPlayedCards", null);
}
else {
status.put("previousPlayedCards", tmp.get());
}
status.put("timer", n);
roomService.sendMessageToRoom(roomId, "/queue/game", status);
// 數到 1,就自動出牌了
if (n.equals("1")) {
boolean isFirstPlayer = this.findFirstPlayer(roomId) != null;
// 如果是第一個出牌的玩家
if (isFirstPlayer) {
ArrayList<Card> cards = new ArrayList<>();
cards.add(new Card(Suit.CLUB, Number.THREE));
this.autoPlay(roomId, finalCurrentPlayer, cards);
sendMyHands(finalCurrentPlayer);
sendHandsInfo(roomId);
return;
}
// 如果都 pass 一輪了
if (finalCurrentPlayer.equals(finalPreviousPlayer)) {
this.autoPlay(roomId, finalCurrentPlayer);
sendMyHands(finalCurrentPlayer);
sendHandsInfo(roomId);
}
}
});
timer.await(25);
if (gameStatus.getHandsByPlayerName(currentPlayer).size() == 0) {
// TODO: 定義結束遊戲的訊息
this.gameStatus.remove(roomId);
this.gameTimerList.remove(roomId);
return;
}
index = (index + 1) % 4;
}
}
稍微修了一下之前的 bug,今天玩了好幾局才發現前幾天寫錯了...
雖然鐵人賽的 30 天已經結束了,但小弟的遊戲明天才是最後一天~~ 有發現標題是 Day 29 嗎