---
# System prepended metadata

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

---

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



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

## 安裝 go-redis

[**go-redis**](https://github.com/go-redis/redis)目前主要有`v6`版跟`v8`版，兩者的語法使用上不相同。

#### go get 全域安裝
V6版本

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

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

這裡會先介紹[**v6**的版本](https://github.com/go-redis/redis/tree/v6.15.9)  


在這邊稍加修改Github上的[**Quickstart**](https://github.com/go-redis/redis/tree/v6.15.9#quickstart)，並且運行  


```go
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來計分，當作玩家擁有的錢。


#### 一開始先宣告會用到的物件，〝玩家〞以及 〝玩家下注〞  
```go
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"`	// 下注金額
}
```

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

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

#### 獲取bet的玩家帳號 及金額
```go
user.Id = c.Param("user")
amountStr := c.Param("amount")
```

#### go-Redis 操作

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

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

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

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

查詢`BetThisRound`獲取此局目前的獎金池  
```go
bets, _ := RC.ZRangeWithScores(BetThisRound, 0, -1).Result()
for _, bet := range bets {
	var userBet UserBet
	userBet.Amount = int(bet.Score)
	prizePool += userBet.Amount
}
```

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

## 完整遊戲小程式
```go
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  

