iT邦幫忙

2022 iThome 鐵人賽

DAY 26
0

今日目標,開始遊戲。

對,今天只有「開始」還不包含遊戲的過程,但在開始之前,我們要先定義一些之後遊戲過程方便操作的類別或實例。
先建立一個 package,名稱為 game。

定義玩家

我們首先定義一個玩家(player),以及它所包含的資訊(名稱、手牌等)、方法(出牌)。

  1. 在 game package 底下建立一個 java class,名稱為 Player,內容為:
    package com.example.game;
    
    import com.example.card.Card;
    import lombok.Getter;
    import lombok.Setter;
    
    import java.util.ArrayList;
    
    @Getter @Setter
    public class Player {
        private String name;
        private ArrayList<Card> hands;
    
        public Player(String name) {
            this.name = name;
        }
    
        public int countHands() {
            return this.hands.size();
        }
    
        public void play(ArrayList<Card> cards) {
            for (Card card : cards) {
                this.hands.remove(card);
            }
        }
    }
    
  2. 注意 play() 內的 this.hands.remove(card) 這行,remove 的實作是根據 Object.equals() 來比較的,所以我們需要回去修改 Card,加入自定義的 equals 方法,加入的片段程式碼為:
    @Override
    public boolean equals(Object object) {
        // 如果 object 屬於 Card 實例
        if (object instanceof Card) {
            Card other = (Card) object;
            // 比較 Suit 和 Number
            return this.suit.equals(other.suit) && this.number.equals(other.number);
        }
        return false;
    }
    

定義打出的牌

我們還需要一個類型是專門處理玩家打出的牌,透過維護一個 ArrayList<Card> 實現,並給予適當的方法。

  1. 在 game package 底下建立一個 java class,名稱為 PlayedCards,內容為:
    package com.example.card;
    
    import java.util.ArrayList;
    import java.util.Comparator;
    
    public class PlayedCards {
        private final ArrayList<Card> cards;
    
        public PlayedCards(ArrayList<Card> playedCards) {
            this.cards = playedCards;
        }
    
        public ArrayList<Card> get() {
            return this.cards;
        }
    }
    

定義遊戲狀態

跟前面的 UserStatus 類似,用來記錄各房間的遊戲狀態。
Status 包含玩家(players)、上一出牌的玩家(previousPlayer)、上一個玩家打出的牌(previousPlayedCards)、當前輪到的玩家(currentPlayer)。
GameStatus 則是藉由維護一個 HashMap,其中 key 為房號(roomId)、value 為狀態(status)。

  1. 在 game package 底下建立一個 java class,名稱為 GameStatus,內容為:
    package com.example.game;
    
    import com.example.card.Card;
    import com.example.card.PlayedCards;
    import com.example.room.UserStatus;
    import lombok.Getter;
    import lombok.Setter;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.Map;
    
    @Getter @Setter
    class Status {
        private Player[] players = new Player[4];
        private String previousPlayer = null;
        private PlayedCards previousPlayedCards = null;
        private String currentPlayer = null;
    }
    
    @Component
    public class GameStatus {
        @Autowired
        private UserStatus userStatus;
    
        private final Map<String, Status> game = new HashMap<>();
    
        public void add(String roomId) {
            this.game.put(roomId, new Status());
        }
    
        public void remove(String roomId) {
            this.game.remove(roomId);
        }
    
        public void setPlayers(String roomId, Player[] players) {
            this.game.get(roomId).setPlayers(players);
        }
    
        public Player[] getPlayers(String roomId) {
            return this.game.get(roomId).getPlayers();
        }
    
        public void setPreviousPlayer(String roomId, String playerName) {
            this.game.get(roomId).setPreviousPlayer(playerName);
        }
    
        public String getPreviousPlayer(String roomId) {
            return this.game.get(roomId).getPreviousPlayer();
        }
    
        public void setPreviousPlayedCards(String roomId, PlayedCards playedCards) {
            this.game.get(roomId).setPreviousPlayedCards(playedCards);
        }
    
        public PlayedCards getPreviousPlayedCards(String roomId) {
            return this.game.get(roomId).getPreviousPlayedCards();
        }
    
        public void setCurrentPlayer(String roomId, String playerName) {
            this.game.get(roomId).setCurrentPlayer(playerName);
        }
    
        public String getCurrentPlayer(String roomId) {
            return this.game.get(roomId).getCurrentPlayer();
        }
    
        public ArrayList<Card> getHandsByPlayerName(String name) {
            String roomId = this.userStatus.getUserRoomId(name);
            Player[] players = this.getPlayers(roomId);
            for (Player player : players) {
                if (player.getName().equals(name)) {
                    return player.getHands();
                }
            }
            return null;
        }
    
        public ArrayList<ArrayList<Card>> getAllPlayersHands(String roomId) {
            ArrayList<ArrayList<Card>> result = new ArrayList<>();
            Player[] players = this.game.get(roomId).getPlayers();
            for (Player player : players) {
                result.add(player.getHands());
            }
            return result;
        }
    }
    

