package logic import ( "context" "errors" "google.golang.org/grpc" "google.golang.org/grpc/reflection" "google.golang.org/protobuf/types/known/emptypb" "net" "sonet/api/gen/auth" "sonet/api/gen/mahjong" "sonet/internal/mahjong/game" "sonet/internal/mahjong/helper" "sonet/internal/mahjong/store" "sonet/pkg/config" "sonet/pkg/grpc/discovery" "sonet/pkg/grpc/interceptor" "sonet/pkg/protocol/deliver" "sonet/pkg/protocol/session" "sonet/pkg/utils/collect" "sonet/pkg/utils/logger" "sonet/pkg/utils/shutdown" "sonet/pkg/utils/state" ) type MahjongServer struct { mahjong.UnimplementedMahjongServer store *store.Store authClient auth.AuthClient deliver *deliver.Deliver } func NewMahjongServer( store *store.Store, authClient auth.AuthClient, deliver *deliver.Deliver) *MahjongServer { return &MahjongServer{ store: store, authClient: authClient, deliver: deliver, } } func (mj *MahjongServer) Run(conf config.GrpcConfig, registry discovery.Registry) (err error) { server := grpc.NewServer( config.GetGrpcOptions( conf, grpc.UnaryInterceptor(interceptor.RecoverInterceptor), )..., ) if !conf.NoReflection { // 注册反射服务 reflection.Register(server) } mahjong.RegisterMahjongServer(server, mj) listen, err := net.Listen("tcp", conf.Address) if err != nil { return } // registry discovery register := conf.Register if register.Name == "" { register.Name = mahjong.Mahjong_ServiceDesc.ServiceName } if register.Addr == "" { register.Addr = conf.Address } ctx, cancel := context.WithCancel(context.Background()) err = registry.Registry(ctx, register) if err != nil { panic(err) } shutdown.AddHook(cancel) // run serve logger.Infof("%s grpc server running %s\n", register.Name, listen.Addr().String()) err = server.Serve(listen) return } func (mj *MahjongServer) PlayerOnline(ctx context.Context, empty *emptypb.Empty) (res *mahjong.ResPlayerOnline, err error) { subject, err := session.GetSubject(ctx) if err != nil { return } res = &mahjong.ResPlayerOnline{} player, err := mj.store.LoadPlayer(subject.Uid) if err == nil { // 重连用户 res.Player = helper.MjPlayer2TfPlayer(player) return } // load player info account, err := mj.authClient.FindByUid(ctx, &auth.ReqFindByUid{Uid: subject.Uid}) if err != nil { return } player = game.NewMjPlayer(subject.Uid) player.Init() player.Username = account.Username player.Avatar = account.Extra["avatar"] // store online player mj.store.StorePlayer(player) res.Player = helper.MjPlayer2TfPlayer(player) return } func (mj *MahjongServer) PlayerOffline(ctx context.Context, empty *emptypb.Empty) (_ *emptypb.Empty, err error) { subject, err := session.GetSubject(ctx) if err != nil { return } // delete offline player mj.store.RemovePlayer(subject.Uid) // TODO persistent player return } func (mj *MahjongServer) LobbyView(ctx context.Context, empty *emptypb.Empty) (r *mahjong.Lobby, err error) { r = &mahjong.Lobby{} mj.store.RangeTable(func(key, value any) bool { table := value.(*game.MjTable) r.Tables = append(r.Tables, &mahjong.LobbyTable{ TableId: table.TableId, PlayerCount: int32(table.PlayerCount), Stage: int32(table.Stage), }) return true }) return } func (mj *MahjongServer) CreateTable(ctx context.Context, empty *emptypb.Empty) (mjTable *mahjong.MjTable, err error) { subject, err := session.GetSubject(ctx) if err != nil { return } tableId := mj.store.NextTableId() table := game.NewMjTable(tableId, subject.Uid) table.Init() master, err := mj.store.LoadPlayer(subject.Uid) // master player if err != nil { return } if master.MjTable != nil { err = errors.New("请先退出当前牌桌") return } _, err = table.PlayerJoin(master) if err != nil { return } table.StateIdleClean() mj.store.StoreTable(table) mjTable = helper.MjTable2ProtoTable(table, "") return } func (mj *MahjongServer) JoinTable(ctx context.Context, req *mahjong.ReqJoinTable) (t *mahjong.MjTable, err error) { subject, err := session.GetSubject(ctx) if err != nil { return } // store player player, err := mj.store.LoadPlayer(subject.Uid) if err != nil { return } if player.MjTable != nil { t = helper.MjTable2ProtoTable(player.MjTable, player.PlayerId) return } table, err := mj.store.LoadTable(req.TableId) if err != nil { return } for _, p := range table.GetPlayers() { // 挤下线 if p.PlayerId == player.PlayerId { player.MjTable = table t = helper.MjTable2ProtoTable(player.MjTable, player.PlayerId) return } } if table.PlayerCount >= 4 { err = errors.New("牌桌已满") return } table.Lock.Lock() defer table.Lock.Unlock() _, err = table.PlayerJoin(player) if err != nil { return } table.State2Broadcast(player.PlayerId) // notice other player join me // return table status t = helper.MjTable2ProtoTable(table, player.PlayerId) return } func (mj *MahjongServer) LeaveTable(ctx context.Context, _ *emptypb.Empty) (res *emptypb.Empty, err error) { subject, err := session.GetSubject(ctx) if err != nil { return } player, err := mj.store.LoadPlayer(subject.Uid) if err != nil { return } if player.MjTable == nil { err = errors.New("未加入牌桌") return } table, err := mj.store.LoadTable(player.MjTable.TableId) if err != nil { return } if collect.In(player.Status, 2, 3) { err = errors.New("进行中暂不能离开") return } if collect.NotIn(table.Stage, 1, 4) { err = errors.New("牌局进行中,不能离开") return } isMaster := table.MasterId == player.PlayerId rest := table.GetPlayers(player.PlayerId) if len(rest) == 0 { // 玩家全退出解散牌桌 _, err = mj.DismissTable(ctx, nil) return } table.Lock.Lock() defer table.Lock.Unlock() err = table.PlayerLeave(player.PlayerId) if err != nil { return } if isMaster { for _, p := range table.Players { if p != nil { table.SetState(state.S{"MasterId": p.PlayerId}) break } } } table.State2Broadcast() //res = &emptypb.Empty{} return } func (mj *MahjongServer) KickoutTable(ctx context.Context, req *mahjong.ReqKickoutTable) (res *emptypb.Empty, err error) { subject, err := session.GetSubject(ctx) if err != nil { return } player, err := mj.store.LoadPlayer(subject.Uid) if err != nil { return } table := player.MjTable if table == nil { err = errors.New("牌桌不存在") return } if table.MasterId != player.PlayerId { err = errors.New("非房主不可踢人") return } table.Lock.Lock() defer table.Lock.Unlock() err = table.PlayerLeave(req.TargetUid) if err != nil { return } table.State2Broadcast() res = &emptypb.Empty{} return } func (mj *MahjongServer) ReadyStart(ctx context.Context, _ *emptypb.Empty) (_ *emptypb.Empty, err error) { subject, err := session.GetSubject(ctx) if err != nil { return } player, err := mj.store.LoadPlayer(subject.Uid) if err != nil { return } if player.MjTable == nil { err = errors.New("牌桌不存在") return } if player.Status == 2 { err = errors.New("已准备") return } player.MjTable.Lock.Lock() defer player.MjTable.Lock.Unlock() player.SetState(state.S{"Status": int8(2)}) player.State2Broadcast() return } func (mj *MahjongServer) CancelReady(ctx context.Context, _ *emptypb.Empty) (_ *emptypb.Empty, err error) { subject, err := session.GetSubject(ctx) if err != nil { return } player, err := mj.store.LoadPlayer(subject.Uid) if err != nil { return } if player.MjTable == nil { err = errors.New("牌桌不存在") return } if player.Status == 1 { err = errors.New("未准备") return } player.MjTable.Lock.Lock() defer player.MjTable.Lock.Unlock() player.SetState(state.S{"Status": int8(1)}) player.State2Broadcast() return } func (mj *MahjongServer) DismissTable(ctx context.Context, _ *emptypb.Empty) (res *emptypb.Empty, err error) { subject, err := session.GetSubject(ctx) if err != nil { return } player, err := mj.store.LoadPlayer(subject.Uid) if err != nil { return } table := player.MjTable if table == nil { err = errors.New("牌桌不存在") return } table.Lock.Lock() defer table.Lock.Unlock() if table.Stage == 2 { err = errors.New("牌局进行中") return } for _, p := range table.GetPlayers() { p.SetState(state.S{"MjTable": nil}) p.State2Assign(p.PlayerId) } receivers := table.GetPlayerIds() mj.store.RemoveTable(table.TableId) _, err = mj.deliver.DeliverBatch(ctx, &mahjong.NoticeDismissTable{}, receivers) if err != nil { return } res = &emptypb.Empty{} return } func (mj *MahjongServer) GameStart(ctx context.Context, _ *emptypb.Empty) (res *emptypb.Empty, err error) { subject, err := session.GetSubject(ctx) if err != nil { return } player, err := mj.store.LoadPlayer(subject.Uid) if err != nil { return } table := player.MjTable if table == nil { err = errors.New("牌桌不存在") return } if table.MasterId != player.PlayerId { err = errors.New("只有房主可以开始") return } if table == nil { err = errors.New("牌桌不存在") return } if table.PlayerCount < 2 { err = errors.New("玩家人数不够") return } if collect.NotIn(table.Stage, 1, 4) { err = errors.New("牌桌正在进行中") return } // 检查玩家准备状态 for _, player := range table.GetPlayers() { if player.Status != 2 && table.MasterId != player.PlayerId { err = errors.New("有玩家未准备") return } } // 初始化状态,发牌 table.Lock.Lock() defer table.Lock.Unlock() // 初始化牌桌状态 err = table.GameStart() if err != nil { return } // action 定缺 for i, p := range table.Players { if p == nil { continue } err = p.GameStart() if err != nil { return } p.State2Broadcast() // 初始化玩家手牌 摸13张 mjHand := table.MjWall.PopN(13) game.SortTiles(mjHand) if i == table.EastPlayerSit() { // 东摸一张牌 p.SetState(state.S{"MjDraw": table.MjWall.Pop()}) table.SetState(state.S{"MjWall": table.MjWall}) } p.SetState(state.S{ "MjHand": mjHand, "Opts": []int8{game.ActionLack}, }) p.State2Assign(p.PlayerId) } table.State2Broadcast() res = &emptypb.Empty{} return } func (mj *MahjongServer) PlayerAction(ctx context.Context, req *mahjong.ReqPlayerAction) (res *emptypb.Empty, err error) { res = &emptypb.Empty{} subject, err := session.GetSubject(ctx) if err != nil { return } player, err := mj.store.LoadPlayer(subject.Uid) if err != nil { return } table := player.MjTable if table == nil { err = errors.New("牌桌不存在") return } actionOpt := int8(req.Opt) actionHit := int8(req.Hit) if collect.NotIn(actionOpt, player.Opts...) { err = errors.New("不允许当前操作") return } table.Lock.Lock() defer table.Lock.Unlock() defer func() { if collect.In(actionOpt, game.ActionGang, game.ActionPeng) { if table.HitPlayerId != player.PlayerId { table.SetState(state.S{"HitPlayerId": player.PlayerId}) table.State2Broadcast() } } player.PrevAction = actionOpt player.PrevActions = append(player.PrevActions, actionOpt) }() switch actionOpt { case game.ActionCancel: if table.HitPlayerId == player.PlayerId { err = errors.New("不可取消") return } // 取消碰杠胡 if !collect.In(game.ActionPeng, player.Opts...) && !collect.In(game.ActionGang, player.Opts...) && !collect.In(game.ActionHu, player.Opts...) { err = errors.New("不可取消") return } err = player.RoundOver() if err != nil { return } player.State2Assign(player.PlayerId) // 还有玩家杠碰胡 waitingPlayers := collect.Filter(table.GetPlayers(), func(_ int, p *game.MjPlayer) bool { return len(p.Opts) > 0 }) if len(waitingPlayers) > 0 { return } // 下个玩家 nextPlayer, e := table.NextDrawPlayer(table.HitPlayerId) if e != nil { err = e return } if nextPlayer != nil { err = nextPlayer.RoundStart() if err != nil { return } nextPlayer.State2Assign(nextPlayer.PlayerId) } table.State2Broadcast() case game.ActionLack: // 定缺 err = player.ActionLack(int8(req.TileLack)) if err != nil { return } err = player.RoundOver() if err != nil { return } player.State2Broadcast() // 手牌排序 game.SortHandTiles(player.TileLack, player.MjHand) player.SetState(state.S{"MjHand": player.MjHand}) player.State2Assign(player.PlayerId) // 所有玩家都已定缺, 东出牌 nonLacks := collect.Filter(table.GetPlayers(), func(_ int, p *game.MjPlayer) bool { return p.TileLack == -1 }) if len(nonLacks) == 0 { // 东出牌 eastPlayer := table.Players[table.EastPlayerSit()] err = eastPlayer.RoundStart() if err != nil { return } eastPlayer.State2Assign(eastPlayer.PlayerId) table.SetState(state.S{"HitPlayerId": eastPlayer.PlayerId}) table.State2Broadcast() } case game.ActionHit: // 出牌 err = player.ActionHit(actionHit) if err != nil { return } err = player.RoundOver() if err != nil { return } player.State2Assign(player.PlayerId) table.SetState(state.S{"Hits": state.SetSliceAppend(actionHit)}) // 计算其他玩家碰、杠、胡可能 otherPlayers := table.GetPlayers(player.PlayerId) waitOpts := false for _, p := range otherPlayers { if p.Status == 3 { continue } if actionHit/10 != p.TileLack { // 碰,手牌中有2张相同 if _, ok := game.FilterTiles(p.MjHand, actionHit, actionHit); ok { p.Opts = append(p.Opts, game.ActionPeng) } // 杠,手牌中有3张相同 if _, ok := game.FilterTiles(p.MjHand, actionHit, actionHit, actionHit); ok { p.Opts = append(p.Opts, game.ActionGang) p.SetState(state.S{"GangTiles": []int8{actionHit}}) } } if !game.IsHuaZhu(p.TileLack, actionHit, p.MjHand) { // 花猪 wins := game.GetHandWins(actionHit, p.MjHand) if len(wins) > 0 { p.Opts = append(p.Opts, game.ActionHu) } } if len(p.Opts) > 0 { p.Opts = append(p.Opts, game.ActionCancel) p.SetState(state.S{"Opts": p.Opts}) p.State2Assign(p.PlayerId) waitOpts = true } } if !waitOpts { // 下一家摸牌 nextPlayer, e := table.NextDrawPlayer(player.PlayerId) if e != nil { err = e return } if nextPlayer != nil { err = nextPlayer.RoundStart() if err != nil { return } nextPlayer.State2Assign(nextPlayer.PlayerId) } } table.State2Broadcast() case game.ActionPeng: err = player.ActionPeng() if err != nil { return } err = player.RoundOver() if err != nil { return } // 碰完打一张 player.SetState(state.S{"Opts": []int8{game.ActionHit}}) player.State2Assign(player.PlayerId) player.SetState(state.S{"MjPeng": player.MjPeng}) player.State2Broadcast(player.PlayerId) table.State2Broadcast() case game.ActionGang: gangType, e := player.ActionGang(actionHit) if e != nil { err = e return } err = player.RoundOver() if err != nil { return } var gangProfit int64 switch gangType { case 1: // 点杠 gangProfit = 2 hitPlayer := table.GetPlayer(table.HitPlayerId) hitPlayer.SetState(state.S{"Coin": hitPlayer.Coin - gangProfit}) hitPlayer.State2Broadcast() case 2: // 巴杠 fallthrough case 3: // 暗杠 score := gangType - 1 for _, p := range table.GetPlayers(player.PlayerId) { if p.Status != 3 { gangProfit += score p.SetState(state.S{"Coin": p.Coin - score}) p.State2Broadcast() } } } // 摸牌, opts player.SetState(state.S{"MjDraw": table.MjWall.Pop()}) table.SetState(state.S{"MjWall": table.MjWall}) table.State2Broadcast() opts := []int8{game.ActionHit} gangTiles := player.GetSelfGangTiles() if len(gangTiles) > 0 { opts = append(opts, game.ActionGang) player.SetState(state.S{"GangTiles": gangTiles}) } if !game.IsHuaZhu(player.TileLack, player.MjDraw, player.MjHand) { wins := game.GetHandWins(player.MjDraw, player.MjHand) if len(wins) > 0 { opts = append(opts, game.ActionHu) } } player.SetState(state.S{"Opts": opts}) player.State2Assign(player.PlayerId) // 广播杠 player.SetState(state.S{ "Coin": player.Coin + gangProfit, "MjGang": player.MjGang, }) player.State2Broadcast() case game.ActionHu: // 自摸 selfDraw := table.HitPlayerId == player.PlayerId winTile := player.MjDraw if !selfDraw { winTile = table.PrevHit() } if game.IsHuaZhu(player.TileLack, winTile, player.MjHand) { // 花猪 err = errors.New("不可胡") return } wins := game.GetHandWins(winTile, player.MjHand) if len(wins) == 0 { err = errors.New("不可胡") return } // calc win coins player.WinSettle = table.PlayerWin(player, selfDraw, wins) var winProfit int64 weight := int64(player.WinSettle.Weight) if !selfDraw { hitPlayer := table.GetPlayer(table.HitPlayerId) winProfit = weight hitPlayer.SetState(state.S{"Coin": hitPlayer.Coin - weight}) } else { // 自摸 for _, p := range table.GetPlayers(player.PlayerId) { if p.Status != 3 { winProfit += weight p.SetState(state.S{"Coin": p.Coin - weight}) p.State2Broadcast() } } } // 设置状态赢 player.SetState(state.S{ "Coin": player.Coin + winProfit, "Status": int8(3), "WinSettle": player.WinSettle, }) player.State2Broadcast() // 整理手牌 cards := game.InsertHand(player.TileLack, winTile, player.MjHand) if selfDraw { player.SetState(state.S{"MjDraw": int8(0)}) } else { table.SetState(state.S{"Hits": state.SetSlicePopN(1)}) hitPlayer := table.GetPlayer(table.HitPlayerId) hitPlayer.SetState(state.S{"MjHits": state.SetSlicePopN(1)}) hitPlayer.State2Broadcast() } player.SetState(state.S{"MjHand": cards}) err = player.RoundOver() if err != nil { return } player.State2Assign(player.PlayerId) // 1家未胡牌游戏结束 nonWins := collect.Filter(table.GetPlayers(), func(_ int, p *game.MjPlayer) bool { return p.Status != 3 }) if len(nonWins) == 1 { err = table.GameOver() return } // nextPlayer nextPlayer, e := table.NextDrawPlayer(player.PlayerId) if e != nil { err = e return } if nextPlayer != nil { err = nextPlayer.RoundStart() if err != nil { return } nextPlayer.State2Assign(nextPlayer.PlayerId) } table.State2Broadcast() return } return }