iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 30
6
Software Development

Go繁不及備載系列 第 30

# Day30 Golang 鍵值資料庫 Redis 實作抽獎小遊戲

Day30 Golang 鍵值資料庫 Redis 實作抽獎小遊戲

接下來要用程式來模擬Client執行的動作。

安裝 go-redis

go-redis目前主要有v6版跟v8版,兩者的語法使用上不相同。

go get 全域安裝

V6版本

$ go get github.com/go-redis/redis

V8版本
$ go get github.com/go-redis/redis/v8

這裡會先介紹v6的版本

在這邊稍加修改Github上的Quickstart,並且運行

package main

import (
	"fmt"
	"github.com/go-redis/redis"
)

func main() {
	c := NewClient()
	test(c)
}
func NewClient() *redis.Client { // 實體化redis.Client 並返回實體的位址
	client := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "", // no password set
		DB:       0,  // use default DB
	})

	pong, err := client.Ping().Result()
	fmt.Println(pong, err)
	return client
}

func test(c *redis.Client) { // 對該 redis.Client 進行操作
	err := c.Set("key", "value", 0).Err() // => SET key value 0 數字代表過期秒數,在這裡0為永不過期
	if err != nil {
		panic(err)
	}

	val, err := c.Get("key").Result()	// => GET key
	if err != nil {
		panic(err)
	}
	fmt.Println("key", val)

	val2, err := c.Get("key2").Result()	// => GET key2
	if err == redis.Nil {
		fmt.Println("key2 does not exist")
	} else if err != nil {
		panic(err)
	} else {
		fmt.Println("key2", val2)
	}
}

事不宜遲,直接入主題:今晚,我想來點...

答對了!就是抽獎小遊戲!

抽獎遊戲

既然Redis能在短時間內快速讀寫,我想透過他、並且加上Gin網頁框架,來製做一個迷你遊戲。

  • 每位玩家註冊後預設有1000塊。可以投注任意正整數金額,投越多錢、中獎機率越高。
  • 伺服器每分鐘會抽一位幸運者出來,並把這局所有的錢給予這名幸運者。

很適合用Redis中 Sorted-Set 這個類型的Score來計分,當作玩家擁有的錢。

一開始先宣告會用到的物件,〝玩家〞以及 〝玩家下注〞

type User struct {
	Id      string `json:"Id"`	// 玩家 ID
	Balance int    `json:"balance"`	// 玩家餘額
}

type UserBet struct {
	Id     string `json:"Id"`	
	Round  int    `json:"round"`	// 局數
	Amount int    `json:"amount"`	// 下注金額
}

設定一些常數

const (
	RoundSecond    = 60               // 每一局的時間
	DefaultBalance = 1000             // 玩家初始化金額
	UserMember     = "game"           // 儲存所有使用者的Balance   	Redis:`Sorted-Set`	SCORE -> USER
	BetThisRound   = "bet_this_round" // 儲存目前局的下注狀況		 	Redis:`Sorted-Set`  SCORE -> USER
)

Gin的操作

router.GET("/bet/:user", GetUserBalance) // 玩家註冊(不須密碼,填入帳號即可)`user`區分大小寫
router.GET("/bet/:user/:amount", Bet)    // 玩家對目前的局面進行下注,`amount`金額

獲取bet的玩家帳號 及金額

user.Id = c.Param("user")
amountStr := c.Param("amount")

go-Redis 操作

下注前,先對用戶做查詢,查看玩家餘額足不足夠。

balance, err := RC.ZScore(UserMember, user.Id).Result() // => ZSCORE `Table` UserID

如果成功下注,玩家餘額為 目前餘額減去下注金額。
並且用BetThisRound 表來記錄 此局此玩家的權重(籤數,越高越容易中獎)。

RC.ZIncrBy(UserMember, float64(-amount), user.Id)
RC.ZIncrBy(BetThisRound, float64(amount), user.Id)

查詢BetThisRound獲取此局目前的獎金池

bets, _ := RC.ZRangeWithScores(BetThisRound, 0, -1).Result()
for _, bet := range bets {
	var userBet UserBet
	userBet.Amount = int(bet.Score)
	prizePool += userBet.Amount
}

抽出一個幸運兒

winNum := rand.Intn(prizePool + 1)	// 亂數一個幸運號碼
for _, userBet := range userBets {
	winNum -= userBet.Amount
	if winNum <= 0 {
		winner = userBet.Id
		break
	}
}

完整遊戲小程式

package main

import (
	"errors"
	"fmt"
	"log"
	"math/rand"
	"net/http"
	"strconv"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/go-redis/redis"
)

const (
	RoundSecond    = 60               // 每一局的時間
	DefaultBalance = 1000             // 玩家初始化金額
	UserMember     = "game"           // 儲存所有使用者的Balance   	Redis:`Sorted-Set`	SCORE -> USER
	BetThisRound   = "bet_this_round" // 儲存目前局的下注狀況		 	Redis:`Sorted-Set`  SCORE -> USER
)

var Round = 0
var startTimeThisRound time.Time
var RC *redis.Client

type User struct {
	Id      string `json:"Id"`      // 玩家 ID
	Balance int    `json:"balance"` // 玩家餘額
}

type UserBet struct {
	Id     string `json:"Id"`
	Round  int    `json:"round"`  // 局數
	Amount int    `json:"amount"` // 下注金額
}

func init() {
	RC = newClient()

	// 初始化清空所有Redis
	RC.Del(UserMember)
	RC.Del(BetThisRound)

	go GameServer()
}