初始化遊戲狀態

再來,等房主開始遊戲時,我們需要對該房間的遊戲狀態做初始化,初始化的方法,我們透過 GameService 提供這項服務。

  1. 在 game package 底下建立一個 java class,名稱為 GameService
  2. 再來,我們將初始化的方法加入 GameService,完整內容為:
    package com.example.game;
    
    import com.example.card.Card;
    import com.example.card.Deck;
    import com.example.room.Room;
    import com.example.room.RoomList;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import java.util.*;
    
    @Service
    public class GameService {
        @Autowired
        private RoomList roomList;
    
        @Autowired
        private GameStatus gameStatus;
    
        public void initializeGameStatus(String roomId) {
            Room room = roomList.getRoomById(roomId);
    
            // 洗牌和發牌
            Deck deck = new Deck();
            deck.shuffle();
            ArrayList<ArrayList<Card>> hands = deck.deal();
    
            // 建立 Player,並分配手牌
            ArrayList<String> roomMembers = room.getAllMembers();
            LinkedHashMap<String, ArrayList<Card>> playerHands = new LinkedHashMap<>();
            Player[] players = new Player[4];
            for (int i=0; i<4; i++) {
                Player newPlayer = new Player(roomMembers.get(i));
                newPlayer.setHands(hands.get(i));
                players[i] = newPlayer;
            }
    
            gameStatus.add(roomId);
            gameStatus.setPlayers(roomId, players);
        }
    }
    

開始遊戲

在遊戲開始時,我們要做遊戲狀態的初始化,讀者還記得什麼時候開始嗎?就是在所有人都準備好了(包含房主)就是開始遊戲!

  1. 到 RoomWsController 修改 readyToPlay(),僅加入此片段程式碼到該函數最後:
    if (response.isAllReady()) {
        this.gameService.initializeGameStatus(roomId);
    }
    
  2. 避免讀者不清楚放在哪,所以這邊附上完整的 readyToPlay()
    @MessageMapping("/ready")
    public void readyToPlay(UserReadyMessage readyMessage, Principal principal) {
        String username = principal.getName();
        this.userStatus.setUserReady(username, readyMessage.isReady());
    
        String roomId = this.userStatus.getUserRoomId(username);
        Room room = roomList.getRoomById(roomId);
        ArrayList<String> roomMembers = room.getAllMembers();
    
        UserReadyResponse response = new UserReadyResponse();
        int counter = 0;
        for (String member : roomMembers) {
            boolean isReady = this.userStatus.isUserReady(member);
            if (isReady) {
                counter += 1;
            }
            response.add(member, isReady);
        }
    
        String owner = room.getOwner();
        if (counter == 4) {
            response.setAllReady(true);
        }
        else {
            this.userStatus.setUserReady(owner, false);
            response.add(owner, false);
        }
    
        for (String member : roomMembers) {
            response.setMessage("");
            if (counter != 4 && member.equals(username)) {
                if (member.equals(owner)) {
                    if (room.count() == 4) {
                        response.setMessage("尚有人未準備好開始遊戲!");
                    }
                    else {
                        response.setMessage("人數不足!");
                    }
                }
                else {
                    if (!readyMessage.isReady()) {
                        response.setMessage("請快點準備!");
                    }
                }
            }
    
            simpMessagingTemplate.convertAndSendToUser(member, "/queue/ready", response);
        }
    
        // 這邊!!!
        if (response.isAllReady()) {
            this.gameService.initializeGameStatus(roomId);
        }
    }
    
  3. 同時,我們要修改 room.js 的片段程式碼,在接收到大家都準備好時,跳轉到遊戲頁面,修改 subscribeReady(),該部分的完整程式碼為:
    function subscribeReady() {
        websocket.subscribe("/user/queue/ready", (response) => {
            response = JSON.parse(response.body);
            let userStatus = response.userStatus;
            for (let user in userStatus) {
                let readyText = userStatus[user] ? '準備' : '';
                if (user === owner) {
                    readyText = "房主"
                }
                $(`#user-${user} .user-ready`).text(readyText);
            }
    
            // 當全部都準備好,跳轉到遊戲畫面
            if (response.allReady) {
                window.location.href = `/game/${roomId}`;
            }
        })
    }
    
  4. 到這邊已經 OK 了,只差把 game 的頁面做出來~~

