refactor: 重构抽奖逻辑以支持可验证凭据 feat(redis): 集成Redis客户端并添加配置支持 fix: 修复订单取消时的优惠券和库存处理逻辑 docs: 添加对对碰游戏前端对接指南和示例JSON test: 添加对对碰游戏模拟测试和验证逻辑
138 lines
3.5 KiB
Go
138 lines
3.5 KiB
Go
package app
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
)
|
|
|
|
// SimulateClientPlay mimics the frontend logic: Match -> Eliminate -> Fill -> Reshuffle
|
|
func SimulateClientPlay(initialCards []*MatchingCard) int {
|
|
// Deep copy to avoid modifying original test data
|
|
deck := make([]*MatchingCard, len(initialCards))
|
|
copy(deck, initialCards)
|
|
|
|
board := make([]*MatchingCard, 9)
|
|
// Initial fill
|
|
for i := 0; i < 9; i++ {
|
|
board[i] = deck[0]
|
|
deck = deck[1:]
|
|
}
|
|
|
|
pairsFound := 0
|
|
|
|
for {
|
|
// 1. Check for matches on board
|
|
counts := make(map[string][]int) // type -> list of indices
|
|
for i, c := range board {
|
|
if c != nil {
|
|
// Cast CardType to string for map key
|
|
counts[string(c.Type)] = append(counts[string(c.Type)], i)
|
|
}
|
|
}
|
|
|
|
matchedType := ""
|
|
for t, indices := range counts {
|
|
if len(indices) >= 2 {
|
|
matchedType = t
|
|
break
|
|
}
|
|
}
|
|
|
|
if matchedType != "" {
|
|
// Elimination
|
|
pairsFound++
|
|
indices := counts[matchedType]
|
|
// Remove first 2
|
|
idx1, idx2 := indices[0], indices[1]
|
|
board[idx1] = nil
|
|
board[idx2] = nil
|
|
|
|
// Filling: Fill empty slots from deck
|
|
for i := 0; i < 9; i++ {
|
|
if board[i] == nil && len(deck) > 0 {
|
|
board[i] = deck[0]
|
|
deck = deck[1:]
|
|
}
|
|
}
|
|
} else {
|
|
// Deadlock (No matches on board)
|
|
// User requirement: "Stop when no pairs can be generated" (i.e., No Reshuffle)
|
|
// If we are stuck, we stop.
|
|
break
|
|
}
|
|
}
|
|
return pairsFound
|
|
}
|
|
|
|
// TestVerification_DataIntegrity simulates the PreOrder logic 10000 times
|
|
func TestVerification_DataIntegrity(t *testing.T) {
|
|
fmt.Println("=== Starting Full Game Simulation (10000 Runs) ===")
|
|
// Using 10k runs to keep test time reasonable
|
|
|
|
configs := []CardTypeConfig{
|
|
{Code: "A", Name: "TypeA", Quantity: 9},
|
|
{Code: "B", Name: "TypeB", Quantity: 9},
|
|
{Code: "C", Name: "TypeC", Quantity: 9},
|
|
{Code: "D", Name: "TypeD", Quantity: 9},
|
|
{Code: "E", Name: "TypeE", Quantity: 9},
|
|
{Code: "F", Name: "TypeF", Quantity: 9},
|
|
{Code: "G", Name: "TypeG", Quantity: 9},
|
|
{Code: "H", Name: "TypeH", Quantity: 9},
|
|
{Code: "I", Name: "TypeI", Quantity: 9},
|
|
{Code: "J", Name: "TypeJ", Quantity: 9},
|
|
{Code: "K", Name: "TypeK", Quantity: 9},
|
|
}
|
|
|
|
scoreDist := make(map[int]int)
|
|
|
|
for i := 0; i < 10000; i++ {
|
|
// 1. Simulate PreOrder generation
|
|
game := NewMatchingGameWithConfig(configs, fmt.Sprintf("pos_%d", i))
|
|
|
|
// 2. Reconstruct "all_cards"
|
|
allCards := make([]*MatchingCard, 0, 99)
|
|
for _, c := range game.Board {
|
|
if c != nil {
|
|
allCards = append(allCards, c)
|
|
}
|
|
}
|
|
allCards = append(allCards, game.Deck...)
|
|
|
|
// 3. Play the game!
|
|
score := SimulateClientPlay(allCards)
|
|
scoreDist[score]++
|
|
// Note: Without reshuffle, score < 44 is expected.
|
|
}
|
|
|
|
// Calculate Stats
|
|
totalScore := 0
|
|
var allScores []int
|
|
for s := 0; s <= 44; s++ {
|
|
count := scoreDist[s]
|
|
for c := 0; c < count; c++ {
|
|
allScores = append(allScores, s)
|
|
totalScore += s
|
|
}
|
|
}
|
|
|
|
mean := float64(totalScore) / float64(len(allScores))
|
|
median := allScores[len(allScores)/2]
|
|
|
|
fmt.Println("\n=== No-Reshuffle Statistical Analysis (10000 Runs) ===")
|
|
fmt.Printf("Mean Score: %.2f / 44\n", mean)
|
|
fmt.Printf("Median Score: %d / 44\n", median)
|
|
fmt.Printf("Pass Rate: %.2f%%\n", float64(scoreDist[44])/100.0)
|
|
fmt.Println("------------------------------------------------")
|
|
|
|
// Output Distribution Segments
|
|
fmt.Println("Detailed Distribution:")
|
|
cumulative := 0
|
|
for s := 0; s <= 44; s++ {
|
|
count := scoreDist[s]
|
|
if count > 0 {
|
|
cumulative += count
|
|
fmt.Printf("Score %d: %d times (%.2f%%) [Cum: %.2f%%]\n", s, count, float64(count)/100.0, float64(cumulative)/100.0)
|
|
}
|
|
}
|
|
}
|