func main() {
	router := gin.Default()

	router.RedirectFixedPath = true
	router.GET("/bet/:user", GetUserBalance) // 玩家註冊(不須密碼,填入帳號即可)`user`區分大小寫
	router.GET("/bet/:user/:amount", Bet)    // 玩家對目前的局面進行下注,`amount`金額
	router.GET("/prize", GetCurrentPrize)    // 此局目前的獎金池
	router.GET("/bets", GetUserBets)         // 此局所有玩家目前的下注
	router.Run(":80")
}

func newClient() *redis.Client {
	client := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "",
		DB:       0,
	})

	pong, err := client.Ping().Result()
	log.Println(pong)
	if err != nil {
		log.Fatalln(err)
	}
	return client
}

func GameServer() {
	rand.Seed(time.Now().UTC().UnixNano())
	ticker := time.NewTicker(RoundSecond * time.Second) // 每過RoundSecond秒,執行一次以下迴圈
	go func() {
		for {
			Round++
			startTimeThisRound = time.Now()
			log.Println(startTimeThisRound.Format("2006-01-02 15:04:05"), "\t round", Round, "start")
			_ = <-ticker.C

			var prizePool = getCurrentPrize()
			var userBets = getUserBets()
			if len(userBets) == 0 {
				log.Println("Round", Round, "沒有任何玩家下注")
				continue
			}

			// 抽獎選贏家
			winNum := rand.Intn(prizePool + 1)
			var winner string
			for _, userBet := range userBets {
				winNum -= userBet.Amount
				if winNum <= 0 {
					winner = userBet.Id
					break
				}
			}
			log.Println("獎金池:", prizePool, "\t 得主:", winner)

			// 發獎金給得主
			RC.ZIncrBy(UserMember, float64(prizePool), winner)

			// 刪除現有Table
			RC.Del(BetThisRound)
		}
	}()
}

func Bet(c *gin.Context) {
	var user User
	user.Id = c.Param("user")
	amountStr := c.Param("amount")
	amount, err := strconv.Atoi(amountStr)
	if err != nil {
		wrapResponse(c, nil, errors.New("下注金額有誤"))
		return
	}

	balance, err := RC.ZScore(UserMember, user.Id).Result()
	if err == redis.Nil {
		wrapResponse(c, nil, errors.New("查無此用戶,請先註冊"))
		return
	}
	user.Balance = int(balance)
	if amount <= 0 {
		wrapResponse(c, nil, errors.New("下注金額需為正整數"))
		return
	}
	if amount > user.Balance {
		wrapResponse(c, nil, errors.New("餘額不足"))
		return
	}

	user.Balance -= amount
	RC.ZIncrBy(UserMember, float64(-amount), user.Id)
	RC.ZIncrBy(BetThisRound, float64(amount), user.Id)

	wrapResponse(c, user, nil)
}

func GetUserBalance(c *gin.Context) {
	var user User
	user.Id = c.Param("user")
	balance, err := RC.ZScore(UserMember, user.Id).Result()
	if err == redis.Nil { //查無使用者,註冊新帳號
		balance = DefaultBalance
		RC.ZAdd(UserMember, redis.Z{
			Score:  balance,
			Member: user.Id,
		})
	}
	user.Balance = int(balance)
	wrapResponse(c, user, nil)

}

func GetCurrentPrize(c *gin.Context) {
	wrapResponse(c, getCurrentPrize(), nil)
}

func getCurrentPrize() (prizePool int) {
	bets, _ := RC.ZRangeWithScores(BetThisRound, 0, -1).Result()
	for _, bet := range bets {
		var userBet UserBet
		userBet.Amount = int(bet.Score)
		prizePool += userBet.Amount
	}
	return
}

func GetUserBets(c *gin.Context) {
	UserBets := getUserBets()
	if len(UserBets) == 0 {
		wrapResponse(c, nil, errors.New("目前沒有任何記錄"))
		return
	}
	wrapResponse(c, UserBets, nil)
}

func getUserBets() (userBets []UserBet) {
	bets, _ := RC.ZRangeWithScores(BetThisRound, 0, -1).Result()
	for _, bet := range bets {
		var userBet UserBet
		userBet.Id = fmt.Sprintf("%s", bet.Member)
		userBet.Amount = int(bet.Score)
		userBet.Round = Round
		userBets = append(userBets, userBet)
	}
	return
}

func wrapResponse(c *gin.Context, data interface{}, err error) {
	type ret struct {
		Status string      `json:"status"`
		Msg    string      `json:"msg"`
		Data   interface{} `json:"data"`
	}

	d := ret{
		Status: "ok",
		Msg:    "",
		Data:   []struct{}{},
	}

	if data != nil {
		d.Data = data
	}

	if err != nil {
		d.Status = "failed"
		d.Msg = err.Error()
	}

	c.JSON(http.StatusOK, d)
}

玩法

返回的Json 數值皆為使用者的餘額
程式執行起來後,開啟多個瀏覽器,每個瀏覽器作為一個獨立的玩家。

註冊Jack帳號:http://127.0.0.1/bet/Jack
Jack下注50元:http://127.0.0.1/bet/Jack/50
註冊Timmy帳號:http://127.0.0.1/bet/Timmy
Timmy下注333元:http://127.0.0.1/bet/Timmy/333
(接著靜待一分鐘,抽出一名幸運兒。)

查看餘額Jack餘額:http://127.0.0.1/bet/Jack
查看餘額Timmy餘額:http://127.0.0.1/bet/Timmy


上一篇
# Day29 Golang 鍵值資料庫 Redis 介紹與安裝
下一篇
# Day31 Golang Protobuf 介紹與使用
系列文
Go繁不及備載35
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言