From 53da17802a06cbc24a3c2ccfdaf43f78977bb9db Mon Sep 17 00:00:00 2001 From: lixiangwuxian Date: Fri, 1 Nov 2024 00:46:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A021=E7=82=B9=E6=B8=B8?= =?UTF-8?q?=E6=88=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- handler/blackjack/blackjack.go | 104 +++++ handler/blackjack/controller/response.go | 13 + handler/blackjack/controller/simulator.go | 183 +++++++++ handler/blackjack/core/game.go | 74 ++++ handler/blackjack/doc.md | 465 ++++++++++++++++++++++ handler/blackjack/model/card.go | 22 + handler/blackjack/model/deck.go | 63 +++ handler/blackjack/util/dict.go | 11 + handler/blackjack/util/status.go | 10 + handler/blackjack/view/viewer.go | 52 +++ handler/wordle/wordle.go | 1 + register.go | 1 + 12 files changed, 999 insertions(+) create mode 100644 handler/blackjack/blackjack.go create mode 100644 handler/blackjack/controller/response.go create mode 100644 handler/blackjack/controller/simulator.go create mode 100644 handler/blackjack/core/game.go create mode 100644 handler/blackjack/doc.md create mode 100644 handler/blackjack/model/card.go create mode 100644 handler/blackjack/model/deck.go create mode 100644 handler/blackjack/util/dict.go create mode 100644 handler/blackjack/util/status.go create mode 100644 handler/blackjack/view/viewer.go diff --git a/handler/blackjack/blackjack.go b/handler/blackjack/blackjack.go new file mode 100644 index 0000000..fbdcbd7 --- /dev/null +++ b/handler/blackjack/blackjack.go @@ -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 +} diff --git a/handler/blackjack/controller/response.go b/handler/blackjack/controller/response.go new file mode 100644 index 0000000..bb4b903 --- /dev/null +++ b/handler/blackjack/controller/response.go @@ -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, + } +} diff --git a/handler/blackjack/controller/simulator.go b/handler/blackjack/controller/simulator.go new file mode 100644 index 0000000..d0a007e --- /dev/null +++ b/handler/blackjack/controller/simulator.go @@ -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 +} diff --git a/handler/blackjack/core/game.go b/handler/blackjack/core/game.go new file mode 100644 index 0000000..9c843ee --- /dev/null +++ b/handler/blackjack/core/game.go @@ -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 +} diff --git a/handler/blackjack/doc.md b/handler/blackjack/doc.md new file mode 100644 index 0000000..890a107 --- /dev/null +++ b/handler/blackjack/doc.md @@ -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 to quit): +start +``` + +在输入 `start` 命令后即可开启一局 21 点游戏: + +```shell +[🐳] Input your Command (Input to quit): +start +[🃏] Dealer: 🂠 ♥️10 +[🎴] Player: ♦3 ♦7 +[🐧] You have started a new Blackjack game. +``` + +输入 `hit` 命令进行要牌操作: + +```shell +[🐳] Input your Command (Input to quit): +hit +[🃏] Dealer: 🂠 ♥️10 +[🎴] Player: ♦3 ♦7 ♠A +[🐧] You have drawn a card ! +``` + +输入 `stand` 命令进行停牌操作: + +```shell +[🐳] Input your Command (Input 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 点游戏系统。系统通过用户端与服务器端的分离,实现了逻辑处理和用户交互的独立性,从而提高了系统的扩展性和可维护性。项目通过详细的需求分析、概要设计和详细设计,确保了系统的功能完整性和用户体验。同时,通过系统测试,验证了系统的可靠性和稳定性。 + +未来可以考虑在以下方面进行改进: +- 增加复杂的游戏规则,例如双倍下注、投降等,以提高游戏的策略性和娱乐性。 +- 优化发牌员的策略,使其更具挑战性。 +- 增加图形化用户界面,以提供更直观的游戏体验。 +- 使用更加安全和高效的通信机制,以提高数据传输的安全性和系统性能。 diff --git a/handler/blackjack/model/card.go b/handler/blackjack/model/card.go new file mode 100644 index 0000000..e679135 --- /dev/null +++ b/handler/blackjack/model/card.go @@ -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 +} diff --git a/handler/blackjack/model/deck.go b/handler/blackjack/model/deck.go new file mode 100644 index 0000000..980481e --- /dev/null +++ b/handler/blackjack/model/deck.go @@ -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 +} diff --git a/handler/blackjack/util/dict.go b/handler/blackjack/util/dict.go new file mode 100644 index 0000000..04c01f4 --- /dev/null +++ b/handler/blackjack/util/dict.go @@ -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"] = "♣" +} diff --git a/handler/blackjack/util/status.go b/handler/blackjack/util/status.go new file mode 100644 index 0000000..fab2822 --- /dev/null +++ b/handler/blackjack/util/status.go @@ -0,0 +1,10 @@ +package util + +type Status int + +const ( + INITIALIZED Status = iota + WINNED Status = iota + FAILED Status = iota + DRAW Status = iota +) diff --git a/handler/blackjack/view/viewer.go b/handler/blackjack/view/viewer.go new file mode 100644 index 0000000..a5b18c4 --- /dev/null +++ b/handler/blackjack/view/viewer.go @@ -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 +} diff --git a/handler/wordle/wordle.go b/handler/wordle/wordle.go index 101d3e3..bb731c9 100644 --- a/handler/wordle/wordle.go +++ b/handler/wordle/wordle.go @@ -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, diff --git a/register.go b/register.go index da5bb30..20f46f0 100644 --- a/register.go +++ b/register.go @@ -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"