feat: 添加21点游戏
This commit is contained in:
parent
dce09c6e1f
commit
53da17802a
104
handler/blackjack/blackjack.go
Normal file
104
handler/blackjack/blackjack.go
Normal file
@ -0,0 +1,104 @@
|
||||
package blackjack
|
||||
|
||||
import (
|
||||
"git.lxtend.com/qqbot/constants"
|
||||
"git.lxtend.com/qqbot/handler"
|
||||
"git.lxtend.com/qqbot/handler/blackjack/controller"
|
||||
"git.lxtend.com/qqbot/handler/blackjack/core"
|
||||
"git.lxtend.com/qqbot/handler/blackjack/view"
|
||||
"git.lxtend.com/qqbot/model"
|
||||
"git.lxtend.com/qqbot/util"
|
||||
)
|
||||
|
||||
var userGameMap map[string]*core.BackJackGame
|
||||
|
||||
func init() {
|
||||
handler.RegisterHandler("bj", blackJack, constants.LEVEL_USER)
|
||||
userGameMap = make(map[string]*core.BackJackGame)
|
||||
}
|
||||
|
||||
func blackJack(msg model.Message) model.Reply {
|
||||
tokens := util.SplitN(msg.RawMsg, 2)
|
||||
if len(tokens) < 2 {
|
||||
return model.Reply{
|
||||
ReplyMsg: "Invalid command",
|
||||
ReferOriginMsg: true,
|
||||
FromMsg: msg,
|
||||
}
|
||||
}
|
||||
|
||||
if tokens[1] == "exit" {
|
||||
delete(userGameMap, util.From(msg.GroupInfo.GroupId, msg.UserId))
|
||||
return model.Reply{
|
||||
ReplyMsg: "Bye",
|
||||
ReferOriginMsg: false,
|
||||
FromMsg: msg,
|
||||
}
|
||||
}
|
||||
|
||||
if tokens[1] == "start" {
|
||||
userGameMap[util.From(msg.GroupInfo.GroupId, msg.UserId)] = core.NewBlackJackGame(controller.NewBlackJackSimulator(), view.NewLiteralViewer(controller.NewBlackJackSimulator()))
|
||||
}
|
||||
if _, ok := userGameMap[util.From(msg.GroupInfo.GroupId, msg.UserId)]; !ok {
|
||||
return model.Reply{
|
||||
ReplyMsg: "Please start a game first",
|
||||
ReferOriginMsg: true,
|
||||
FromMsg: msg,
|
||||
}
|
||||
}
|
||||
response, _ := userGameMap[util.From(msg.GroupInfo.GroupId, msg.UserId)].AddCommand(tokens[1])
|
||||
handler.RegisterLiveHandler(msg.GroupInfo.GroupId, msg.UserId, blackJackWithNoBj)
|
||||
if response[len(response)-1:] == "\n" {
|
||||
response = response[:len(response)-1]
|
||||
}
|
||||
return model.Reply{
|
||||
ReplyMsg: response,
|
||||
ReferOriginMsg: false,
|
||||
FromMsg: msg,
|
||||
}
|
||||
}
|
||||
|
||||
func blackJackWithNoBj(msg model.Message) (model.Reply, bool) {
|
||||
if msg.RawMsg == "exit" {
|
||||
delete(userGameMap, util.From(msg.GroupInfo.GroupId, msg.UserId))
|
||||
handler.UnRegisterLiveHandler(msg.GroupInfo.GroupId, msg.UserId)
|
||||
return model.Reply{
|
||||
ReplyMsg: "Bye",
|
||||
ReferOriginMsg: false,
|
||||
FromMsg: msg,
|
||||
}, true
|
||||
}
|
||||
|
||||
if msg.RawMsg == "start" {
|
||||
userGameMap[util.From(msg.GroupInfo.GroupId, msg.UserId)] = core.NewBlackJackGame(controller.NewBlackJackSimulator(), view.NewLiteralViewer(controller.NewBlackJackSimulator()))
|
||||
}
|
||||
|
||||
if _, ok := userGameMap[util.From(msg.GroupInfo.GroupId, msg.UserId)]; !ok {
|
||||
return model.Reply{
|
||||
ReplyMsg: "Please start a game first",
|
||||
ReferOriginMsg: true,
|
||||
FromMsg: msg,
|
||||
}, true
|
||||
}
|
||||
response, err := userGameMap[util.From(msg.GroupInfo.GroupId, msg.UserId)].AddCommand(msg.RawMsg)
|
||||
if err != nil {
|
||||
if err.Error() == "invalid command" {
|
||||
return model.Reply{
|
||||
ReplyMsg: "",
|
||||
ReferOriginMsg: false,
|
||||
FromMsg: msg,
|
||||
}, false
|
||||
} else if err.Error() == "game over" {
|
||||
delete(userGameMap, util.From(msg.GroupInfo.GroupId, msg.UserId))
|
||||
handler.UnRegisterLiveHandler(msg.GroupInfo.GroupId, msg.UserId)
|
||||
}
|
||||
}
|
||||
if len(response) > 1 && response[len(response)-1:] == "\n" {
|
||||
response = response[:len(response)-1]
|
||||
}
|
||||
return model.Reply{
|
||||
ReplyMsg: response,
|
||||
ReferOriginMsg: false,
|
||||
FromMsg: msg,
|
||||
}, true
|
||||
}
|
13
handler/blackjack/controller/response.go
Normal file
13
handler/blackjack/controller/response.go
Normal file
@ -0,0 +1,13 @@
|
||||
package controller
|
||||
|
||||
type BackJackResponse struct {
|
||||
Code int
|
||||
Description string
|
||||
}
|
||||
|
||||
func NewBlackJackResponse(code int, description string) *BackJackResponse {
|
||||
return &BackJackResponse{
|
||||
Code: code,
|
||||
Description: description,
|
||||
}
|
||||
}
|
183
handler/blackjack/controller/simulator.go
Normal file
183
handler/blackjack/controller/simulator.go
Normal file
@ -0,0 +1,183 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"git.lxtend.com/qqbot/handler/blackjack/model"
|
||||
"git.lxtend.com/qqbot/handler/blackjack/util"
|
||||
)
|
||||
|
||||
type BackJackSimulator struct {
|
||||
deck *model.Deck
|
||||
dealerCards *model.Deck
|
||||
playerCards *model.Deck
|
||||
dealerScore int
|
||||
playerScore int
|
||||
status util.Status
|
||||
}
|
||||
|
||||
func NewBlackJackSimulator() *BackJackSimulator {
|
||||
simulator := &BackJackSimulator{
|
||||
deck: model.NewDeck(),
|
||||
dealerCards: model.NewDeck(),
|
||||
playerCards: model.NewDeck(),
|
||||
}
|
||||
|
||||
simulator.Init()
|
||||
return simulator
|
||||
}
|
||||
|
||||
func initDeck(deck *model.Deck) *model.Deck {
|
||||
colors := []string{"Spade", "Heart", "Diamond", "Club"}
|
||||
|
||||
for _, color := range colors {
|
||||
for i := 2; i <= 10; i++ {
|
||||
deck.Add(model.NewCard(color, strconv.Itoa(i), []int{i}, true))
|
||||
}
|
||||
|
||||
for _, name := range []string{"J", "Q", "K"} {
|
||||
deck.Add(model.NewCard(color, name, []int{10}, true))
|
||||
}
|
||||
|
||||
deck.Add(model.NewCard(color, "A", []int{1, 11}, true))
|
||||
}
|
||||
|
||||
return deck.Shuffle()
|
||||
}
|
||||
|
||||
func initDealerCards(dealerCards *model.Deck, deck *model.Deck) *model.Deck {
|
||||
if firstCard := deck.Draw(); deck != nil {
|
||||
dealerCards.Add(firstCard.Flip())
|
||||
}
|
||||
|
||||
if secondCard := deck.Draw(); deck != nil {
|
||||
dealerCards.Add(secondCard)
|
||||
}
|
||||
return dealerCards
|
||||
}
|
||||
|
||||
func initPlayerCards(playerCards *model.Deck, deck *model.Deck) *model.Deck {
|
||||
if firstCard := deck.Draw(); deck != nil {
|
||||
playerCards.Add(firstCard)
|
||||
}
|
||||
|
||||
if secondCard := deck.Draw(); deck != nil {
|
||||
playerCards.Add(secondCard)
|
||||
}
|
||||
return playerCards
|
||||
}
|
||||
|
||||
func (simulator *BackJackSimulator) Init() *BackJackSimulator {
|
||||
simulator.deck = model.NewDeck()
|
||||
simulator.dealerCards = model.NewDeck()
|
||||
simulator.playerCards = model.NewDeck()
|
||||
|
||||
simulator.deck = initDeck(simulator.deck)
|
||||
simulator.dealerCards = initDealerCards(simulator.dealerCards, simulator.deck)
|
||||
simulator.playerCards = initPlayerCards(simulator.playerCards, simulator.deck)
|
||||
|
||||
simulator.dealerScore, simulator.playerScore = 0, 0
|
||||
simulator.status = util.INITIALIZED
|
||||
|
||||
return simulator
|
||||
}
|
||||
|
||||
func (simulator *BackJackSimulator) Hit() *BackJackResponse {
|
||||
if simulator.status == util.FAILED || simulator.status == util.WINNED || simulator.status == util.DRAW {
|
||||
return NewBlackJackResponse(400, "[🐧] You have failed in this Blackjack game !\n")
|
||||
}
|
||||
|
||||
if card := simulator.deck.Draw(); card != nil {
|
||||
simulator.playerCards.Add(card)
|
||||
simulator.playerScore = 0
|
||||
|
||||
for _, card := range simulator.playerCards.Cards() {
|
||||
simulator.playerScore += card.Value[0]
|
||||
}
|
||||
|
||||
if simulator.playerScore > 21 {
|
||||
simulator.status = util.FAILED
|
||||
return NewBlackJackResponse(400, "[🐧] Your total score exceeds 21 ! You lose !\n")
|
||||
}
|
||||
}
|
||||
|
||||
return NewBlackJackResponse(200, "[🐧] You have drawn a card !\n")
|
||||
}
|
||||
|
||||
func (simulator *BackJackSimulator) Stand() *BackJackResponse {
|
||||
if simulator.status == util.FAILED || simulator.status == util.WINNED || simulator.status == util.DRAW {
|
||||
return NewBlackJackResponse(400, "[🐧] You have failed in this Blackjack game !\n")
|
||||
}
|
||||
|
||||
simulator.dealerScore, simulator.playerScore = 0, 0
|
||||
maxDealerScore, maxPlayerScore, minDealerScore, minPlayerScore := 0, 0, 0, 0
|
||||
|
||||
for _, card := range simulator.dealerCards.Cards() {
|
||||
if !card.Visible {
|
||||
card.Flip()
|
||||
}
|
||||
|
||||
if card.Name == "A" {
|
||||
maxDealerScore += card.Value[1]
|
||||
minDealerScore += card.Value[0]
|
||||
} else {
|
||||
maxDealerScore += card.Value[0]
|
||||
minDealerScore += card.Value[0]
|
||||
}
|
||||
}
|
||||
|
||||
for _, card := range simulator.playerCards.Cards() {
|
||||
if card.Name == "A" {
|
||||
maxPlayerScore += card.Value[1]
|
||||
minPlayerScore += card.Value[0]
|
||||
} else {
|
||||
maxPlayerScore += card.Value[0]
|
||||
minPlayerScore += card.Value[0]
|
||||
}
|
||||
}
|
||||
|
||||
if maxDealerScore < 17 {
|
||||
for maxDealerScore < 17 {
|
||||
if card := simulator.deck.Draw(); card != nil {
|
||||
simulator.dealerCards.Add(card)
|
||||
if card.Name == "A" {
|
||||
maxDealerScore += card.Value[1]
|
||||
minDealerScore += card.Value[0]
|
||||
} else {
|
||||
maxDealerScore += card.Value[0]
|
||||
minDealerScore += card.Value[0]
|
||||
}
|
||||
}
|
||||
|
||||
if minDealerScore > 21 {
|
||||
simulator.status = util.WINNED
|
||||
return NewBlackJackResponse(400, "[🐧] The dealer burst and you have won the game !\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if maxDealerScore > maxPlayerScore {
|
||||
simulator.status = util.FAILED
|
||||
return NewBlackJackResponse(400, "[🐧] The dealer wins the game !\n")
|
||||
} else if maxDealerScore < maxPlayerScore {
|
||||
simulator.status = util.WINNED
|
||||
return NewBlackJackResponse(400, "[🐧] You have won the game !\n")
|
||||
} else if maxDealerScore == maxPlayerScore {
|
||||
simulator.status = util.DRAW
|
||||
return NewBlackJackResponse(200, "[🐧] It is a draw !\n")
|
||||
}
|
||||
|
||||
return NewBlackJackResponse(400, "[🐧] Unrecognized Error !\n")
|
||||
}
|
||||
|
||||
func (simulator *BackJackSimulator) GetDealerCards() *model.Deck {
|
||||
return simulator.dealerCards
|
||||
}
|
||||
|
||||
func (simulator *BackJackSimulator) GetPlayerCards() *model.Deck {
|
||||
return simulator.playerCards
|
||||
}
|
||||
|
||||
func (simulator *BackJackSimulator) GetStatus() util.Status {
|
||||
return simulator.status
|
||||
}
|
74
handler/blackjack/core/game.go
Normal file
74
handler/blackjack/core/game.go
Normal file
@ -0,0 +1,74 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"git.lxtend.com/qqbot/handler/blackjack/controller"
|
||||
"git.lxtend.com/qqbot/handler/blackjack/view"
|
||||
)
|
||||
|
||||
type BackJackGame struct {
|
||||
simulator *controller.BackJackSimulator
|
||||
viewer *view.LiteralViewer
|
||||
}
|
||||
|
||||
func NewBlackJackGame(simulator *controller.BackJackSimulator, viewer *view.LiteralViewer) *BackJackGame {
|
||||
return &BackJackGame{
|
||||
simulator: simulator,
|
||||
viewer: viewer,
|
||||
}
|
||||
}
|
||||
|
||||
func (game *BackJackGame) AddCommand(command string) (string, error) {
|
||||
command = strings.Split(command, " ")[0]
|
||||
response := ""
|
||||
err := error(nil)
|
||||
|
||||
switch command {
|
||||
case "start":
|
||||
{
|
||||
game.simulator.Init()
|
||||
response += game.viewer.GetResponse()
|
||||
response += "[🐧] You have started a new Blackjack game.\n"
|
||||
response += "[🐧] Type hit to hit.\n"
|
||||
response += "[🐧] Type stand to stand.\n"
|
||||
response += "[🐧] Type start to restart a new game.\n"
|
||||
response += "[🐧] Type exit to leave the game.\n"
|
||||
}
|
||||
|
||||
case "hit":
|
||||
{
|
||||
result := game.simulator.Hit()
|
||||
response += game.viewer.GetResponse()
|
||||
response += result.Description
|
||||
if result.Code != 200 {
|
||||
err = errors.New("game over")
|
||||
}
|
||||
}
|
||||
|
||||
case "stand":
|
||||
{
|
||||
result := game.simulator.Stand()
|
||||
response += game.viewer.GetResponse()
|
||||
response += "[🐧] You decide to stand !\n"
|
||||
response += result.Description
|
||||
if result.Code != 200 {
|
||||
err = errors.New("game over")
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
response += "[🐧] Invalid command !\n"
|
||||
err = errors.New("invalid command")
|
||||
}
|
||||
}
|
||||
|
||||
return response, err
|
||||
}
|
||||
|
||||
func (game *BackJackGame) Close() {
|
||||
game.viewer = nil
|
||||
game.simulator = nil
|
||||
}
|
465
handler/blackjack/doc.md
Normal file
465
handler/blackjack/doc.md
Normal file
@ -0,0 +1,465 @@
|
||||
# 基于 Go 语言的 21 点游戏 RPC 交互系统
|
||||
|
||||
南京大学 计算机科学与技术学院 马一鸣 502024330038
|
||||
|
||||
## 项目概述
|
||||
|
||||
本项目旨在使用 Go 语言设计并实现一款基于远程过程调用(RPC)机制进行通信的 Blackjack(即 21 点)游戏系统。系统由用户端和服务器端组成,用户可以通过用户端与服务器端的 AI 发牌员进行对战。整个系统的设计目标是提供流畅且公平的游戏体验,并通过可靠的通信机制保证用户与服务器之间的有效互动。
|
||||
|
||||
## 需求分析
|
||||
|
||||
本项目的需求分析如下:
|
||||
|
||||
1. **通信机制:** 使用 RPC 作为用户和服务器之间的数据交互机制,以实现低延迟和高可靠性的远程通信。
|
||||
2. **游戏规则:** 游戏采用 Blackjack 的基本规则,玩家与发牌员对战,以接近 21 点为目标。为了提高游戏性和互动性。
|
||||
3. **策略设计:** 发牌员的策略可灵活设计,但必须遵循公平原则,保证游戏的可玩性。策略的制定应考虑不同场景下的最优选择,以增强游戏的挑战性。
|
||||
4. **用户体验:** 系统应具备良好的用户交互设计,保证玩家在操作过程中的易用性和流畅性。
|
||||
|
||||
## 概要设计
|
||||
|
||||
本项目基于 Go 语言实现 Blackjack 游戏系统,主要分为用户端和服务器端两部分。
|
||||
|
||||
- **用户端:** 用户端提供玩家操作界面,通过 RPC 机制向服务器端发送请求,进行拿牌、停牌等操作,并接收游戏结果。
|
||||
- **服务器端:** 服务器端负责处理所有的游戏逻辑,包括初始化牌组、为玩家和发牌员发牌、计算点数、判断胜负等。服务器端通过 RPC 服务对外提供功能。
|
||||
- **通信机制:** 使用 Go 的 `net/rpc` 库实现用户端与服务器端的通信,保证数据传输的安全性与实时性。
|
||||
|
||||
## 详细设计
|
||||
|
||||
本项目的代码结构如下:
|
||||
|
||||
``` shell
|
||||
❯ tree .
|
||||
.
|
||||
├── client
|
||||
│ └── client.go
|
||||
├── controller
|
||||
│ ├── response.go
|
||||
│ └── simulator.go
|
||||
├── core
|
||||
│ └── game.go
|
||||
├── doc.md
|
||||
├── go.mod
|
||||
├── main.go
|
||||
├── model
|
||||
│ ├── card.go
|
||||
│ └── deck.go
|
||||
├── rpc
|
||||
│ ├── request.go
|
||||
│ └── response.go
|
||||
├── server
|
||||
│ └── server.go
|
||||
├── util
|
||||
│ ├── dict.go
|
||||
│ └── status.go
|
||||
└── view
|
||||
└── viewer.go
|
||||
|
||||
9 directories, 15 files
|
||||
```
|
||||
|
||||
其中 21 点游戏的模拟部分遵循 `MVC` 设计模式。
|
||||
|
||||
* `model` 包负责对 21 点游戏中出现的各类对象(如卡牌 Card,卡组 Deck)等进行抽象。
|
||||
* `view` 包负责将游戏的交互逻辑进行呈现。
|
||||
* `controller` 包负责对于游戏的控制逻辑进行抽象,如发牌,要牌,判定胜负等交互逻辑。
|
||||
* `game` 包负责将游戏整体进行封装,并实现了基于命令模式等指令解析模块。
|
||||
|
||||
对于 RPC 通信实现了如下包:
|
||||
|
||||
* `rpc` 包封装了 RPC 通信所需要的结构体。
|
||||
* `server` 包实现了 RPC 通信的服务端构建。
|
||||
* `client` 包实现了 RPC 通信的客户端构建。
|
||||
|
||||
此外 `util` 包实现了各类轻量化工具代码。
|
||||
|
||||
### 21 点游戏的模拟设计
|
||||
|
||||
#### 牌组设计
|
||||
|
||||
系统中的牌组包含 52 张牌,由四种花色(红桃、方块、梅花和黑桃)组成,每种花色有 13 张牌(2 到 10、J、Q、K、A)。每张牌的点数如下:
|
||||
|
||||
- **数字牌 (2-10):** 牌的点数等于其面值。
|
||||
- **人头牌 (J、Q、K):** 每张牌的点数为 10。
|
||||
- **A 牌:** 可以视为 1 分或 11 分,具体取决于对玩家更有利的值。
|
||||
|
||||
在 `model/card.go` 中实现了对于 `Card` 结构体的封装:
|
||||
|
||||
```go
|
||||
type Card struct {
|
||||
Color string
|
||||
Value []int
|
||||
Name string
|
||||
Visible bool
|
||||
}
|
||||
```
|
||||
|
||||
在 `model/deck.go` 中实现了对于 `Deck` 结构体的封装:
|
||||
|
||||
```go
|
||||
type Deck struct {
|
||||
cards []*Card
|
||||
}
|
||||
```
|
||||
|
||||
一副卡组(Deck)由若干卡牌(Card)组成。与此同时,在 `Deck` 中封装了若干 API,最为重要的是抽牌 `Deck.Draw()` 与洗牌 `Deck.Shuffle()` API:
|
||||
|
||||
```go
|
||||
func (deck *Deck) Shuffle() *Deck {
|
||||
for i := range deck.cards {
|
||||
j := i + rand.Intn(len(deck.cards) - i)
|
||||
deck.cards[i], deck.cards[j] = deck.cards[j], deck.cards[i]
|
||||
}
|
||||
return deck
|
||||
}
|
||||
|
||||
func (deck *Deck) Draw() *Card {
|
||||
if len(deck.cards) == 0 {
|
||||
return nil
|
||||
}
|
||||
card := deck.cards[0]
|
||||
deck.cards = deck.cards[1:]
|
||||
return card
|
||||
}
|
||||
```
|
||||
|
||||
#### 游戏流程设计
|
||||
|
||||
在 `controller/simulator.go` 中封装了游戏的交互逻辑:
|
||||
|
||||
```go
|
||||
type BackJackSimulator struct {
|
||||
deck *model.Deck
|
||||
dealerCards *model.Deck
|
||||
playerCards *model.Deck
|
||||
dealerScore int
|
||||
playerScore int
|
||||
status util.Status
|
||||
}
|
||||
```
|
||||
|
||||
通过维护 `deck`,`dealerCards` 与 `playerCards` 三副卡组用来模拟 21 点的游戏流程。
|
||||
|
||||
设计实现游戏状态的初始化逻辑:
|
||||
|
||||
```go
|
||||
func (simulator *BackJackSimulator) Init() *BackJackSimulator {
|
||||
simulator.deck = model.NewDeck()
|
||||
simulator.dealerCards = model.NewDeck()
|
||||
simulator.playerCards = model.NewDeck()
|
||||
|
||||
simulator.deck = initDeck(simulator.deck)
|
||||
simulator.dealerCards = initDealerCards(simulator.dealerCards, simulator.deck)
|
||||
simulator.playerCards = initPlayerCards(simulator.playerCards, simulator.deck)
|
||||
|
||||
simulator.dealerScore, simulator.playerScore = 0, 0
|
||||
simulator.status = util.INITIALIZED
|
||||
|
||||
return simulator
|
||||
}
|
||||
```
|
||||
|
||||
值得注意的是,在 `BackJackSimulator` 中基于状态机模型对游戏的状态进行管理。
|
||||
|
||||
在游戏中共包含了四个不同的状态,分别是初始化(INITIALIZED),闲家胜利(WINNED),庄家胜利(FAILED)以及平局(DRAW),相关状态封装在 `util/status.go` 中:
|
||||
|
||||
```go
|
||||
type Status int
|
||||
|
||||
const (
|
||||
INITIALIZED Status = iota
|
||||
WINNED Status = iota
|
||||
FAILED Status = iota
|
||||
DRAW Status = iota
|
||||
)
|
||||
```
|
||||
|
||||
游戏的流程如下:
|
||||
|
||||
- **发牌阶段:** 游戏开始时,玩家和发牌员各自得到两张牌。玩家的两张牌均为明牌,发牌员有一张明牌和一张暗牌(底牌),例如发牌员的发牌逻辑如下:
|
||||
|
||||
```go
|
||||
if firstCard := deck.Draw(); deck != nil {
|
||||
dealerCards.Add(firstCard.Flip())
|
||||
}
|
||||
if secondCard := deck.Draw(); deck != nil {
|
||||
dealerCards.Add(secondCard)
|
||||
}
|
||||
```
|
||||
|
||||
- **要牌阶段:** 玩家可以选择“要牌”(Hit,即再抽一张牌)或“停牌”(Stand,保持当前手牌)。玩家可以多次选择“要牌”,直到总点数超过 21 点(称为“爆牌”)或选择“停牌”。
|
||||
|
||||
对于要牌逻辑与爆牌判定逻辑实现如下:
|
||||
|
||||
```go
|
||||
if card := simulator.deck.Draw(); card != nil {
|
||||
simulator.playerCards.Add(card)
|
||||
simulator.playerScore = 0
|
||||
|
||||
for _, card := range simulator.playerCards.Cards() {
|
||||
simulator.playerScore += card.Value[0]
|
||||
}
|
||||
|
||||
if simulator.playerScore > 21 {
|
||||
simulator.status = util.FAILED
|
||||
return NewBlackJackResponse(400, "[🐧] Your total score exceeds 21 ! You lose !\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
值得注意的是在 `simulator` 中封装了 `BlackJackResponse` 结构体以返回游戏的状态反馈信息:
|
||||
|
||||
```go
|
||||
type BackJackResponse struct {
|
||||
Code int
|
||||
Description string
|
||||
}
|
||||
```
|
||||
|
||||
当玩家停止要牌之后即进入停牌阶段。
|
||||
|
||||
- **停牌阶段:** 发牌员揭开底牌后,必须继续“要牌”,直到手牌点数达到或超过 17 点:
|
||||
|
||||
```go
|
||||
for maxDealerScore < 17 {
|
||||
if card := simulator.deck.Draw(); card != nil {
|
||||
simulator.dealerCards.Add(card)
|
||||
if card.Name == "A" {
|
||||
maxDealerScore += card.Value[1]
|
||||
minDealerScore += card.Value[0]
|
||||
} else {
|
||||
maxDealerScore += card.Value[0]
|
||||
minDealerScore += card.Value[0]
|
||||
}
|
||||
}
|
||||
|
||||
if minDealerScore > 21 {
|
||||
simulator.status = util.WINNED
|
||||
return NewBlackJackResponse(200, "[🐧] The dealer burst and you have won the game !\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **胜负判定:** 游戏结束后,根据玩家和发牌员的点数判断胜负。若玩家爆牌则输掉游戏;若发牌员爆牌,则玩家获胜;若双方均未爆牌,则点数接近 21 的一方获胜:
|
||||
|
||||
```go
|
||||
if maxDealerScore > maxPlayerScore {
|
||||
simulator.status = util.FAILED
|
||||
return NewBlackJackResponse(200, "[🐧] The dealer wins the game !\n")
|
||||
} else if maxDealerScore < maxPlayerScore {
|
||||
simulator.status = util.WINNED
|
||||
return NewBlackJackResponse(200, "[🐧] You have won the game !\n")
|
||||
} else if maxDealerScore == maxPlayerScore {
|
||||
simulator.status = util.DRAW
|
||||
return NewBlackJackResponse(200, "[🐧] It is a draw !\n")
|
||||
}
|
||||
```
|
||||
|
||||
#### 游戏界面设计
|
||||
|
||||
基于命令行对于游戏的界面进行设计。
|
||||
|
||||
在 `view/viewer.go` 中实现界面结构体:
|
||||
|
||||
```go
|
||||
type LiteralViewer struct {
|
||||
response string
|
||||
simulator *controller.BackJackSimulator
|
||||
}
|
||||
```
|
||||
|
||||
该结构体通过 `GetResponse` 结构输出游戏的状态信息。
|
||||
|
||||
将上述基于 MVC 架构的代码封装在 `BackJackGame` 结构体中:
|
||||
|
||||
```go
|
||||
type BackJackGame struct {
|
||||
simulator *controller.BackJackSimulator
|
||||
viewer *view.LiteralViewer
|
||||
}
|
||||
```
|
||||
|
||||
该结构通过命令模式对游戏逻辑进行控制:
|
||||
|
||||
```go
|
||||
func (game *BackJackGame) AddCommand(command string) string {
|
||||
command = strings.Split(command, " ")[0]
|
||||
response := ""
|
||||
|
||||
switch command {
|
||||
case "start": {}
|
||||
case "hit": {}
|
||||
case "stand": {}
|
||||
case "help": {}
|
||||
default: {}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
```
|
||||
|
||||
### RPC 通信的客户端与服务器端设计
|
||||
|
||||
#### 服务端设计
|
||||
|
||||
服务器端的核心任务是处理游戏逻辑,并通过 RPC 服务向用户端提供接口。
|
||||
|
||||
服务端的核心代码如下:
|
||||
|
||||
```go
|
||||
commandService := new(CommandService)
|
||||
rpc.Register(commandService)
|
||||
|
||||
listener, err := net.Listen("tcp", ":1234")
|
||||
if err != nil {
|
||||
fmt.Println("[🐧] Listening Failed:", err)
|
||||
return
|
||||
}
|
||||
defer listener.Close()
|
||||
fmt.Println("...")
|
||||
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
fmt.Println("[🐧] Connecting Failed:", err)
|
||||
continue
|
||||
}
|
||||
go rpc.ServeConn(conn)
|
||||
}
|
||||
```
|
||||
|
||||
上述代码实现了一个简单的 RPC 服务器,使用了 `Go` 语言中的 `net` 和 `rpc` 包。
|
||||
|
||||
在 `main` 函数中创建一个 `CommandService` 实例并将 `commandService` 实例注册到 RPC 服务器,使其公开方法可以被远程调用。
|
||||
|
||||
服务器开始监听 TCP 连接,端口号为 1234。`net.Listen()` 函数返回一个 listener(监听器)对象和一个错误(err)。
|
||||
|
||||
使用 `defer` 关键字确保在函数退出时关闭监听器,释放资源。
|
||||
|
||||
服务器等待客户端连接。当有客户端连接时,`listener.Accept()` 会返回一个表示该连接的 `conn` 对象。
|
||||
|
||||
通过 `go` 关键字保证每个连接都通过一个新的 `goroutine` 来处理,`rpc.ServeConn()` 用于处理该连接上的 RPC 请求。
|
||||
|
||||
对于 `CommandService` 结构体封装了调用方法如下:
|
||||
|
||||
```go
|
||||
func (cs *CommandService) ExecuteCommand(req brpc.Request, res *brpc.Response) error {
|
||||
command := req.Command
|
||||
response := backJackGame.AddCommand(command)
|
||||
res.Result = fmt.Sprint(response)
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
#### 客户端设计
|
||||
|
||||
客户端负责与玩家交互,并通过 RPC 向服务器发送请求。
|
||||
|
||||
客户端的核心代码如下:
|
||||
|
||||
```go
|
||||
client, err := rpc.Dial("tcp", "localhost:1234")
|
||||
if err != nil {
|
||||
fmt.Println("[🐳] Connecting Failed:", err)
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
for {
|
||||
command, _ := reader.ReadString('\n')
|
||||
command = command[:len(command)-1]
|
||||
|
||||
if command == "exit" {
|
||||
break
|
||||
}
|
||||
|
||||
req := brpc.Request{Command: command}
|
||||
var res brpc.Response
|
||||
|
||||
err = client.Call("CommandService.ExecuteCommand", req, &res)
|
||||
if err != nil {
|
||||
fmt.Println("[🐳] RPC Failed:", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
上述代码使用 `rpc.Dial()` 函数创建一个 RPC 客户端,并通过 TCP 连接到 localhost 上的端口 1234。
|
||||
|
||||
使用 `defer` 关键字确保在函数结束时关闭 RPC 客户端,释放资源。
|
||||
|
||||
创建一个从标准输入读取用户输入的 `bufio.Reader`,用于从命令行接收用户输入。
|
||||
|
||||
通过 `client.Call()` 调用服务器端的 `CommandService.ExecuteCommand` 方法。
|
||||
|
||||
## 测试与验证
|
||||
|
||||
欲运行服务端和客户端程序需要分别编译运行客户端与服务端代码:
|
||||
|
||||
```shell
|
||||
$ go run server.go
|
||||
```
|
||||
|
||||
```shell
|
||||
$ go run client.go
|
||||
```
|
||||
|
||||
其中客户端的交互页面如下:
|
||||
|
||||
```shell
|
||||
go run client.go
|
||||
[🐳] Input your Command (Input <exit> to quit):
|
||||
start
|
||||
```
|
||||
|
||||
在输入 `start` 命令后即可开启一局 21 点游戏:
|
||||
|
||||
```shell
|
||||
[🐳] Input your Command (Input <exit> to quit):
|
||||
start
|
||||
[🃏] Dealer: 🂠 ♥️10
|
||||
[🎴] Player: ♦3 ♦7
|
||||
[🐧] You have started a new Blackjack game.
|
||||
```
|
||||
|
||||
输入 `hit` 命令进行要牌操作:
|
||||
|
||||
```shell
|
||||
[🐳] Input your Command (Input <exit> to quit):
|
||||
hit
|
||||
[🃏] Dealer: 🂠 ♥️10
|
||||
[🎴] Player: ♦3 ♦7 ♠A
|
||||
[🐧] You have drawn a card !
|
||||
```
|
||||
|
||||
输入 `stand` 命令进行停牌操作:
|
||||
|
||||
```shell
|
||||
[🐳] Input your Command (Input <exit> to quit):
|
||||
stand
|
||||
[🃏] Dealer: ♠K ♥️10
|
||||
[🎴] Player: ♦3 ♦7 ♠A
|
||||
[🐧] You decide to stand !
|
||||
[🐧] You have won the game !
|
||||
```
|
||||
|
||||
此时服务端也会输出对应的信息:
|
||||
|
||||
```shell
|
||||
...
|
||||
[🐧] Command Received: start
|
||||
[🐧] Command Received: hit
|
||||
[🐧] Command Received: stand
|
||||
```
|
||||
|
||||
通过多轮测试与验证,本系统满足了 21 点游戏的交互逻辑的正确性。
|
||||
|
||||
# 结论
|
||||
|
||||
本项目通过使用 Go 语言和 RPC 通信机制,本项目实现了一款完整且可扩展的 21 点游戏系统。系统通过用户端与服务器端的分离,实现了逻辑处理和用户交互的独立性,从而提高了系统的扩展性和可维护性。项目通过详细的需求分析、概要设计和详细设计,确保了系统的功能完整性和用户体验。同时,通过系统测试,验证了系统的可靠性和稳定性。
|
||||
|
||||
未来可以考虑在以下方面进行改进:
|
||||
- 增加复杂的游戏规则,例如双倍下注、投降等,以提高游戏的策略性和娱乐性。
|
||||
- 优化发牌员的策略,使其更具挑战性。
|
||||
- 增加图形化用户界面,以提供更直观的游戏体验。
|
||||
- 使用更加安全和高效的通信机制,以提高数据传输的安全性和系统性能。
|
22
handler/blackjack/model/card.go
Normal file
22
handler/blackjack/model/card.go
Normal file
@ -0,0 +1,22 @@
|
||||
package model
|
||||
|
||||
type Card struct {
|
||||
Color string
|
||||
Value []int
|
||||
Name string
|
||||
Visible bool
|
||||
}
|
||||
|
||||
func NewCard(color string, name string, value []int, visible bool) *Card {
|
||||
return &Card{
|
||||
Color: color,
|
||||
Value: value,
|
||||
Name: name,
|
||||
Visible: visible,
|
||||
}
|
||||
}
|
||||
|
||||
func (card *Card) Flip() *Card {
|
||||
card.Visible = !card.Visible
|
||||
return card
|
||||
}
|
63
handler/blackjack/model/deck.go
Normal file
63
handler/blackjack/model/deck.go
Normal file
@ -0,0 +1,63 @@
|
||||
package model
|
||||
|
||||
import "math/rand"
|
||||
|
||||
type Deck struct {
|
||||
cards []*Card
|
||||
}
|
||||
|
||||
func NewDeck() *Deck {
|
||||
deck := &Deck{
|
||||
cards: []*Card{},
|
||||
}
|
||||
return deck
|
||||
}
|
||||
|
||||
func (deck *Deck) Cards() []*Card {
|
||||
return deck.cards
|
||||
}
|
||||
|
||||
func (deck *Deck) Clear() *Deck {
|
||||
deck.cards = []*Card{}
|
||||
return deck;
|
||||
}
|
||||
|
||||
func (deck *Deck) Length() int {
|
||||
return len(deck.cards)
|
||||
}
|
||||
|
||||
func (deck *Deck) Add(card *Card) *Deck {
|
||||
deck.cards = append(deck.cards, card)
|
||||
return deck
|
||||
}
|
||||
|
||||
func (deck *Deck) Shuffle() *Deck {
|
||||
for i := range deck.cards {
|
||||
j := i + rand.Intn(len(deck.cards) - i)
|
||||
deck.cards[i], deck.cards[j] = deck.cards[j], deck.cards[i]
|
||||
}
|
||||
return deck
|
||||
}
|
||||
|
||||
func (deck *Deck) Draw() *Card {
|
||||
if len(deck.cards) == 0 {
|
||||
return nil
|
||||
}
|
||||
card := deck.cards[0]
|
||||
deck.cards = deck.cards[1:]
|
||||
return card
|
||||
}
|
||||
|
||||
func (deck *Deck) Contains(color string, num int) bool {
|
||||
for _, card := range deck.cards {
|
||||
if card.Color == color {
|
||||
for _, value := range card.Value {
|
||||
if value == num {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
11
handler/blackjack/util/dict.go
Normal file
11
handler/blackjack/util/dict.go
Normal file
@ -0,0 +1,11 @@
|
||||
package util
|
||||
|
||||
var Dict map[string]string
|
||||
|
||||
func init() {
|
||||
Dict := make(map[string]string)
|
||||
Dict["Spade"] = "♠"
|
||||
Dict["Heart"] = "♥"
|
||||
Dict["Diamond"] = "♦"
|
||||
Dict["Club"] = "♣"
|
||||
}
|
10
handler/blackjack/util/status.go
Normal file
10
handler/blackjack/util/status.go
Normal file
@ -0,0 +1,10 @@
|
||||
package util
|
||||
|
||||
type Status int
|
||||
|
||||
const (
|
||||
INITIALIZED Status = iota
|
||||
WINNED Status = iota
|
||||
FAILED Status = iota
|
||||
DRAW Status = iota
|
||||
)
|
52
handler/blackjack/view/viewer.go
Normal file
52
handler/blackjack/view/viewer.go
Normal file
@ -0,0 +1,52 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"git.lxtend.com/qqbot/handler/blackjack/controller"
|
||||
"git.lxtend.com/qqbot/handler/blackjack/model"
|
||||
)
|
||||
|
||||
func getEmojiOfColor(color string) string {
|
||||
switch color {
|
||||
case "Spade":
|
||||
return "♠"
|
||||
case "Heart":
|
||||
return "♥️"
|
||||
case "Diamond":
|
||||
return "♦"
|
||||
case "Club":
|
||||
return "♣"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
type LiteralViewer struct {
|
||||
response string
|
||||
simulator *controller.BackJackSimulator
|
||||
}
|
||||
|
||||
func NewLiteralViewer(simulator *controller.BackJackSimulator) *LiteralViewer {
|
||||
return &LiteralViewer{
|
||||
simulator: simulator,
|
||||
response: "",
|
||||
}
|
||||
}
|
||||
|
||||
func generateCardSeries(deck *model.Deck) string {
|
||||
cards := ""
|
||||
for _, card := range deck.Cards() {
|
||||
if card.Visible {
|
||||
cards += getEmojiOfColor(card.Color) + card.Name + " "
|
||||
} else {
|
||||
cards += "🂠 "
|
||||
}
|
||||
}
|
||||
return cards
|
||||
}
|
||||
|
||||
func (viewer *LiteralViewer) GetResponse() string {
|
||||
viewer.response = ""
|
||||
viewer.response += "[🃏] Dealer: " + generateCardSeries(viewer.simulator.GetDealerCards()) + "\n"
|
||||
viewer.response += "[🎴] Player: " + generateCardSeries(viewer.simulator.GetPlayerCards()) + "\n"
|
||||
return viewer.response
|
||||
}
|
@ -20,6 +20,7 @@ func wordle(msg model.Message) (reply model.Reply) {
|
||||
}
|
||||
|
||||
func tempTrigger(msg model.Message) (reply model.Reply, isTrigger bool) {
|
||||
handler.UnRegisterLiveHandler(msg.GroupInfo.GroupId, msg.UserId)
|
||||
return model.Reply{
|
||||
ReplyMsg: " ",
|
||||
ReferOriginMsg: true,
|
||||
|
@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
_ "git.lxtend.com/qqbot/handler/auth"
|
||||
_ "git.lxtend.com/qqbot/handler/beatleader"
|
||||
_ "git.lxtend.com/qqbot/handler/blackjack"
|
||||
_ "git.lxtend.com/qqbot/handler/drawback"
|
||||
_ "git.lxtend.com/qqbot/handler/echo"
|
||||
_ "git.lxtend.com/qqbot/handler/exec"
|
||||
|
Loading…
x
Reference in New Issue
Block a user