You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

782 lines
18 KiB

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/state"
)
type MahjongServer struct {
mahjong.UnimplementedMahjongServer
store *store.Store
deliver *deliver.Deliver
authClient auth.AuthClient
}
func NewMahjongServer(store *store.Store, deliver *deliver.Deliver, authClient auth.AuthClient) *MahjongServer {
return &MahjongServer{
store: store,
deliver: deliver,
authClient: authClient,
}
}
func (mj *MahjongServer) Run(conf config.GrpcConfig, register *discovery.Register) (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
reg := conf.Register
if reg.Name == "" {
reg.Name = mahjong.Mahjong_ServiceDesc.ServiceName
}
if reg.Addr == "" {
reg.Addr, err = discovery.RegisterAddress(conf.Address)
if err != nil {
return
}
}
if err = register.Register(reg); err != nil {
return
}
// run serve
logger.Infof("%s grpc server running %s\n", reg.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 {
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, &emptypb.Empty{})
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)
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.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
}