Skip to content

Commit

Permalink
More gameplay improvements (#4)
Browse files Browse the repository at this point in the history
* Correctly handle hole progression on room creation

* Handle players crashing/leaving gracefully.

* If game would end after player leaves, don't crash

* Add experience points.

* Fake EXP support, some state update fixes.

* Fix reverse migration.
  • Loading branch information
jchv committed Jul 4, 2023
1 parent 8a06d6f commit 2fc5a62
Show file tree
Hide file tree
Showing 14 changed files with 372 additions and 48 deletions.
32 changes: 32 additions & 0 deletions database/accounts/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -911,3 +911,35 @@ func (s *Service) DeleteExpiredSessions(ctx context.Context) error {
func (s *Service) GetPlayerInventory(ctx context.Context, playerID int64) ([]dbmodels.Inventory, error) {
return s.queries.GetPlayerInventory(ctx, playerID)
}

func (s *Service) AddExp(ctx context.Context, playerID int64, add int) (pangya.Rank, int, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return 0, 0, err
}
defer tx.Rollback()

queries := s.queries.WithTx(tx)

rank, err := queries.GetPlayerRank(ctx, playerID)
if err != nil {
return 0, 0, err
}

newRank, newExp := pangya.AddExperience(pangya.Rank(rank.Rank), int(rank.Exp), add)
values, err := queries.SetPlayerRank(ctx, dbmodels.SetPlayerRankParams{
PlayerID: playerID,
Rank: int64(newRank),
Exp: int64(newExp),
})
if err != nil {
return 0, 0, err
}

err = tx.Commit()
if err != nil {
return 0, 0, err
}

return pangya.Rank(values.Rank), int(values.Exp), nil
}
1 change: 1 addition & 0 deletions game/model/room.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ type RoomState struct {
OwnerConnID uint32
NaturalWind uint32

StartPlayers int
GamePhase GamePhase
ShotSync *ShotSyncData
Holes []RoomHole
Expand Down
32 changes: 21 additions & 11 deletions game/packet/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ var ClientMessageTable = common.NewMessageTable(map[uint16]ClientMessage{
0x0032: &ClientSetIdleStatus{},
0x0033: &ClientException{},
0x0034: &ClientFirstShotReady{},
0x0037: &ClientLastPlayerLeaveGame{},
0x0042: &ClientShotArrow{},
0x0043: &ClientRequestServerList{},
0x0048: &ClientLoadProgress{},
Expand Down Expand Up @@ -132,17 +133,18 @@ type ClientRoomEdit struct {
// ClientRoomCreate is sent by the client when creating a room.
type ClientRoomCreate struct {
ClientMessage_
Unknown byte
ShotTimerMS uint32
GameTimerMS uint32
MaxUsers uint8
RoomType byte
NumHoles byte
Course byte
Unknown2 [5]byte
RoomName common.PString
Password common.PString
Unknown3 [4]byte
Unknown byte
ShotTimerMS uint32
GameTimerMS uint32
MaxUsers uint8
RoomType byte
NumHoles byte
Course byte
HoleProgression byte
Unknown2 [4]byte
RoomName common.PString
Password common.PString
Unknown3 [4]byte
}

// ClientRoomJoin is sent by the client when joining a room.
Expand Down Expand Up @@ -434,6 +436,14 @@ type ClientFirstShotReady struct {
ClientMessage_
}

// ClientLastPlayerLeaveGame is sent when the last player leaves a room.
// We can't rely on it for much; it just tells us the client thinks it shouldn't
// be punished for leaving the room since everyone else has already left.
// In the future it may need to be rejected in some cases.
type ClientLastPlayerLeaveGame struct {
ClientMessage_
}

// ClientRequestPlayerHistory is an unknown message.
type ClientRequestPlayerHistory struct {
ClientMessage_
Expand Down
8 changes: 7 additions & 1 deletion game/packet/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ var ServerMessageTable = common.NewMessageTable(map[uint16]ServerMessage{
0x005B: &ServerRoomSetWind{},
0x005D: &ServerRoomUserTypingAnnounce{},
0x0060: &ServerRoomShotCometReliefAnnounce{},
0x0061: &ServerPlayerQuitGame{},
0x0063: &ServerRoomActiveUserAnnounce{},
0x0064: &ServerRoomShotSync{},
0x0065: &ServerRoomFinishHole{},
Expand Down Expand Up @@ -580,6 +581,11 @@ type ServerRoomShotCometReliefAnnounce struct {
X, Y, Z float32
}

type ServerPlayerQuitGame struct {
ServerMessage_
ConnID uint32
}

type ServerRoomActiveUserAnnounce struct {
ServerMessage_
ConnID uint32
Expand All @@ -599,7 +605,7 @@ type PlayerGameResult struct {
Place uint8
Score int8
Unknown uint8
Unknown2 uint16
Exp uint16
Pang uint64
BonusPang uint64
Unknown3 uint64
Expand Down
8 changes: 8 additions & 0 deletions game/room/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,21 @@ type RoomPlayerJoin struct {
Entry *gamemodel.RoomPlayerEntry
PlayerData pangya.PlayerData
Conn *gamepacket.ServerConn
UpdateFunc func()
}

type RoomPlayerLeave struct {
roomEvent
ConnID uint32
}

type RoomPlayerUpdateData struct {
roomEvent
ConnID uint32
Entry *gamemodel.RoomPlayerEntry
PlayerData pangya.PlayerData
}

type RoomAction struct {
roomEvent
ConnID uint32
Expand Down
85 changes: 76 additions & 9 deletions game/room/room.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,13 @@ type RoomPlayer struct {
Entry *gamemodel.RoomPlayerEntry
Conn *gamepacket.ServerConn
PlayerData pangya.PlayerData
UpdateFunc func()
GameReady bool
ShotSync *gamemodel.ShotSyncData
TurnEnd bool
HoleEnd bool
GameEnd bool
StartShot bool
Pang uint64
BonusPang uint64
LastTotal int8
Expand Down Expand Up @@ -186,6 +189,9 @@ func (r *Room) handleEvent(ctx context.Context, t *actor.Task[RoomEvent], msg ac
case RoomPlayerLeave:
return rejectOnError(r.handlePlayerLeave(ctx, event))

case RoomPlayerUpdateData:
return rejectOnError(r.handlePlayerUpdateData(ctx, event))

case RoomAction:
return rejectOnError(r.handleRoomAction(ctx, event))

Expand Down Expand Up @@ -291,6 +297,7 @@ func (r *Room) handlePlayerJoin(ctx context.Context, event RoomPlayerJoin) error
Entry: event.Entry,
Conn: event.Conn,
PlayerData: event.PlayerData,
UpdateFunc: event.UpdateFunc,
})
if present {
return errors.New("already in room")
Expand Down Expand Up @@ -324,6 +331,16 @@ func (r *Room) handlePlayerLeave(ctx context.Context, event RoomPlayerLeave) err
return r.removePlayer(ctx, event.ConnID)
}

func (r *Room) handlePlayerUpdateData(ctx context.Context, event RoomPlayerUpdateData) error {
if pair := r.players.GetPair(event.ConnID); pair != nil {
*pair.Value.Entry = *event.Entry
pair.Value.PlayerData = event.PlayerData
// TODO: only need to send delta here
return r.broadcastPlayerList(ctx)
}
return nil
}

func (r *Room) handleRoomAction(ctx context.Context, event RoomAction) error {
if event.Action.Rotation != nil {
if pair := r.players.GetPair(event.ConnID); pair != nil {
Expand Down Expand Up @@ -469,15 +486,19 @@ func (r *Room) handleRoomStartGame(ctx context.Context, event RoomStartGame) err
Players: make([]gamepacket.GamePlayer, r.players.Len()),
},
}
r.state.StartPlayers = r.players.Len()
for i, pair := 0, r.players.Oldest(); pair != nil; pair = pair.Next() {
// Clear ready status.
pair.Value.Entry.StatusFlags &^= gamemodel.RoomStateReady

// Set initial turn order.
// Set initial player state.
pair.Value.TurnOrder = i
pair.Value.Distance = math.Inf(1)
pair.Value.Stroke = 0
pair.Value.LastTotal = 0
pair.Value.TurnEnd = false
pair.Value.HoleEnd = false
pair.Value.GameEnd = false

player := pair.Value
gameInit.Full.Players[i] = gamepacket.GamePlayer{
Expand Down Expand Up @@ -635,6 +656,7 @@ func (r *Room) handleRoomGameShotSync(ctx context.Context, event RoomGameShotSyn
Data: *r.state.ShotSync,
})
if pair := r.players.GetPair(r.state.ShotSync.ActiveConnID); pair != nil {
pair.Value.StartShot = true
pair.Value.Pang = uint64(r.state.ShotSync.Pang)
pair.Value.BonusPang = uint64(r.state.ShotSync.BonusPang)

Expand Down Expand Up @@ -679,9 +701,21 @@ func (r *Room) endTurn(ctx context.Context) error {
r.broadcast(ctx, &gamepacket.ServerRoomShotEnd{
ConnID: r.state.ActiveConnID,
})
if pair := r.players.GetPair(r.state.ActiveConnID); pair != nil {
if pair.Value.GameEnd {
r.broadcast(ctx, &gamepacket.ServerRoomPlayerFinished{})
}
}
for pair := r.players.Oldest(); pair != nil; pair = pair.Next() {
pair.Value.TurnEnd = false
}
return r.nextTurn(ctx)
}

func (r *Room) nextTurn(ctx context.Context) error {
if pair := r.players.GetPair(r.state.ActiveConnID); pair != nil {
pair.Value.StartShot = false
}
nextPlayer := r.getNextPlayer()
if nextPlayer == nil {
return r.endHole(ctx)
Expand Down Expand Up @@ -724,15 +758,15 @@ func (r *Room) getNextPlayer() *RoomPlayer {
}

func (r *Room) endHole(ctx context.Context) error {
r.state.CurrentHole++
if r.state.CurrentHole > r.state.NumHoles {
return r.endGame(ctx)
} else {
if r.haveNextHole() {
r.advanceHole()
r.broadcast(ctx, &gamepacket.ServerRoomFinishHole{})
for pair := r.players.Oldest(); pair != nil; pair = pair.Next() {
pair.Value.HoleEnd = false
}
r.setupNextTurnOrder()
} else {
return r.endGame(ctx)
}
r.stateUpdated(ctx)
return nil
Expand Down Expand Up @@ -760,17 +794,23 @@ func (r *Room) setupNextTurnOrder() {
}

func (r *Room) endGame(ctx context.Context) error {
if r.players.Len() == 0 {
return nil
}
results := &gamepacket.ServerRoomFinishGame{
NumPlayers: uint8(r.players.Len()),
Standings: make([]gamepacket.PlayerGameResult, r.players.Len()),
}
for i, pair := 0, r.players.Oldest(); pair != nil; pair = pair.Next() {
clearBonus := r.lobby.configProvider.GetCourseBonus(r.state.Course, r.state.StartPlayers, int(r.state.NumHoles))
exp := int(clearBonus / 2) // TODO: it should be based on course difficulty I believe.
bonusPang := pair.Value.BonusPang
bonusPang += r.lobby.configProvider.GetCourseBonus(r.state.Course, r.players.Len(), int(r.state.NumHoles))
bonusPang += clearBonus
results.Standings[i].ConnID = pair.Value.Entry.ConnID
results.Standings[i].Pang = pair.Value.Pang
results.Standings[i].Score = int8(pair.Value.Score)
results.Standings[i].BonusPang = bonusPang
results.Standings[i].Exp = uint16(exp)

totalPang := bonusPang + pair.Value.Pang

Expand All @@ -779,10 +819,18 @@ func (r *Room) endGame(ctx context.Context) error {
log.WithError(err).Error("failed giving game-ending pang")
}

_, _, err = r.accounts.AddExp(ctx, int64(pair.Value.Entry.PlayerID), exp)
if err != nil {
log.WithError(err).Error("failed giving game-ending exp")
}

if err := pair.Value.Conn.SendMessage(ctx, &gamepacket.ServerPangBalanceData{PangsRemaining: uint64(newPang)}); err != nil {
log.WithError(err).Error("failed informing player of game-ending pang")
}

// Tell conn to update player so that it sees new EXP/etc.
pair.Value.UpdateFunc()

pair.Value.Score = 0
pair.Value.Pang = 0
pair.Value.BonusPang = 0
Expand Down Expand Up @@ -907,6 +955,12 @@ func (r *Room) removePlayer(ctx context.Context, connID uint32) error {
},
})
r.players.Delete(connID)
if r.state.GamePhase == gamemodel.InGame {
r.broadcast(ctx, &gamepacket.ServerPlayerQuitGame{ConnID: connID})
if r.state.ActiveConnID == connID {
r.nextTurn(ctx)
}
}
r.lobby.Send(ctx, LobbyPlayerUpdateRoom{
ConnID: connID,
RoomNumber: -1,
Expand Down Expand Up @@ -943,17 +997,30 @@ func (r *Room) roomStatus() *gamepacket.ServerRoomStatus {
RoomType: r.state.RoomType,
Course: r.state.Course,
NumHoles: r.state.NumHoles,
HoleProgression: 1,
NaturalWind: 0,
HoleProgression: r.state.HoleProgression,
NaturalWind: r.state.NaturalWind,
MaxUsers: r.state.MaxUsers,
ShotTimerMS: r.state.ShotTimerMS,
GameTimerMS: r.state.GameTimerMS,
Flags: 0,
Flags: 0, // TODO
Owner: true,
RoomName: common.ToPString(r.state.RoomName),
}
}

func (r *Room) currentHole() *gamemodel.RoomHole {
// Note: CurrentHole is 1-based.
return &r.state.Holes[r.state.CurrentHole-1]
}

func (r *Room) haveNextHole() bool {
// Note: CurrentHole is 1-based.
return r.state.CurrentHole < r.state.NumHoles
}

func (r *Room) advanceHole() {
// Note: CurrentHole is 1-based.
if r.state.CurrentHole < r.state.NumHoles {
r.state.CurrentHole++
}
}
Loading

0 comments on commit 2fc5a62

Please sign in to comment.