2026-06-11 18:14:04 +08:00

822 lines
30 KiB
Go

package dice
import (
"context"
"strings"
"testing"
"time"
walletv1 "hyapp.local/api/proto/wallet/v1"
"hyapp/pkg/xerr"
dicedomain "hyapp/services/game-service/internal/domain/dice"
gamedomain "hyapp/services/game-service/internal/domain/game"
)
func TestRollMatchDebitsRollsAndPaysWinner(t *testing.T) {
repo := newFakeDiceRepository()
wallet := &fakeDiceWallet{balances: map[int64]int64{101: 1000, 202: 1000}}
svc := New(Config{FeeBPS: 500}, repo, wallet)
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
rolls := []int{5, 0}
svc.randomInt = func(max int) (int, error) {
if len(rolls) == 0 {
t.Fatal("randomInt called too many times")
}
value := rolls[0]
rolls = rolls[1:]
return value, nil
}
created, _, err := svc.CreateMatch(context.Background(), CreateMatchCommand{
AppCode: "lalu",
RequestID: "req-create",
UserID: 101,
GameID: "dice",
RoomID: "room_1",
RegionID: 100,
StakeCoin: 100,
MinPlayers: 2,
MaxPlayers: 3,
})
if err != nil {
t.Fatalf("CreateMatch failed: %v", err)
}
if _, _, err := svc.JoinMatch(context.Background(), JoinMatchCommand{AppCode: "lalu", RequestID: "req-join", UserID: 202, MatchID: created.MatchID}); err != nil {
t.Fatalf("JoinMatch failed: %v", err)
}
settled, _, err := svc.RollMatch(context.Background(), RollMatchCommand{AppCode: "lalu", RequestID: "req-roll", UserID: 101, MatchID: created.MatchID})
if err != nil {
t.Fatalf("RollMatch failed: %v", err)
}
if settled.Status != dicedomain.MatchStatusSettled || settled.Result != dicedomain.ParticipantResultWin {
t.Fatalf("match not settled as win: %+v", settled)
}
if len(settled.Participants) != 2 {
t.Fatalf("participants len = %d", len(settled.Participants))
}
winner := participantByUser(settled.Participants, 101)
loser := participantByUser(settled.Participants, 202)
if len(winner.DicePoints) != 1 || winner.DicePoints[0] != 6 {
t.Fatalf("winner should have one dice point, got %+v", winner.DicePoints)
}
if len(loser.DicePoints) != 1 || loser.DicePoints[0] != 1 {
t.Fatalf("loser should have one dice point, got %+v", loser.DicePoints)
}
if winner.Result != dicedomain.ParticipantResultWin || winner.PayoutCoin != 188 || winner.BalanceAfter != 1088 {
t.Fatalf("winner settlement mismatch: %+v", winner)
}
if loser.Result != dicedomain.ParticipantResultLose || loser.PayoutCoin != 0 || loser.BalanceAfter != 900 {
t.Fatalf("loser settlement mismatch: %+v", loser)
}
if wallet.calls != 3 {
t.Fatalf("wallet calls = %d, want 3", wallet.calls)
}
if repo.levelDebitOrders != 2 {
t.Fatalf("debit orders should still feed game order success path, got %d", repo.levelDebitOrders)
}
if len(repo.exploreWinners) != 1 {
t.Fatalf("explore winner records = %d, want 1", len(repo.exploreWinners))
}
exploreWinner := repo.exploreWinners[0]
if exploreWinner.GameCode != dicedomain.ExploreGameCodeDice || exploreWinner.GameID != "dice" || exploreWinner.UserID != 101 || exploreWinner.CoinAmount != 188 {
t.Fatalf("explore winner mismatch: %+v", exploreWinner)
}
if exploreWinner.WinID == "" || exploreWinner.WonAtMS != 1700000000000 {
t.Fatalf("explore winner identity/time mismatch: %+v", exploreWinner)
}
}
func TestRollMatchRequiresMinimumParticipantsBeforeWalletDebit(t *testing.T) {
repo := newFakeDiceRepository()
wallet := &fakeDiceWallet{balances: map[int64]int64{101: 1000}}
svc := New(Config{}, repo, wallet)
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
created, _, err := svc.CreateMatch(context.Background(), CreateMatchCommand{
AppCode: "lalu",
RequestID: "req-create",
UserID: 101,
GameID: "dice",
StakeCoin: 100,
MinPlayers: 2,
MaxPlayers: 3,
})
if err != nil {
t.Fatalf("CreateMatch failed: %v", err)
}
_, _, err = svc.RollMatch(context.Background(), RollMatchCommand{AppCode: "lalu", RequestID: "req-roll", UserID: 101, MatchID: created.MatchID})
if !xerr.IsCode(err, xerr.Conflict) {
t.Fatalf("RollMatch should reject not enough participants, got %v", err)
}
if wallet.calls != 0 {
t.Fatalf("wallet should not be called, got %d", wallet.calls)
}
}
func TestRollMatchDrawRerollsUntilWinner(t *testing.T) {
repo := newFakeDiceRepository()
wallet := &fakeDiceWallet{balances: map[int64]int64{101: 1000, 202: 1000}}
svc := New(Config{FeeBPS: 500}, repo, wallet)
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
rolls := []int{2, 2, 5, 0}
svc.randomInt = func(max int) (int, error) {
if len(rolls) == 0 {
t.Fatal("randomInt called too many times")
}
value := rolls[0]
rolls = rolls[1:]
return value, nil
}
created, _, err := svc.CreateMatch(context.Background(), CreateMatchCommand{
AppCode: "lalu",
RequestID: "req-create",
UserID: 101,
GameID: "dice",
StakeCoin: 100,
MinPlayers: 2,
MaxPlayers: 2,
})
if err != nil {
t.Fatalf("CreateMatch failed: %v", err)
}
if _, _, err := svc.JoinMatch(context.Background(), JoinMatchCommand{AppCode: "lalu", RequestID: "req-join", UserID: 202, MatchID: created.MatchID}); err != nil {
t.Fatalf("JoinMatch failed: %v", err)
}
draw, _, err := svc.RollMatch(context.Background(), RollMatchCommand{AppCode: "lalu", RequestID: "req-roll-1", UserID: 101, MatchID: created.MatchID})
if err != nil {
t.Fatalf("first RollMatch failed: %v", err)
}
if draw.Status != dicedomain.MatchStatusReady || draw.Result != dicedomain.ParticipantResultDraw || draw.RoundNo != 2 {
t.Fatalf("draw should keep match ready for reroll: %+v", draw)
}
if wallet.calls != 2 {
t.Fatalf("draw should only debit both users, wallet calls = %d", wallet.calls)
}
for _, participant := range draw.Participants {
if participant.Result != dicedomain.ParticipantResultDraw || participant.PayoutCoin != 0 || len(participant.DicePoints) != 1 || participant.DicePoints[0] != 3 {
t.Fatalf("draw participant mismatch: %+v", participant)
}
}
settled, _, err := svc.RollMatch(context.Background(), RollMatchCommand{AppCode: "lalu", RequestID: "req-roll-2", UserID: 101, MatchID: created.MatchID})
if err != nil {
t.Fatalf("second RollMatch failed: %v", err)
}
if settled.Status != dicedomain.MatchStatusSettled || settled.Result != dicedomain.ParticipantResultWin {
t.Fatalf("reroll should settle with winner: %+v", settled)
}
if wallet.calls != 3 {
t.Fatalf("reroll should not debit again and should only pay winner, wallet calls = %d", wallet.calls)
}
winner := participantByUser(settled.Participants, 101)
loser := participantByUser(settled.Participants, 202)
if winner.DicePoints[0] != 6 || winner.PayoutCoin != 188 || winner.BalanceAfter != 1088 {
t.Fatalf("winner after reroll mismatch: %+v", winner)
}
if loser.DicePoints[0] != 1 || loser.PayoutCoin != 0 || loser.BalanceAfter != 900 {
t.Fatalf("loser after reroll mismatch: %+v", loser)
}
}
func TestRPSRollUsesPreselectedGesturesWithoutRandomRoll(t *testing.T) {
repo := newFakeDiceRepository()
configureFakeRock(repo)
wallet := &fakeDiceWallet{balances: map[int64]int64{101: 1000, 202: 1000}}
svc := New(Config{FeeBPS: 500}, repo, wallet)
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
svc.randomInt = func(max int) (int, error) {
t.Fatalf("rock settlement must not call randomInt, max=%d", max)
return 0, nil
}
created, _, err := svc.CreateMatch(context.Background(), CreateMatchCommand{
AppCode: "lalu",
RequestID: "req-create-rps",
UserID: 101,
GameID: "rock",
StakeCoin: 100,
MinPlayers: 2,
MaxPlayers: 2,
RPSGesture: "rock",
})
if err != nil {
t.Fatalf("CreateMatch failed: %v", err)
}
if _, _, err := svc.JoinMatch(context.Background(), JoinMatchCommand{AppCode: "lalu", RequestID: "req-join-rps", UserID: 202, MatchID: created.MatchID, GameID: "rock", RPSGesture: "scissors"}); err != nil {
t.Fatalf("JoinMatch failed: %v", err)
}
settled, _, err := svc.RollMatch(context.Background(), RollMatchCommand{AppCode: "lalu", RequestID: "req-roll-rps", UserID: 101, MatchID: created.MatchID, GameID: "rock", RPSGesture: "rock"})
if err != nil {
t.Fatalf("RollMatch failed: %v", err)
}
if settled.Status != dicedomain.MatchStatusSettled || settled.Result != dicedomain.ParticipantResultWin {
t.Fatalf("rock match should settle with winner: %+v", settled)
}
winner := participantByUser(settled.Participants, 101)
loser := participantByUser(settled.Participants, 202)
if winner.RPSGesture != "rock" || len(winner.DicePoints) != 1 || winner.DicePoints[0] != 1 || winner.Result != dicedomain.ParticipantResultWin || winner.PayoutCoin != 188 || winner.BalanceAfter != 1088 {
t.Fatalf("winner rps settlement mismatch: %+v", winner)
}
if loser.RPSGesture != "scissors" || len(loser.DicePoints) != 1 || loser.DicePoints[0] != 3 || loser.Result != dicedomain.ParticipantResultLose || loser.PayoutCoin != 0 || loser.BalanceAfter != 900 {
t.Fatalf("loser rps settlement mismatch: %+v", loser)
}
if wallet.calls != 3 {
t.Fatalf("rock win should debit both and pay winner, wallet calls=%d", wallet.calls)
}
if len(repo.exploreWinners) != 1 || repo.exploreWinners[0].GameCode != dicedomain.ExploreGameCodeRock || repo.exploreWinners[0].UserID != 101 {
t.Fatalf("rock explore winner mismatch: %+v", repo.exploreWinners)
}
}
func TestRPSDrawRefundsBothAndSettles(t *testing.T) {
repo := newFakeDiceRepository()
configureFakeRock(repo)
wallet := &fakeDiceWallet{balances: map[int64]int64{101: 1000, 202: 1000}}
svc := New(Config{FeeBPS: 500}, repo, wallet)
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
svc.randomInt = func(max int) (int, error) {
t.Fatalf("rock draw settlement must not call randomInt, max=%d", max)
return 0, nil
}
created, _, err := svc.CreateMatch(context.Background(), CreateMatchCommand{
AppCode: "lalu",
RequestID: "req-create-rps-draw",
UserID: 101,
GameID: "rock",
StakeCoin: 100,
MinPlayers: 2,
MaxPlayers: 2,
RPSGesture: "rock",
})
if err != nil {
t.Fatalf("CreateMatch failed: %v", err)
}
if _, _, err := svc.JoinMatch(context.Background(), JoinMatchCommand{AppCode: "lalu", RequestID: "req-join-rps-draw", UserID: 202, MatchID: created.MatchID, GameID: "rock", RPSGesture: "rock"}); err != nil {
t.Fatalf("JoinMatch failed: %v", err)
}
settled, _, err := svc.RollMatch(context.Background(), RollMatchCommand{AppCode: "lalu", RequestID: "req-roll-rps-draw", UserID: 101, MatchID: created.MatchID, GameID: "rock"})
if err != nil {
t.Fatalf("RollMatch failed: %v", err)
}
if settled.Status != dicedomain.MatchStatusSettled || settled.Result != dicedomain.ParticipantResultDraw || settled.RoundNo != 1 {
t.Fatalf("rock draw should settle without reroll: %+v", settled)
}
for _, participant := range settled.Participants {
if participant.Result != dicedomain.ParticipantResultDraw || participant.PayoutCoin != participant.StakeCoin || participant.BalanceAfter != 1000 || participant.RPSGesture != "rock" {
t.Fatalf("draw participant should be refunded: %+v", participant)
}
}
if wallet.calls != 4 {
t.Fatalf("rock draw should debit and refund both users, wallet calls=%d", wallet.calls)
}
if len(repo.exploreWinners) != 0 {
t.Fatalf("draw must not record explore winners: %+v", repo.exploreWinners)
}
}
func TestRPSRequiresGestureBeforeMatchmaking(t *testing.T) {
tests := []struct {
name string
call func(*Service) (dicedomain.Match, int64, error)
}{
{
name: "create",
call: func(svc *Service) (dicedomain.Match, int64, error) {
return svc.CreateMatch(context.Background(), CreateMatchCommand{AppCode: "lalu", UserID: 101, GameID: "rock", StakeCoin: 100, MinPlayers: 2, MaxPlayers: 2})
},
},
{
name: "match",
call: func(svc *Service) (dicedomain.Match, int64, error) {
return svc.Match(context.Background(), MatchCommand{AppCode: "lalu", UserID: 101, GameID: "rock", StakeCoin: 100})
},
},
{
name: "join",
call: func(svc *Service) (dicedomain.Match, int64, error) {
return svc.JoinMatch(context.Background(), JoinMatchCommand{AppCode: "lalu", UserID: 202, MatchID: "match-rock-1", GameID: "rock"})
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repo := newFakeDiceRepository()
configureFakeRock(repo)
repo.match = dicedomain.Match{
AppCode: "lalu",
MatchID: "match-rock-1",
GameID: "rock",
PlatformCode: "dice",
ProviderGameID: "rock",
MinPlayers: 2,
MaxPlayers: 2,
CurrentPlayers: 1,
StakeCoin: 100,
Status: dicedomain.MatchStatusCreated,
Participants: []dicedomain.Participant{{
AppCode: "lalu",
MatchID: "match-rock-1",
UserID: 101,
SeatNo: 1,
Status: dicedomain.ParticipantStatusJoined,
StakeCoin: 100,
RPSGesture: "paper",
}},
}
wallet := &fakeDiceWallet{balances: map[int64]int64{101: 1000, 202: 1000}}
svc := New(Config{}, repo, wallet)
_, _, err := tt.call(svc)
if !xerr.IsCode(err, xerr.InvalidArgument) {
t.Fatalf("%s should reject missing rps gesture, got %v", tt.name, err)
}
if wallet.calls != 0 {
t.Fatalf("%s should reject before wallet mutation, calls=%d", tt.name, wallet.calls)
}
})
}
}
func TestRPSRollRejectsGestureMismatchBeforeMutation(t *testing.T) {
repo := newFakeDiceRepository()
configureFakeRock(repo)
repo.match = dicedomain.Match{
AppCode: "lalu",
MatchID: "match-rock-1",
GameID: "rock",
PlatformCode: "dice",
ProviderGameID: "rock",
MinPlayers: 2,
MaxPlayers: 2,
CurrentPlayers: 2,
StakeCoin: 100,
Status: dicedomain.MatchStatusReady,
Participants: []dicedomain.Participant{
{AppCode: "lalu", MatchID: "match-rock-1", UserID: 101, SeatNo: 1, Status: dicedomain.ParticipantStatusJoined, StakeCoin: 100, RPSGesture: "rock"},
{AppCode: "lalu", MatchID: "match-rock-1", UserID: 202, SeatNo: 2, Status: dicedomain.ParticipantStatusJoined, StakeCoin: 100, RPSGesture: "paper"},
},
}
wallet := &fakeDiceWallet{balances: map[int64]int64{101: 1000, 202: 1000}}
svc := New(Config{}, repo, wallet)
_, _, err := svc.RollMatch(context.Background(), RollMatchCommand{AppCode: "lalu", UserID: 101, MatchID: "match-rock-1", GameID: "rock", RPSGesture: "scissors"})
if !xerr.IsCode(err, xerr.Conflict) {
t.Fatalf("roll should reject changed rps gesture, got %v", err)
}
if repo.match.Status != dicedomain.MatchStatusReady || wallet.calls != 0 {
t.Fatalf("gesture mismatch must not claim roll or call wallet: match=%+v wallet=%d", repo.match, wallet.calls)
}
}
func TestExploreGameCodeForMatchUsesSelfGameIDs(t *testing.T) {
tests := []struct {
gameID string
want string
}{
{gameID: "dice", want: dicedomain.ExploreGameCodeDice},
{gameID: "rock", want: dicedomain.ExploreGameCodeRock},
{gameID: "room_rps", want: ""},
{gameID: "ocean_hunt", want: ""},
}
for _, tt := range tests {
if got := exploreGameCodeForMatch(tt.gameID); got != tt.want {
t.Fatalf("exploreGameCodeForMatch(%q) = %q, want %q", tt.gameID, got, tt.want)
}
}
}
func TestSelfGameOperationsRejectWrongGameIDBeforeMutation(t *testing.T) {
tests := []struct {
name string
call func(*Service) (dicedomain.Match, int64, error)
}{
{
name: "join",
call: func(svc *Service) (dicedomain.Match, int64, error) {
return svc.JoinMatch(context.Background(), JoinMatchCommand{AppCode: "lalu", UserID: 202, MatchID: "match-rock-1", GameID: "dice"})
},
},
{
name: "get",
call: func(svc *Service) (dicedomain.Match, int64, error) {
return svc.GetMatch(context.Background(), GetMatchCommand{AppCode: "lalu", UserID: 101, MatchID: "match-rock-1", GameID: "dice"})
},
},
{
name: "roll",
call: func(svc *Service) (dicedomain.Match, int64, error) {
return svc.RollMatch(context.Background(), RollMatchCommand{AppCode: "lalu", UserID: 101, MatchID: "match-rock-1", GameID: "dice"})
},
},
{
name: "cancel",
call: func(svc *Service) (dicedomain.Match, int64, error) {
return svc.CancelMatch(context.Background(), CancelMatchCommand{AppCode: "lalu", UserID: 101, MatchID: "match-rock-1", GameID: "dice"})
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repo := newFakeDiceRepository()
repo.match = dicedomain.Match{
AppCode: "lalu",
MatchID: "match-rock-1",
GameID: "rock",
PlatformCode: "dice",
ProviderGameID: "rock",
MinPlayers: 2,
MaxPlayers: 2,
CurrentPlayers: 1,
StakeCoin: 100,
Status: dicedomain.MatchStatusCreated,
Participants: []dicedomain.Participant{{
AppCode: "lalu",
MatchID: "match-rock-1",
UserID: 101,
SeatNo: 1,
Status: dicedomain.ParticipantStatusJoined,
StakeCoin: 100,
}},
}
svc := New(Config{}, repo, &fakeDiceWallet{balances: map[int64]int64{101: 1000, 202: 1000}})
_, _, err := tt.call(svc)
if !xerr.IsCode(err, xerr.NotFound) {
t.Fatalf("%s should hide cross-game match as not found, got %v", tt.name, err)
}
if repo.match.Status != dicedomain.MatchStatusCreated || len(repo.match.Participants) != 1 {
t.Fatalf("%s must not mutate mismatched game match: %+v", tt.name, repo.match)
}
})
}
}
type fakeDiceRepository struct {
game gamedomain.LaunchableGame
match dicedomain.Match
config dicedomain.Config
poolBalance int64
orders map[string]gamedomain.GameOrder
levelDebitOrders int
exploreWinners []dicedomain.ExploreWinner
}
func newFakeDiceRepository() *fakeDiceRepository {
return &fakeDiceRepository{
game: gamedomain.LaunchableGame{
CatalogItem: gamedomain.CatalogItem{
AppCode: "lalu",
GameID: "dice",
PlatformCode: "dice",
ProviderGameID: "dice",
GameName: "Dice",
Status: gamedomain.StatusActive,
},
PlatformStatus: gamedomain.StatusActive,
},
config: dicedomain.Config{
AppCode: "lalu",
GameID: "dice",
Status: dicedomain.ConfigStatusActive,
StakeOptions: dicedomain.NormalizeStakeOptions(nil),
FeeBPS: int32(dicedomain.DefaultFeeBPS),
PoolBPS: int32(dicedomain.DefaultPoolBPS),
MinPlayers: dicedomain.DefaultMinPlayers,
MaxPlayers: dicedomain.DefaultMaxPlayers,
RobotEnabled: true,
RobotMatchWaitMS: dicedomain.DefaultRobotWaitMS,
},
orders: map[string]gamedomain.GameOrder{},
}
}
func configureFakeRock(repo *fakeDiceRepository) {
repo.game.CatalogItem.GameID = "rock"
repo.game.CatalogItem.ProviderGameID = "rock"
repo.game.CatalogItem.GameName = "Rock Paper Scissors"
repo.config.GameID = "rock"
}
func (r *fakeDiceRepository) GetLaunchableGame(context.Context, string, string) (gamedomain.LaunchableGame, error) {
return r.game, nil
}
func (r *fakeDiceRepository) CreateGameOrder(_ context.Context, order gamedomain.GameOrder) (gamedomain.GameOrder, bool, error) {
if existing, ok := r.orders[order.OrderID]; ok {
if existing.RequestHash != order.RequestHash {
return gamedomain.GameOrder{}, true, xerr.New(xerr.IdempotencyConflict, "provider order payload conflict")
}
return existing, true, nil
}
r.orders[order.OrderID] = order
return order, false, nil
}
func (r *fakeDiceRepository) MarkOrderSucceeded(_ context.Context, _ string, orderID string, walletTransactionID string, balanceAfter int64, nowMs int64) error {
order := r.orders[orderID]
order.Status = gamedomain.OrderStatusSucceeded
order.WalletTransactionID = walletTransactionID
order.WalletBalanceAfter = balanceAfter
order.UpdatedAtMS = nowMs
r.orders[orderID] = order
if order.OpType == "debit" {
r.levelDebitOrders++
}
return nil
}
func (r *fakeDiceRepository) MarkOrderFailed(_ context.Context, _ string, orderID string, status string, code string, message string, nowMs int64) error {
order := r.orders[orderID]
order.Status = status
order.FailureCode = code
order.FailureMessage = message
order.UpdatedAtMS = nowMs
r.orders[orderID] = order
return nil
}
func (r *fakeDiceRepository) CreateDiceMatch(_ context.Context, match dicedomain.Match) (dicedomain.Match, error) {
r.match = cloneDiceMatch(match)
return cloneDiceMatch(r.match), nil
}
func (r *fakeDiceRepository) JoinDiceMatch(_ context.Context, _ string, _ string, userID int64, rpsGesture string, nowMs int64) (dicedomain.Match, error) {
for _, participant := range r.match.Participants {
if participant.UserID == userID {
return cloneDiceMatch(r.match), nil
}
}
seatNo := int32(len(r.match.Participants) + 1)
r.match.Participants = append(r.match.Participants, dicedomain.Participant{
AppCode: r.match.AppCode,
MatchID: r.match.MatchID,
UserID: userID,
SeatNo: seatNo,
Status: dicedomain.ParticipantStatusJoined,
StakeCoin: r.match.StakeCoin,
RPSGesture: strings.TrimSpace(rpsGesture),
JoinedAtMS: nowMs,
UpdatedAtMS: nowMs,
})
r.match.CurrentPlayers = int32(len(r.match.Participants))
r.match.Status = dicedomain.MatchStatusJoining
r.match.UpdatedAtMS = nowMs
return cloneDiceMatch(r.match), nil
}
func (r *fakeDiceRepository) GetDiceMatch(context.Context, string, string) (dicedomain.Match, error) {
return cloneDiceMatch(r.match), nil
}
func (r *fakeDiceRepository) ClaimDiceMatchForRoll(_ context.Context, _ string, _ string, userID int64, nowMs int64) (dicedomain.Match, error) {
if !hasFakeDiceParticipant(r.match.Participants, userID) {
return dicedomain.Match{}, xerr.New(xerr.PermissionDenied, "dice participant is required")
}
if len(r.match.Participants) < int(r.match.MinPlayers) {
return dicedomain.Match{}, xerr.New(xerr.Conflict, "dice match does not have enough participants")
}
if r.match.Result == dicedomain.ParticipantResultDraw {
r.match.Result = ""
for index := range r.match.Participants {
r.match.Participants[index].DicePoints = nil
r.match.Participants[index].Result = ""
r.match.Participants[index].PayoutCoin = 0
r.match.Participants[index].UpdatedAtMS = nowMs
}
}
r.match.Status = dicedomain.MatchStatusSettling
r.match.UpdatedAtMS = nowMs
return cloneDiceMatch(r.match), nil
}
func (r *fakeDiceRepository) SaveDiceRolls(_ context.Context, match dicedomain.Match, nowMs int64) (dicedomain.Match, error) {
for _, source := range match.Participants {
for index := range r.match.Participants {
if r.match.Participants[index].UserID == source.UserID {
r.match.Participants[index].DicePoints = append([]int32(nil), source.DicePoints...)
r.match.Participants[index].RPSGesture = source.RPSGesture
r.match.Participants[index].Result = source.Result
r.match.Participants[index].PayoutCoin = source.PayoutCoin
r.match.Participants[index].UpdatedAtMS = nowMs
}
}
}
r.match.Status = dicedomain.MatchStatusPayoutApplying
r.match.Result = match.Result
r.match.UpdatedAtMS = nowMs
return cloneDiceMatch(r.match), nil
}
func (r *fakeDiceRepository) SaveDiceDrawForReroll(_ context.Context, match dicedomain.Match, nowMs int64) (dicedomain.Match, error) {
for _, source := range match.Participants {
for index := range r.match.Participants {
if r.match.Participants[index].UserID == source.UserID {
r.match.Participants[index].DicePoints = append([]int32(nil), source.DicePoints...)
r.match.Participants[index].RPSGesture = source.RPSGesture
r.match.Participants[index].Result = dicedomain.ParticipantResultDraw
r.match.Participants[index].PayoutCoin = 0
r.match.Participants[index].UpdatedAtMS = nowMs
}
}
}
r.match.Status = dicedomain.MatchStatusReady
r.match.Result = dicedomain.ParticipantResultDraw
r.match.RoundNo++
r.match.UpdatedAtMS = nowMs
return cloneDiceMatch(r.match), nil
}
func (r *fakeDiceRepository) MarkDiceParticipantDebitSucceeded(_ context.Context, _ string, _ string, userID int64, orderID string, balanceAfter int64, nowMs int64) error {
return r.updateParticipant(userID, func(participant *dicedomain.Participant) {
participant.Status = dicedomain.ParticipantStatusDebitSucceeded
participant.DebitOrderID = orderID
participant.BalanceAfter = balanceAfter
participant.UpdatedAtMS = nowMs
})
}
func (r *fakeDiceRepository) MarkDiceParticipantDebitFailed(_ context.Context, _ string, _ string, userID int64, orderID string, nowMs int64) error {
return r.updateParticipant(userID, func(participant *dicedomain.Participant) {
participant.Status = dicedomain.ParticipantStatusDebitFailed
participant.DebitOrderID = orderID
participant.UpdatedAtMS = nowMs
})
}
func (r *fakeDiceRepository) MarkDiceParticipantPayoutSucceeded(_ context.Context, _ string, _ string, userID int64, orderID string, balanceAfter int64, nowMs int64) error {
return r.updateParticipant(userID, func(participant *dicedomain.Participant) {
participant.Status = dicedomain.ParticipantStatusPayoutSucceeded
participant.PayoutOrderID = orderID
participant.BalanceAfter = balanceAfter
participant.UpdatedAtMS = nowMs
})
}
func (r *fakeDiceRepository) MarkDiceParticipantRefundSucceeded(_ context.Context, _ string, _ string, userID int64, orderID string, balanceAfter int64, nowMs int64) error {
return r.updateParticipant(userID, func(participant *dicedomain.Participant) {
participant.Status = dicedomain.ParticipantStatusRefundSucceeded
participant.RefundOrderID = orderID
participant.BalanceAfter = balanceAfter
participant.UpdatedAtMS = nowMs
})
}
func (r *fakeDiceRepository) MarkDiceMatchSettled(_ context.Context, _ string, _ string, result string, nowMs int64) error {
r.match.Status = dicedomain.MatchStatusSettled
r.match.Result = result
r.match.UpdatedAtMS = nowMs
r.match.SettledAtMS = nowMs
for index := range r.match.Participants {
r.match.Participants[index].Status = dicedomain.ParticipantStatusSettled
r.match.Participants[index].UpdatedAtMS = nowMs
}
return nil
}
func (r *fakeDiceRepository) MarkDiceMatchFailed(_ context.Context, _ string, _ string, result string, nowMs int64) error {
r.match.Status = dicedomain.MatchStatusFailed
r.match.Result = result
r.match.UpdatedAtMS = nowMs
return nil
}
func (r *fakeDiceRepository) GetDiceConfig(context.Context, string, string) (dicedomain.Config, error) {
return r.config, nil
}
func (r *fakeDiceRepository) ListSelfGameConfigs(context.Context, string) ([]dicedomain.Config, error) {
return []dicedomain.Config{r.config}, nil
}
func (r *fakeDiceRepository) UpsertDiceConfig(_ context.Context, config dicedomain.Config, _ int64) (dicedomain.Config, error) {
r.config = config
return config, nil
}
func (r *fakeDiceRepository) FindAndJoinWaitingDiceMatch(context.Context, string, string, int64, int64, string, int64) (dicedomain.Match, error) {
return dicedomain.Match{}, xerr.New(xerr.NotFound, "waiting dice match not found")
}
func (r *fakeDiceRepository) JoinDiceRobot(context.Context, string, string, int64, string, string, int64) (dicedomain.Match, error) {
return dicedomain.Match{}, xerr.New(xerr.NotFound, "dice robot not found")
}
func (r *fakeDiceRepository) CancelDiceMatch(context.Context, string, string, int64, int64) (dicedomain.Match, error) {
r.match.Status = dicedomain.MatchStatusCanceled
return cloneDiceMatch(r.match), nil
}
func (r *fakeDiceRepository) PickActiveDiceRobot(context.Context, string, string, string) (dicedomain.Robot, error) {
return dicedomain.Robot{}, xerr.New(xerr.NotFound, "dice robot not found")
}
func (r *fakeDiceRepository) AdjustDicePool(_ context.Context, adjustment dicedomain.PoolAdjustment) (dicedomain.PoolAdjustment, error) {
if adjustment.Direction == dicedomain.PoolDirectionOut {
r.poolBalance -= adjustment.AmountCoin
} else {
r.poolBalance += adjustment.AmountCoin
}
adjustment.BalanceAfter = r.poolBalance
return adjustment, nil
}
func (r *fakeDiceRepository) ListDiceRobots(context.Context, string, string, string, int32, string) ([]dicedomain.Robot, string, error) {
return nil, "", nil
}
func (r *fakeDiceRepository) RegisterDiceRobots(context.Context, string, string, []int64, int64, int64) ([]dicedomain.Robot, error) {
return nil, nil
}
func (r *fakeDiceRepository) SetDiceRobotStatus(context.Context, string, string, int64, string, int64) (dicedomain.Robot, error) {
return dicedomain.Robot{}, nil
}
func (r *fakeDiceRepository) DeleteDiceRobot(context.Context, string, string, int64) error {
return nil
}
func (r *fakeDiceRepository) RecordExploreWinner(_ context.Context, winner dicedomain.ExploreWinner) error {
r.exploreWinners = append(r.exploreWinners, winner)
return nil
}
func (r *fakeDiceRepository) ListExploreWinners(context.Context, string, int32) ([]dicedomain.ExploreWinner, error) {
return nil, nil
}
func (r *fakeDiceRepository) updateParticipant(userID int64, apply func(*dicedomain.Participant)) error {
for index := range r.match.Participants {
if r.match.Participants[index].UserID == userID {
apply(&r.match.Participants[index])
return nil
}
}
return xerr.New(xerr.NotFound, "dice participant not found")
}
type fakeDiceWallet struct {
balances map[int64]int64
calls int
}
func (w *fakeDiceWallet) ApplyGameCoinChange(_ context.Context, req *walletv1.ApplyGameCoinChangeRequest) (*walletv1.ApplyGameCoinChangeResponse, error) {
w.calls++
if req.GetCoinAmount() <= 0 {
return nil, xerr.New(xerr.InvalidArgument, "coin_amount must be positive")
}
switch req.GetOpType() {
case "debit":
if w.balances[req.GetUserId()] < req.GetCoinAmount() {
return nil, xerr.New(xerr.InsufficientBalance, "insufficient balance")
}
w.balances[req.GetUserId()] -= req.GetCoinAmount()
case "credit", "refund":
w.balances[req.GetUserId()] += req.GetCoinAmount()
default:
return nil, xerr.New(xerr.InvalidArgument, "game op_type is invalid")
}
return &walletv1.ApplyGameCoinChangeResponse{
WalletTransactionId: "wtx_" + strings.ReplaceAll(req.GetProviderOrderId(), ":", "_"),
BalanceAfter: w.balances[req.GetUserId()],
}, nil
}
func (w *fakeDiceWallet) GetBalances(_ context.Context, req *walletv1.GetBalancesRequest) (*walletv1.GetBalancesResponse, error) {
return &walletv1.GetBalancesResponse{Balances: []*walletv1.AssetBalance{{
AssetType: "COIN",
AvailableAmount: w.balances[req.GetUserId()],
}}}, nil
}
func participantByUser(participants []dicedomain.Participant, userID int64) dicedomain.Participant {
for _, participant := range participants {
if participant.UserID == userID {
return participant
}
}
return dicedomain.Participant{}
}
func hasFakeDiceParticipant(participants []dicedomain.Participant, userID int64) bool {
return participantByUser(participants, userID).UserID == userID
}
func cloneDiceMatch(match dicedomain.Match) dicedomain.Match {
cloned := match
cloned.Participants = append([]dicedomain.Participant(nil), match.Participants...)
for index := range cloned.Participants {
cloned.Participants[index].DicePoints = append([]int32(nil), match.Participants[index].DicePoints...)
}
return cloned
}