顯示頁面

最後,我們先簡單布置遊戲畫面,這邊先只放骨架,之後會透過 JavaScript 渲染。

  1. 在 templates 底下建立一個 HTML file,名稱為 game,內容為:
    <!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">15</div>
    
        <div class="other-hands-90 other-hands-left" id="user-3">
            <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" id="user-4">
            <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="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 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="card-type">
            <div class="m-card text-center">
                <div>
                    ♠️
                    <br>
                    10
                </div>
            </div>
            <div class="m-card text-center">
                <div>
                    ♠️
                    <br>
                    J
                </div>
            </div>
            <div class="m-card text-center">
                <div>
                    ♠️
                    <br>
                    Q
                </div>
            </div>
            <div class="m-card text-center">
                <div>
                    ♠️
                    <br>
                    K
                </div>
            </div>
            <div class="m-card text-center">
                <div>
                    ♠️
                    <br>
                    A
                </div>
            </div>
        </div>
    
        <div class="action">
            <button type="button" class="btn btn-outline-success btn-lg" id="button-play">出牌</button>
            <button type="button" class="btn btn-outline-danger btn-lg" id="button-pass">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>
    
  2. 順便連 css 和 js 一起建立吧~~
    • game.css
      .card {
          flex-direction: inherit !important;
      }
      .m-card {
          display: inline-block;
          position: relative;
          background-color: #fff;
          background-clip: border-box;
          border: 1px solid rgba(0,0,0,.125);
          border-radius: 0.25rem;
      }
      
      .other-hands {
          max-height: 100px;
          margin: 10px auto 0 auto;
      }
      .other-hands .m-card {
          width: 60px;
          height: 100px;
          margin-left: -20px;
      }
      
      .other-hands-90 {
          max-height: 325px;
          padding-top: 17px;
          margin: auto 0;
      }
      .other-hands-left {
          margin-left: 10px;
      }
      .other-hands-right {
          margin-right: 10px;
      }
      
      .other-hands-90 .m-card {
          display: block;
          width: 100px;
          height: 60px;
          margin-top: -35px;
      }
      
      .my-hands {
          position: absolute;
          max-height: 110px;
          font-size: 35px;
          word-break: normal;
          left: 12px;
          top: 490px;
      }
      .my-hands .m-card {
          width: 80px;
          height: 110px;
      }
      
      .card-type {
          position: absolute;
          top: 240px;
          font-size: 35px;
          word-break: normal;
      }
      .card-type .m-card {
          width: 80px;
          height: 110px;
      }
      
      .action {
          position: absolute;
          left: 487px;
          top: 410px;
      }
      #button-pass {
          margin-left: 50px;
      }
      
      .timer {
          height: 50px;
          width: 50px;
          position: absolute;
          padding: 1px;
          border-radius: 100%;
          border: 2px solid red;
          font-size: 30px;
          font-weight: bold;
          color: red;
          top: 30px;
          left: 30px;
      }
      
    • game.js
      $(document).ready(() => {
          relocateMyHands();
          relocatePlayedCards();
      })
      
      function relocateMyHands() {
          let newLocationX = ($(".game-window").width() - $(".my-hands").width()) / 2;
          $(".my-hands").css("left", newLocationX);
      }
      
      function relocatePlayedCards() {
          let newLocationX = ($(".game-window").width() - $(".card-type").width()) / 2;
          $(".card-type").css("left", newLocationX);
      }
      
  3. 在 game package 底下建立一個 java class,名稱為 GameController,內容為:
    package com.example.game;
    
    import com.example.user.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    
    @Controller
    public class GameController {
        @Autowired
        private UserService userService;
    
        @GetMapping("/game/{roomId}")
        public String viewGamePage(@PathVariable("roomId") String roomId, Model model) {
            model.addAttribute("username", userService.getUsername());
            return "game";
        }
    }
    
  4. 再來就可以去看看頁面了~~ 剛才在 GameController 並沒有對 game 的頁面做任何限制,所以現在可以用隨便的網址先進去看頁面架構,例如 http://localhost:8080/game/123

Demo

再來就直接去創建房間,然後開 4 隻帳號(對,你需要 4 個瀏覽器,或是 2 個分別都用一般跟無痕),然後大家一起準備好就開始吧~~
/images/emoticon/emoticon07.gif


上一篇
Day 24 - 卡
下一篇
Day 26 - 手牌
系列文
Spring Boot... 深不可測31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言