]> Skullheadx's Git Forge - monopoly-web.git/commitdiff
refactor + better error msg for validation
authorSkullheadx <admonty1@protonmail.com>
Tue, 9 Jun 2026 00:12:01 +0000 (20:12 -0400)
committerSkullheadx <admonty1@protonmail.com>
Tue, 9 Jun 2026 00:12:01 +0000 (20:12 -0400)
15 files changed:
game/actions.go [new file with mode: 0644]
game/chance.go
game/chest.go
game/config.go
game/game.go
game/handlers.go [new file with mode: 0644]
game/helpers.go
game/jail.go
game/movement.go
game/server.go [new file with mode: 0644]
game/todo
game/types.go
game/validate.go [new file with mode: 0644]
monopoly-web
public/index.js

diff --git a/game/actions.go b/game/actions.go
new file mode 100644 (file)
index 0000000..bd7c2f1
--- /dev/null
@@ -0,0 +1,50 @@
+package game
+
+func (ctx *Context) RollDice() {
+       ctx.EventPop()
+
+       // Roll Dice
+       diceRoll1 := ctx.Random.Int32N(6) + 1
+       diceRoll2 := ctx.Random.Int32N(6) + 1
+
+       ctx.Turn.NumDiceRolled++
+       ctx.Turn.DiceRollsRemaining--
+
+       if diceRoll1 == diceRoll2 {
+               ctx.Turn.RolledDoubles = true
+               ctx.Turn.DiceRollsRemaining++
+
+               ctx.RemovePlayerFromJail(ctx.Turn.Current)
+
+               ctx.Turn.EventStack = append(ctx.Turn.EventStack, EventRollDice)
+       }
+
+       if ctx.Turn.NumDiceRolled >= 3 {
+               ctx.PutPlayerInJail(ctx.Turn.Current)
+       }
+
+       ctx.Turn.MoveQueue = append(ctx.Turn.MoveQueue, diceRoll1+diceRoll2)
+}
+
+func (ctx *Context) Buy() {
+       ctx.EventPop()
+
+       playerID := ctx.Turn.Current
+       player := ctx.Players.Alive[playerID.Index()]
+       prop := ColorProperties[BoardSpaces[player.CurrentSpaceID.Index()].SubIndexID]
+
+       for i, p := range ctx.Properties.Owners {
+               if p.SpaceID == player.CurrentSpaceID {
+                       ctx.Properties.Owners[i].OwnerID = playerID
+               }
+       }
+
+       ctx.AdjustPlayerMoney(playerID, -prop.Price)
+}
+
+func (ctx *Context) EndTurn() {
+       ctx.EventPop()
+       nextTurnPlayerID := PlayerID{ID: (ctx.Turn.Current.ID + 1) % int32(len(ctx.Players.Alive))}
+       ctx.Turn = initTurn(nextTurnPlayerID, !ctx.PlayerCanMove(nextTurnPlayerID))
+       // TODO: send options list to the next player
+}
index 13034755315cf205339a73a188fc895a85c9f581..3baa945e5e1656fb45257eae47266889acd2f1ab 100644 (file)
@@ -61,7 +61,7 @@ func (ctx *Context) ProcessChance() {
                case 6:
                        ctx.Turn.MoveQueue = append(ctx.Turn.MoveQueue, GetPlayerMoveDistance(currentPos, SpecialSpaces.Go))
                case 7:
-                       ctx.Players.Alive[ctx.Turn.Current.Index()].GetOutOfJailCards++
+                       ctx.Players.Alive[ctx.Turn.Current.Index()].PardonCards++
                case 8:
                        ctx.Turn.MoveQueue = append(ctx.Turn.MoveQueue, GetPlayerMoveDistance(currentPos, SpecialSpaces.Boardwalk))
                case 9:
index 9a31622d93695f41b12ab15f3b1ee0c0aa197f8b..9dbfb896f23d276f1dfe0dc86decb4c74fd2db50 100644 (file)
@@ -65,7 +65,7 @@ func (ctx *Context) ProcessChest() {
                case 13:
                        ctx.PutPlayerInJail(visitorID)
                case 14:
-                       ctx.Players.Alive[ctx.Turn.Current.Index()].GetOutOfJailCards++
+                       ctx.Players.Alive[ctx.Turn.Current.Index()].PardonCards++
                case 15:
                        ctx.AdjustPlayerMoney(visitorID, 10)
                }
index 5f01d9f8b2cc7779636c676ba4ad9238f2d03732..9e309cee78e19628016319cafa99ad8d368801ac 100644 (file)
@@ -218,7 +218,7 @@ const GoSalary int32 = 200
 const ColorMaxHouses = 4
 
 const StartingMoney int32 = 1500
-const StartingGetOutOfJailFreeCards int32 = 0
+const StartingPardonCards int32 = 0
 
 var ChestCards = [...]string{
        0:  "Advance to Go. (Collect $200)",
index 862d00b9684b440182cdbc192f0b8ceb47973746..e2dc5d30554030d60ab5deaec6f0da403c6882f8 100644 (file)
@@ -2,342 +2,18 @@ package game
 
 import (
        "context"
-       "encoding/json"
-       "errors"
-       "fmt"
-       "github.com/coder/websocket"
-       "github.com/google/uuid"
-       "golang.org/x/time/rate"
-       "io"
-       "log"
-       "math/rand/v2"
-       "net"
-       "net/http"
-       "sync"
-       "time"
 )
 
-type MonopolyServer struct {
-       subscriberMessageBuffer int
-       publishLimiter          *rate.Limiter
-
-       logf func(f string, v ...any)
-
-       serveMux http.ServeMux
-
-       subscribersMu sync.Mutex
-       subscribers   map[*subscriber]string
-
-       // uuid to username
-       users map[string]string
-
-       gameCtxMu sync.Mutex
-       gameCtx   *Context
-       randSeed  *rand.PCG
-}
-
-func (ctx *Context) logGameCtx() {
-
-       type miniContext struct {
-               Players    Players
-               Turn       Turn
-               Visitors   Visitors
-               properties Properties
-       }
-
-       data, _ := json.MarshalIndent(miniContext{
-               Players:    ctx.Players,
-               Turn:       ctx.Turn,
-               Visitors:   ctx.Visitors,
-               properties: ctx.Properties,
-       }, "", "  ")
-       fmt.Println(string(data))
-}
-
-func NewMonopolyServer() *MonopolyServer {
-       ms := &MonopolyServer{
-               subscriberMessageBuffer: 16,
-               logf:                    log.Printf,
-               subscribers:             make(map[*subscriber]string),
-               users:                   make(map[string]string),
-               publishLimiter:          rate.NewLimiter(rate.Every(time.Millisecond*100), 8),
-               gameCtx:                 nil,
-               randSeed:                rand.NewPCG(20, 26),
-       }
-       ms.serveMux.Handle("/", http.FileServer(http.Dir("public/")))
-       ms.serveMux.HandleFunc("/login", ms.loginHandler)
-       ms.serveMux.HandleFunc("/loggedin", ms.loggedInHandler)
-       ms.serveMux.HandleFunc("/subscribe", ms.subscribeHandler)
-       ms.serveMux.HandleFunc("/start", ms.startHandler)
-       ms.serveMux.HandleFunc("/roll", ms.rollHandler)
-       ms.serveMux.HandleFunc("/buy", ms.buyHandler)
-       // ms.serveMux.HandleFunc("/auction", ms.auctionHandler)
-
-       return ms
-}
-
-type subscriber struct {
-       msgs      chan []byte
-       closeSlow func()
-}
-
-func (ms *MonopolyServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-       ms.serveMux.ServeHTTP(w, r)
-}
-
-func (ms *MonopolyServer) subscribeHandler(w http.ResponseWriter, r *http.Request) {
-       err := ms.subscribe(w, r)
-       if errors.Is(err, context.Canceled) {
-               return
-       }
-
-       if websocket.CloseStatus(err) == websocket.StatusNormalClosure || websocket.CloseStatus(err) == websocket.StatusGoingAway {
-               return
-       }
-
-       if err != nil {
-               ms.logf("%v", err)
-               return
-       }
-}
-
-func (ms *MonopolyServer) loggedInHandler(w http.ResponseWriter, r *http.Request) {
-       if r.Method != "GET" {
-               http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
-               return
-       }
-
-       cookie, err := r.Cookie("user")
-       if err != nil {
-               if err == http.ErrNoCookie {
-                       http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
-                       return
-               }
-
-               http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
-               return
-       }
-
-       userUUID := cookie.Value
-
-       _, ok := ms.users[userUUID]
-       if !ok {
-               http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
-               return
-       }
-
-       w.WriteHeader(http.StatusOK)
-}
-
-func (ms *MonopolyServer) loginHandler(w http.ResponseWriter, r *http.Request) {
-       if r.Method != "POST" {
-               http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
-               return
-       }
-
-       body := http.MaxBytesReader(w, r.Body, 8192)
-       username, err := io.ReadAll(body)
-       if err != nil {
-               http.Error(w, http.StatusText(http.StatusRequestEntityTooLarge), http.StatusRequestEntityTooLarge)
-               return
-       }
-       userUUID := uuid.NewString()
-
-       ms.users[userUUID] = string(username)
-
-       http.SetCookie(w, &http.Cookie{
-               Name:     "user",
-               Value:    userUUID,
-               Path:     "/",
-               Domain:   "",
-               MaxAge:   int(7 * 24 * time.Hour / time.Second),
-               HttpOnly: true,
-               Secure:   true,
-               SameSite: http.SameSiteLaxMode,
-       })
-       w.WriteHeader(http.StatusOK)
-}
-
-func (ms *MonopolyServer) startHandler(w http.ResponseWriter, r *http.Request) {
-       if r.Method != "POST" {
-               http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
-               return
-       }
-
-       cookie, err := r.Cookie("user")
-       if err != nil {
-               if err == http.ErrNoCookie {
-                       http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
-                       return
-               }
-
-               http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
-               return
-       }
-
-       userUUID := cookie.Value
-       _, ok := ms.users[userUUID]
-       if !ok {
-               http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
-               return
-       }
-
-       if ms.gameCtx != nil {
-               http.Error(w, http.StatusText(http.StatusConflict), http.StatusConflict)
-               return
-       }
-
-       ms.start()
-
-       w.WriteHeader(http.StatusAccepted)
-}
-
-func (ms *MonopolyServer) buyHandler(w http.ResponseWriter, r *http.Request) {
-       if r.Method != "POST" {
-               http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
-               return
-       }
-
-       cookie, err := r.Cookie("user")
-       if err != nil {
-               if err == http.ErrNoCookie {
-                       http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
-                       return
-               }
-
-               http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
-               return
-       }
-
-       userUUID := cookie.Value
-
-       _, ok := ms.users[userUUID]
-       if !ok {
-               http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
-               return
-       }
-
-       if ms.gameCtx == nil {
-               http.Error(w, "Game has not started yet", http.StatusConflict)
-               return
-       }
-       if !ms.gameCtx.ValidateCanBuy(userUUID) {
-               http.Error(w, "Not your turn", http.StatusForbidden)
-               return
-       }
-       ms.buy()
-       w.WriteHeader(http.StatusOK)
-}
-
-func (ms *MonopolyServer) buy() {
-       ms.gameCtx.Buy()
-
-       ms.gameCtx.logGameCtx()
-}
-
-func (ms *MonopolyServer) rollHandler(w http.ResponseWriter, r *http.Request) {
-       if r.Method != "POST" {
-               http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
-               return
-       }
-
-       cookie, err := r.Cookie("user")
-       if err != nil {
-               if err == http.ErrNoCookie {
-                       http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
-                       return
-               }
-
-               http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
-               return
-       }
-
-       userUUID := cookie.Value
-
-       _, ok := ms.users[userUUID]
-       if !ok {
-               http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
-               return
-       }
-
-       if ms.gameCtx == nil {
-               http.Error(w, "Game has not started yet", http.StatusConflict)
-               return
-       }
-       if !ms.gameCtx.ValidateCanRoll(userUUID) {
-               http.Error(w, "Not your turn", http.StatusForbidden)
-               return
-       }
-       ms.roll()
-       w.WriteHeader(http.StatusOK)
-}
-
-func (ms *MonopolyServer) roll() {
-       ms.gameCtx.RollDice()
-       ms.gameCtx.ProcessMovement()
-
-       ms.gameCtx.logGameCtx()
+func (ms *MonopolyServer) addSubscriber(s *subscriber, userUUID string) {
+       ms.subscribersMu.Lock()
+       ms.subscribers[s] = userUUID
+       ms.subscribersMu.Unlock()
 }
 
-func (ms *MonopolyServer) subscribe(w http.ResponseWriter, r *http.Request) error {
-       cookie, err := r.Cookie("user")
-       if err != nil {
-               if err == http.ErrNoCookie {
-                       http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
-                       return err
-               }
-
-               http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
-               return err
-       }
-
-       userUUID := cookie.Value
-
-       var mu sync.Mutex
-       var c *websocket.Conn
-       var closed bool
-
-       s := &subscriber{
-               msgs: make(chan []byte, ms.subscriberMessageBuffer),
-               closeSlow: func() {
-                       mu.Lock()
-                       defer mu.Unlock()
-                       closed = true
-                       if c != nil {
-                               c.Close(websocket.StatusPolicyViolation, "connection too slow to keep up with messages")
-                       }
-               },
-       }
-       ms.addSubscriber(s, userUUID)
-       defer ms.deleteSubscriber(s)
-
-       c2, err := websocket.Accept(w, r, nil)
-       if err != nil {
-               return err
-       }
-
-       mu.Lock()
-       if closed {
-               mu.Unlock()
-               return net.ErrClosed
-       }
-
-       c = c2
-       mu.Unlock()
-       defer c.CloseNow()
-
-       ctx := c.CloseRead(context.Background())
-       for {
-               select {
-               case msg := <-s.msgs:
-                       err := writeTimeout(ctx, time.Second*5, c, msg)
-                       if err != nil {
-                               return err
-                       }
-               case <-ctx.Done():
-                       return ctx.Err()
-               }
-       }
+func (ms *MonopolyServer) deleteSubscriber(s *subscriber) {
+       ms.subscribersMu.Lock()
+       delete(ms.subscribers, s)
+       ms.subscribersMu.Unlock()
 }
 
 func (ms *MonopolyServer) start() {
@@ -367,272 +43,15 @@ func (ms *MonopolyServer) start() {
 
 }
 
-// func (ms *MonopolyServer) publish(msg []byte) {
-//     ms.subscribersMu.Lock()
-//     defer ms.subscribersMu.Unlock()
-//
-//     ms.publishLimiter.Wait(context.Background())
-//
-//     for s := range ms.subscribers {
-//             select {
-//             case s.msgs <- msg:
-//             default:
-//                     go s.closeSlow()
-//             }
-//     }
-//
-// }
-
-func (ms *MonopolyServer) addSubscriber(s *subscriber, userUUID string) {
-       ms.subscribersMu.Lock()
-       ms.subscribers[s] = userUUID
-       ms.subscribersMu.Unlock()
-}
-
-func (ms *MonopolyServer) deleteSubscriber(s *subscriber) {
-       ms.subscribersMu.Lock()
-       delete(ms.subscribers, s)
-       ms.subscribersMu.Unlock()
-}
-
-func writeTimeout(ctx context.Context, timeout time.Duration, c *websocket.Conn, msg []byte) error {
-       ctx, cancel := context.WithTimeout(ctx, timeout)
-       defer cancel()
-
-       return c.Write(ctx, websocket.MessageText, msg)
-}
-
-func initTurn(pID PlayerID, inJail bool) Turn {
-
-       eS := []EventType{EventEndTurn}
-       if inJail {
-               eS = append(eS, EventJail)
-       } else {
-               eS = append(eS, EventRollDice)
-       }
-
-       return Turn{
-               Current:            pID,
-               Ended:              false,
-               DiceRollsRemaining: StartingDiceRolls,
-               NumDiceRolled:      0,
-               RolledDoubles:      false,
-               MoveQueue:          []int32{},
-               EventStack:         eS,
-               InDebt:             false,
-               Modifier: Modifiers{
-                       RailroadRentMultiplier:     1,
-                       UtilityForceRentMultiplier: false,
-               },
-       }
-}
-
-func InitCtx(randSeed rand.Source, players []Player) *Context {
-       startingPlayerID := PlayerID{ID: 0}
-
-       playerIDs := []PlayerID{}
-       for i := range players {
-               playerIDs = append(playerIDs, PlayerID{ID: int32(i)})
-       }
-
-       ownableProps := []OwnableProperty{}
-       for i, s := range BoardSpaces {
-               spaceID := SpaceID{ID: int32(i)}
-
-               propertyType := s.PropertyType
-               if propertyType == TypeColor || propertyType == TypeRailroad || propertyType == TypeUtility {
-                       ownableProps = append(ownableProps, OwnableProperty{
-                               OwnerID: BankPlayerID,
-                               SpaceID: spaceID,
-                       })
-               }
-       }
-
-       return &Context{
-               Random: rand.New(randSeed),
-               Players: Players{
-                       Alive:   players,
-                       CanMove: playerIDs,
-               },
-               Turn: initTurn(startingPlayerID, false),
-               Visitors: Visitors{
-                       Unowned:  []UnownedPropertyVisitor{},
-                       Color:    []OwnedColorVisitor{},
-                       Railroad: []OwnedRailroadVisitor{},
-                       Utility:  []OwnedUtilityVisitor{},
-                       Go:       []PlayerID{},
-                       Tax:      []TaxVisitor{},
-                       Chance:   []PlayerID{},
-                       Chest:    []PlayerID{},
-                       InJail:   []InJailVisitor{},
-                       // Parking:  []PlayerID{},
-                       // Police:   []PlayerID{},
-               },
-               Properties: Properties{
-                       Owners:    ownableProps,
-                       Mortgages: []PropertyID{},
-               },
-       }
-}
-
-func InitPlayer(userUUID string) Player {
-       return Player{
-               UserUUID:          userUUID,
-               Money:             StartingMoney,
-               CurrentSpaceID:    SpecialSpaces.Go,
-               GetOutOfJailCards: StartingGetOutOfJailFreeCards,
-       }
-}
-
-func (ctx *Context) PlayerCanMove(pID PlayerID) bool {
-       for i, _ := range ctx.Players.CanMove {
-               if i == pID.Index() {
-                       return true
-               }
-       }
-       return false
-}
-
-func (ctx *Context) GetCurrentTurnPlayer() *Player {
-       return &ctx.Players.Alive[ctx.Turn.Current.Index()]
-}
-
-func (ctx *Context) ValidateIsTurn(userUUID string) bool {
-       if ctx.GetCurrentTurnPlayer().UserUUID == userUUID {
-               return true
-       }
-       return false
-}
-
-func (ctx *Context) ValidateCanBuy(userUUID string) bool {
-       player := ctx.Players.Alive[ctx.Turn.Current.Index()]
-       prop := ColorProperties[BoardSpaces[player.CurrentSpaceID.Index()].SubIndexID]
-
-       if ctx.ValidateIsTurn(userUUID) && ctx.EventPeek() == EventLandUnowned && player.Money >= prop.Price {
-               return true
-       }
-       return false
-}
-
-func (ctx *Context) ValidateCanRoll(userUUID string) bool {
-       if ctx.ValidateIsTurn(userUUID) && (ctx.EventPeek() == EventRollDice || ctx.EventPeek() == EventJail) {
-               return true
-       }
-       return false
-}
-
-func (ctx *Context) ValidateCanEndTurn(userUUID string) bool {
-       if ctx.ValidateIsTurn(userUUID) && ctx.EventPeek() == EventEndTurn && ctx.GetCurrentTurnPlayer().Money >= 0 {
-               return true
-       }
-       return false
-}
-
-func (ctx *Context) ValidateCanExitJail(userUUID string) bool {
-       inJail := false
-       for _, iJV := range ctx.Visitors.InJail {
-               if ctx.Players.Alive[iJV.VisitorID.Index()].UserUUID == userUUID {
-                       inJail = true
-               }
-       }
-       if ctx.ValidateIsTurn(userUUID) && inJail && ctx.EventPeek() == EventJail {
-               return true
-       }
-       return false
-}
-
-func (ctx *Context) ProcessLanding() {
-       ctx.ProcessGo()
-       ctx.ProcessTax()
-
-       ctx.ProcessOwnedColors()
-       ctx.ProcessOwnedUtility()
-       ctx.ProcessOwnedRailroad()
-       ctx.ProcessUnowned()
-
-       ctx.ProcessChance()
-       ctx.ProcessChest()
-       // ProcessPolice()
-
-       ctx.ProcessJail()
-}
-
-func (ctx *Context) RollDice() {
-       ctx.EventPop()
-
-       // Roll Dice
-       diceRoll1 := ctx.Random.Int32N(6) + 1
-       diceRoll2 := ctx.Random.Int32N(6) + 1
-
-       ctx.Turn.NumDiceRolled++
-       ctx.Turn.DiceRollsRemaining--
-
-       if diceRoll1 == diceRoll2 {
-               ctx.Turn.RolledDoubles = true
-               ctx.Turn.DiceRollsRemaining++
-
-               ctx.RemovePlayerFromJail(ctx.Turn.Current)
-
-               ctx.Turn.EventStack = append(ctx.Turn.EventStack, EventRollDice)
-       }
-
-       if ctx.Turn.NumDiceRolled >= 3 {
-               ctx.PutPlayerInJail(ctx.Turn.Current)
-       }
-
-       ctx.Turn.MoveQueue = append(ctx.Turn.MoveQueue, diceRoll1+diceRoll2)
-}
-
-func (ctx *Context) Buy() {
-       ctx.EventPop()
-
-       playerID := ctx.Turn.Current
-       player := ctx.Players.Alive[playerID.Index()]
-       prop := ColorProperties[BoardSpaces[player.CurrentSpaceID.Index()].SubIndexID]
-
-       for i, p := range ctx.Properties.Owners {
-               if p.SpaceID == player.CurrentSpaceID {
-                       ctx.Properties.Owners[i].OwnerID = playerID
-               }
-       }
-
-       ctx.AdjustPlayerMoney(playerID, -prop.Price)
-}
-
-func (ctx *Context) EndTurn() {
-       ctx.EventPop()
-       nextTurnPlayerID := PlayerID{ID: (ctx.Turn.Current.ID + 1) % int32(len(ctx.Players.Alive))}
-       ctx.Turn = initTurn(nextTurnPlayerID, !ctx.PlayerCanMove(nextTurnPlayerID))
-       // TODO: send options list to the next player
-}
+func (ms *MonopolyServer) roll() {
+       ms.gameCtx.RollDice()
+       ms.gameCtx.ProcessMovement()
 
-func (ctx *Context) IsMortgaged(propID PropertyID) bool {
-       for _, oPID := range ctx.Properties.Mortgages {
-               if oPID == propID {
-                       return true
-               }
-       }
-       return false
+       ms.gameCtx.logGameCtx()
 }
 
-func (ctx *Context) IsOwned(spaceID SpaceID) bool {
-       for _, prop := range ctx.Properties.Owners {
-               if spaceID == prop.SpaceID {
-                       if prop.OwnerID == BankPlayerID {
-                               return false
-                       } else {
-                               return true
-                       }
-               }
-       }
-       panic("Space is not an ownable property")
-}
+func (ms *MonopolyServer) buy() {
+       ms.gameCtx.Buy()
 
-func (ctx *Context) getPropID(spaceID SpaceID) PropertyID {
-       for i, prop := range ctx.Properties.Owners {
-               if spaceID == prop.SpaceID {
-                       return PropertyID{ID: int32(i)}
-               }
-       }
-       panic("Space is not an ownable property")
+       ms.gameCtx.logGameCtx()
 }
diff --git a/game/handlers.go b/game/handlers.go
new file mode 100644 (file)
index 0000000..ef43a61
--- /dev/null
@@ -0,0 +1,272 @@
+package game
+
+import (
+       "context"
+       "errors"
+       "github.com/coder/websocket"
+       "github.com/google/uuid"
+       "io"
+       "net"
+       "net/http"
+       "sync"
+       "time"
+)
+
+func (ms *MonopolyServer) subscribeHandler(w http.ResponseWriter, r *http.Request) {
+       err := ms.subscribe(w, r)
+       if errors.Is(err, context.Canceled) {
+               return
+       }
+
+       if websocket.CloseStatus(err) == websocket.StatusNormalClosure || websocket.CloseStatus(err) == websocket.StatusGoingAway {
+               return
+       }
+
+       if err != nil {
+               ms.logf("%v", err)
+               return
+       }
+}
+
+func (ms *MonopolyServer) loggedInHandler(w http.ResponseWriter, r *http.Request) {
+       if r.Method != "GET" {
+               http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
+               return
+       }
+
+       cookie, err := r.Cookie("user")
+       if err != nil {
+               if err == http.ErrNoCookie {
+                       http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
+                       return
+               }
+
+               http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
+               return
+       }
+
+       userUUID := cookie.Value
+
+       _, ok := ms.users[userUUID]
+       if !ok {
+               http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
+               return
+       }
+
+       w.WriteHeader(http.StatusOK)
+}
+
+func (ms *MonopolyServer) loginHandler(w http.ResponseWriter, r *http.Request) {
+       if r.Method != "POST" {
+               http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
+               return
+       }
+
+       body := http.MaxBytesReader(w, r.Body, 8192)
+       username, err := io.ReadAll(body)
+       if err != nil {
+               http.Error(w, http.StatusText(http.StatusRequestEntityTooLarge), http.StatusRequestEntityTooLarge)
+               return
+       }
+       userUUID := uuid.NewString()
+
+       ms.users[userUUID] = string(username)
+
+       http.SetCookie(w, &http.Cookie{
+               Name:     "user",
+               Value:    userUUID,
+               Path:     "/",
+               Domain:   "",
+               MaxAge:   int(7 * 24 * time.Hour / time.Second),
+               HttpOnly: true,
+               Secure:   true,
+               SameSite: http.SameSiteLaxMode,
+       })
+       w.WriteHeader(http.StatusOK)
+}
+
+func (ms *MonopolyServer) startHandler(w http.ResponseWriter, r *http.Request) {
+       if r.Method != "POST" {
+               http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
+               return
+       }
+
+       cookie, err := r.Cookie("user")
+       if err != nil {
+               if err == http.ErrNoCookie {
+                       http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
+                       return
+               }
+
+               http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
+               return
+       }
+
+       userUUID := cookie.Value
+       _, ok := ms.users[userUUID]
+       if !ok {
+               http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
+               return
+       }
+
+       if ms.gameCtx != nil {
+               http.Error(w, http.StatusText(http.StatusConflict), http.StatusConflict)
+               return
+       }
+
+       ms.start()
+
+       w.WriteHeader(http.StatusAccepted)
+}
+
+func (ms *MonopolyServer) buyHandler(w http.ResponseWriter, r *http.Request) {
+       if r.Method != "POST" {
+               http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
+               return
+       }
+
+       cookie, err := r.Cookie("user")
+       if err != nil {
+               if err == http.ErrNoCookie {
+                       http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
+                       return
+               }
+
+               http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
+               return
+       }
+
+       userUUID := cookie.Value
+
+       _, ok := ms.users[userUUID]
+       if !ok {
+               http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
+               return
+       }
+
+       if ms.gameCtx == nil {
+               http.Error(w, "Game has not started yet", http.StatusConflict)
+               return
+       }
+       if err := ms.gameCtx.ValidateCanBuy(userUUID); err != nil {
+               http.Error(w, err.Error(), ErrStatusCode(err))
+               return
+       }
+
+       ms.buy()
+       w.WriteHeader(http.StatusOK)
+}
+
+func (ms *MonopolyServer) rollHandler(w http.ResponseWriter, r *http.Request) {
+       if r.Method != "POST" {
+               http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
+               return
+       }
+
+       cookie, err := r.Cookie("user")
+       if err != nil {
+               if err == http.ErrNoCookie {
+                       http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
+                       return
+               }
+
+               http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
+               return
+       }
+
+       userUUID := cookie.Value
+
+       _, ok := ms.users[userUUID]
+       if !ok {
+               http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
+               return
+       }
+
+       if ms.gameCtx == nil {
+               http.Error(w, "Game has not started yet", http.StatusConflict)
+               return
+       }
+       if err := ms.gameCtx.ValidateCanRoll(userUUID); err != nil {
+               http.Error(w, err.Error(), ErrStatusCode(err))
+               return
+       }
+       ms.roll()
+       w.WriteHeader(http.StatusOK)
+}
+
+func (ms *MonopolyServer) subscribe(w http.ResponseWriter, r *http.Request) error {
+       cookie, err := r.Cookie("user")
+       if err != nil {
+               if err == http.ErrNoCookie {
+                       http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
+                       return err
+               }
+
+               http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
+               return err
+       }
+
+       userUUID := cookie.Value
+
+       var mu sync.Mutex
+       var c *websocket.Conn
+       var closed bool
+
+       s := &subscriber{
+               msgs: make(chan []byte, ms.subscriberMessageBuffer),
+               closeSlow: func() {
+                       mu.Lock()
+                       defer mu.Unlock()
+                       closed = true
+                       if c != nil {
+                               c.Close(websocket.StatusPolicyViolation, "connection too slow to keep up with messages")
+                       }
+               },
+       }
+       ms.addSubscriber(s, userUUID)
+       defer ms.deleteSubscriber(s)
+
+       c2, err := websocket.Accept(w, r, nil)
+       if err != nil {
+               return err
+       }
+
+       mu.Lock()
+       if closed {
+               mu.Unlock()
+               return net.ErrClosed
+       }
+
+       c = c2
+       mu.Unlock()
+       defer c.CloseNow()
+
+       ctx := c.CloseRead(context.Background())
+       for {
+               select {
+               case msg := <-s.msgs:
+                       err := writeTimeout(ctx, time.Second*5, c, msg)
+                       if err != nil {
+                               return err
+                       }
+               case <-ctx.Done():
+                       return ctx.Err()
+               }
+       }
+}
+
+// func (ms *MonopolyServer) publish(msg []byte) {
+//     ms.subscribersMu.Lock()
+//     defer ms.subscribersMu.Unlock()
+//
+//     ms.publishLimiter.Wait(context.Background())
+//
+//     for s := range ms.subscribers {
+//             select {
+//             case s.msgs <- msg:
+//             default:
+//                     go s.closeSlow()
+//             }
+//     }
+//
+// }
index 467e1dd8304673e71b301c09d0ea89b9b7ebc40f..c6c1081cf55284771e6d321731449ef0cc7f214c 100644 (file)
@@ -1,5 +1,11 @@
 package game
 
+import (
+       "encoding/json"
+       "fmt"
+       "math/rand/v2"
+)
+
 func (ctx *Context) AdjustPlayerMoney(playerID PlayerID, amount int32) {
        ctx.Players.Alive[playerID.Index()].Money += amount
 
@@ -10,3 +16,147 @@ func (ctx *Context) AdjustPlayerMoney(playerID PlayerID, amount int32) {
        }
 
 }
+
+func (ctx *Context) IsMortgaged(propID PropertyID) bool {
+       for _, oPID := range ctx.Properties.Mortgages {
+               if oPID == propID {
+                       return true
+               }
+       }
+       return false
+}
+
+func (ctx *Context) IsOwned(spaceID SpaceID) bool {
+       for _, prop := range ctx.Properties.Owners {
+               if spaceID == prop.SpaceID {
+                       if prop.OwnerID == BankPlayerID {
+                               return false
+                       } else {
+                               return true
+                       }
+               }
+       }
+       panic("Space is not an ownable property")
+}
+
+func (ctx *Context) getOwnablePropID(spaceID SpaceID) PropertyID {
+       for i, prop := range ctx.Properties.Owners {
+               if spaceID == prop.SpaceID {
+                       return PropertyID{ID: int32(i)}
+               }
+       }
+       panic("Space is not an ownable property")
+}
+
+func (ctx *Context) GetCurrentTurnPlayer() *Player {
+       return &ctx.Players.Alive[ctx.Turn.Current.Index()]
+}
+
+func (ctx *Context) PlayerCanMove(pID PlayerID) bool {
+       for i, _ := range ctx.Players.CanMove {
+               if i == pID.Index() {
+                       return true
+               }
+       }
+       return false
+}
+
+func initTurn(pID PlayerID, inJail bool) Turn {
+
+       eS := []EventType{EventEndTurn}
+       if inJail {
+               eS = append(eS, EventJail)
+       } else {
+               eS = append(eS, EventRollDice)
+       }
+
+       return Turn{
+               Current:            pID,
+               Ended:              false,
+               DiceRollsRemaining: StartingDiceRolls,
+               NumDiceRolled:      0,
+               RolledDoubles:      false,
+               MoveQueue:          []int32{},
+               EventStack:         eS,
+               InDebt:             false,
+               Modifier: Modifiers{
+                       RailroadRentMultiplier:     1,
+                       UtilityForceRentMultiplier: false,
+               },
+       }
+}
+
+func InitCtx(randSeed rand.Source, players []Player) *Context {
+       startingPlayerID := PlayerID{ID: 0}
+
+       playerIDs := []PlayerID{}
+       for i := range players {
+               playerIDs = append(playerIDs, PlayerID{ID: int32(i)})
+       }
+
+       ownableProps := []OwnableProperty{}
+       for i, s := range BoardSpaces {
+               spaceID := SpaceID{ID: int32(i)}
+
+               propertyType := s.PropertyType
+               if propertyType == TypeColor || propertyType == TypeRailroad || propertyType == TypeUtility {
+                       ownableProps = append(ownableProps, OwnableProperty{
+                               OwnerID: BankPlayerID,
+                               SpaceID: spaceID,
+                       })
+               }
+       }
+
+       return &Context{
+               Random: rand.New(randSeed),
+               Players: Players{
+                       Alive:   players,
+                       CanMove: playerIDs,
+               },
+               Turn: initTurn(startingPlayerID, false),
+               Visitors: Visitors{
+                       Unowned:  []UnownedPropertyVisitor{},
+                       Color:    []OwnedColorVisitor{},
+                       Railroad: []OwnedRailroadVisitor{},
+                       Utility:  []OwnedUtilityVisitor{},
+                       Go:       []PlayerID{},
+                       Tax:      []TaxVisitor{},
+                       Chance:   []PlayerID{},
+                       Chest:    []PlayerID{},
+                       InJail:   []InJailVisitor{},
+                       // Parking:  []PlayerID{},
+                       // Police:   []PlayerID{},
+               },
+               Properties: Properties{
+                       Owners:    ownableProps,
+                       Mortgages: []PropertyID{},
+               },
+       }
+}
+
+func InitPlayer(userUUID string) Player {
+       return Player{
+               UserUUID:       userUUID,
+               Money:          StartingMoney,
+               CurrentSpaceID: SpecialSpaces.Go,
+               PardonCards:    StartingPardonCards,
+       }
+}
+
+func (ctx *Context) logGameCtx() {
+
+       type miniContext struct {
+               Players    Players
+               Turn       Turn
+               Visitors   Visitors
+               properties Properties
+       }
+
+       data, _ := json.MarshalIndent(miniContext{
+               Players:    ctx.Players,
+               Turn:       ctx.Turn,
+               Visitors:   ctx.Visitors,
+               properties: ctx.Properties,
+       }, "", "  ")
+       fmt.Println(string(data))
+}
index df32d55866e188fca983c4282d27755d44712fe1..a88674fe97c22bbf38bc1384b6935d89e577190b 100644 (file)
@@ -1,9 +1,5 @@
 package game
 
-import (
-       "errors"
-)
-
 func (ctx *Context) RemovePlayerFromJail(playerID PlayerID) {
        for i, iJV := range ctx.Visitors.InJail {
                if playerID == iJV.VisitorID {
@@ -44,28 +40,14 @@ func (ctx *Context) ProcessJail() {
        }
 }
 
-var ErrNotEnoughJailCards = errors.New("Cannot use jail card: player does not have enough get out of jail free cards")
-var ErrNotEnoughMoney = errors.New("Cannot execute action: player does not have enough money")
-
-func (ctx *Context) JailUseCard() error {
+func (ctx *Context) JailPardon() {
        currID := ctx.Turn.Current
-       if ctx.Players.Alive[currID.Index()].GetOutOfJailCards > 0 {
-               ctx.RemovePlayerFromJail(currID)
-               ctx.Players.Alive[currID.Index()].GetOutOfJailCards -= 1
-               return nil
-
-       } else {
-               return ErrNotEnoughJailCards
-       }
+       ctx.RemovePlayerFromJail(currID)
+       ctx.Players.Alive[currID.Index()].PardonCards -= 1
 }
 
-func (ctx *Context) JailBuyout() error {
+func (ctx *Context) JailBuyout() {
        currID := ctx.Turn.Current
-       if ctx.Players.Alive[currID.Index()].Money >= JailBuyoutCost {
-               ctx.RemovePlayerFromJail(currID)
-               ctx.AdjustPlayerMoney(currID, -JailBuyoutCost)
-               return nil
-       } else {
-               return ErrNotEnoughMoney
-       }
+       ctx.RemovePlayerFromJail(currID)
+       ctx.AdjustPlayerMoney(currID, -JailBuyoutCost)
 }
index 98190097f894cd5f795097cba193ae91ee633152..0b165aaeab0fe51b9a79e387427424fd130209a3 100644 (file)
@@ -23,6 +23,22 @@ func (ctx *Context) ProcessMovement() {
 
 }
 
+func (ctx *Context) ProcessLanding() {
+       ctx.ProcessGo()
+       ctx.ProcessTax()
+
+       ctx.ProcessOwnedColors()
+       ctx.ProcessOwnedUtility()
+       ctx.ProcessOwnedRailroad()
+       ctx.ProcessUnowned()
+
+       ctx.ProcessChance()
+       ctx.ProcessChest()
+       // ProcessPolice()
+
+       ctx.ProcessJail()
+}
+
 func CalculateNextPos(currentPosition SpaceID, distance int32) SpaceID {
        nextPos := Add(currentPosition, distance)
        nextPos.ID %= int32(len(BoardSpaces))
@@ -59,7 +75,7 @@ func (ctx *Context) AdvancePlayer(playerID PlayerID, currentPosition SpaceID, di
        case TypePolice: // hardcoding to send straight to jail
                ctx.PutPlayerInJail(playerID)
        case TypeColor:
-               propID := ctx.getPropID(nextPos)
+               propID := ctx.getOwnablePropID(nextPos)
                ownerID := ctx.Properties.Owners[propID.Index()].OwnerID
                if ownerID != BankPlayerID { // property owned?
                        if ownerID != playerID && !ctx.IsMortgaged(propID) { // not by you
@@ -69,7 +85,7 @@ func (ctx *Context) AdvancePlayer(playerID PlayerID, currentPosition SpaceID, di
                        ctx.Visitors.Unowned = append(ctx.Visitors.Unowned, UnownedPropertyVisitor{VisitorID: playerID, PropertyID: propID})
                }
        case TypeRailroad:
-               propID := ctx.getPropID(nextPos)
+               propID := ctx.getOwnablePropID(nextPos)
                ownerID := ctx.Properties.Owners[propID.Index()].OwnerID
                if ownerID != BankPlayerID { // property owned?
                        if ownerID != playerID && !ctx.IsMortgaged(propID) { // not by you
@@ -79,7 +95,7 @@ func (ctx *Context) AdvancePlayer(playerID PlayerID, currentPosition SpaceID, di
                        ctx.Visitors.Unowned = append(ctx.Visitors.Unowned, UnownedPropertyVisitor{VisitorID: playerID, PropertyID: propID})
                }
        case TypeUtility:
-               propID := ctx.getPropID(nextPos)
+               propID := ctx.getOwnablePropID(nextPos)
                ownerID := ctx.Properties.Owners[propID.Index()].OwnerID
                if ownerID != BankPlayerID { // property owned?
                        if ownerID != playerID && !ctx.IsMortgaged(propID) { // not by you
diff --git a/game/server.go b/game/server.go
new file mode 100644 (file)
index 0000000..f420e60
--- /dev/null
@@ -0,0 +1,43 @@
+package game
+
+import (
+       "context"
+       "github.com/coder/websocket"
+       "golang.org/x/time/rate"
+       "log"
+       "math/rand/v2"
+       "net/http"
+       "time"
+)
+
+func NewMonopolyServer() *MonopolyServer {
+       ms := &MonopolyServer{
+               subscriberMessageBuffer: 16,
+               logf:                    log.Printf,
+               subscribers:             make(map[*subscriber]string),
+               users:                   make(map[string]string),
+               publishLimiter:          rate.NewLimiter(rate.Every(time.Millisecond*100), 8),
+               gameCtx:                 nil,
+               randSeed:                rand.NewPCG(20, 26),
+       }
+       ms.serveMux.Handle("/", http.FileServer(http.Dir("public/")))
+       ms.serveMux.HandleFunc("/login", ms.loginHandler)
+       ms.serveMux.HandleFunc("/loggedin", ms.loggedInHandler)
+       ms.serveMux.HandleFunc("/subscribe", ms.subscribeHandler)
+       ms.serveMux.HandleFunc("/start", ms.startHandler)
+       ms.serveMux.HandleFunc("/roll", ms.rollHandler)
+       ms.serveMux.HandleFunc("/buy", ms.buyHandler)
+       // ms.serveMux.HandleFunc("/auction", ms.auctionHandler)
+
+       return ms
+}
+
+func (ms *MonopolyServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+       ms.serveMux.ServeHTTP(w, r)
+}
+func writeTimeout(ctx context.Context, timeout time.Duration, c *websocket.Conn, msg []byte) error {
+       ctx, cancel := context.WithTimeout(ctx, timeout)
+       defer cancel()
+
+       return c.Write(ctx, websocket.MessageText, msg)
+}
index bb30a255741dc2ae14c730458de7c2230d629e22..f679787368216c31d1c20c78c1911a1282ae8ee3 100644 (file)
--- a/game/todo
+++ b/game/todo
 
 - TODO Bankruptcy 
 
-- TODO Unowned event
+* TODO Unowned event
+** DONE buy prop
+** TODO auction
+
+* login multiple times makes new websocket
+* DONE Better errors for validators
+* TODO Response for frontend game log
+** TODO start
+** TODO roll
+** TODO buy
+
index e0c5b6d7bf7af56ab91d5d7c5e8958b80c10f362..5f7d9dd72e487769f527210e865ec383f3992673 100644 (file)
@@ -1,14 +1,17 @@
 package game
 
 import (
+       "golang.org/x/time/rate"
        "math/rand/v2"
+       "net/http"
+       "sync"
 )
 
 type Player struct {
-       UserUUID          string
-       Money             int32
-       CurrentSpaceID    SpaceID
-       GetOutOfJailCards int32
+       UserUUID       string
+       Money          int32
+       CurrentSpaceID SpaceID
+       PardonCards    int32
 }
 
 type PropertyType int
@@ -189,3 +192,27 @@ type Context struct {
        Visitors   Visitors
        Properties Properties
 }
+
+type MonopolyServer struct {
+       subscriberMessageBuffer int
+       publishLimiter          *rate.Limiter
+
+       logf func(f string, v ...any)
+
+       serveMux http.ServeMux
+
+       subscribersMu sync.Mutex
+       subscribers   map[*subscriber]string
+
+       // uuid to username
+       users map[string]string
+
+       gameCtxMu sync.Mutex
+       gameCtx   *Context
+       randSeed  *rand.PCG
+}
+
+type subscriber struct {
+       msgs      chan []byte
+       closeSlow func()
+}
diff --git a/game/validate.go b/game/validate.go
new file mode 100644 (file)
index 0000000..b174a95
--- /dev/null
@@ -0,0 +1,138 @@
+package game
+
+import (
+       "errors"
+       "net/http"
+)
+
+var (
+       ErrNotYourTurn        = errors.New("Not your turn")
+       ErrEventDoesNotPermit = errors.New("Event does not permit this action")
+       ErrNotEnoughMoney     = errors.New("Not enough money")
+       ErrNotInJail          = errors.New("Not in jail")
+       ErrNotEnoughPardons   = errors.New("Not enough pardons")
+)
+
+func ErrStatusCode(err error) int {
+       switch {
+       case errors.Is(err, ErrNotYourTurn), errors.Is(err, ErrNotInJail):
+               return http.StatusForbidden
+       case errors.Is(err, ErrEventDoesNotPermit):
+               return http.StatusConflict
+       case errors.Is(err, ErrNotEnoughMoney), errors.Is(err, ErrNotEnoughPardons):
+               return http.StatusBadRequest
+       default:
+               return http.StatusInternalServerError
+       }
+}
+
+func (ctx *Context) ValidateIsTurn(userUUID string) error {
+       if ctx.GetCurrentTurnPlayer().UserUUID != userUUID {
+               return ErrNotYourTurn
+       }
+
+       return nil
+}
+
+func (ctx *Context) ValidateCanBuy(userUUID string) error {
+       player := ctx.Players.Alive[ctx.Turn.Current.Index()]
+       prop := ColorProperties[BoardSpaces[player.CurrentSpaceID.Index()].SubIndexID]
+
+       if err := ctx.ValidateIsTurn(userUUID); err != nil {
+               return err
+       }
+
+       if ctx.EventPeek() != EventLandUnowned {
+               return ErrEventDoesNotPermit
+       }
+
+       if player.Money < prop.Price {
+               return ErrNotEnoughMoney
+       }
+
+       return nil
+}
+
+func (ctx *Context) ValidateCanRoll(userUUID string) error {
+       if err := ctx.ValidateIsTurn(userUUID); err != nil {
+               return err
+       }
+
+       if !(ctx.EventPeek() == EventRollDice || ctx.EventPeek() == EventJail) {
+               return ErrEventDoesNotPermit
+       }
+
+       return nil
+}
+
+func (ctx *Context) ValidateCanEndTurn(userUUID string) error {
+       if err := ctx.ValidateIsTurn(userUUID); err != nil {
+               return err
+       }
+
+       if ctx.EventPeek() != EventEndTurn {
+               return ErrEventDoesNotPermit
+       }
+
+       if ctx.GetCurrentTurnPlayer().Money >= 0 {
+               return ErrNotEnoughMoney
+       }
+
+       return nil
+}
+
+func (ctx *Context) ValidateCanPardonJail(userUUID string) error {
+       if err := ctx.ValidateIsTurn(userUUID); err != nil {
+               return err
+       }
+
+       if ctx.EventPeek() != EventJail {
+               return ErrEventDoesNotPermit
+       }
+
+       inJail := false
+       for _, iJV := range ctx.Visitors.InJail {
+               if ctx.Players.Alive[iJV.VisitorID.Index()].UserUUID == userUUID {
+                       inJail = true
+               }
+       }
+
+       if !inJail {
+               return ErrNotInJail
+       }
+
+       player := ctx.Players.Alive[ctx.Turn.Current.Index()]
+       if player.PardonCards == 0 {
+               return ErrNotEnoughPardons
+       }
+
+       return nil
+}
+
+func (ctx *Context) ValidateCanBuyoutJail(userUUID string) error {
+       if err := ctx.ValidateIsTurn(userUUID); err != nil {
+               return err
+       }
+
+       if ctx.EventPeek() != EventJail {
+               return ErrEventDoesNotPermit
+       }
+
+       inJail := false
+       for _, iJV := range ctx.Visitors.InJail {
+               if ctx.Players.Alive[iJV.VisitorID.Index()].UserUUID == userUUID {
+                       inJail = true
+               }
+       }
+
+       if !inJail {
+               return ErrNotInJail
+       }
+
+       player := ctx.Players.Alive[ctx.Turn.Current.Index()]
+       if player.Money < JailBuyoutCost {
+               return ErrNotEnoughMoney
+       }
+
+       return nil
+}
index 0ae8eef1d6082e0780642bc8b2fa9278d18a1a9e..bd523d033b9b293d3d63e648b4839ea31c3b9bf2 100755 (executable)
Binary files a/monopoly-web and b/monopoly-web differ
index 83c1844d6e9aa6732c1b963242061321fb727a47..c6af83936d55a7ac2566a12aa9df727786b845ea 100644 (file)
@@ -75,7 +75,7 @@
                                 body: msg,
                         })
                         if (resp.status !== 200) {
-                                throw new Error(`Unexpected HTTP Status ${resp.status} ${resp.statusText} ${resp.message}`)
+                                throw new Error(`Unexpected HTTP Status ${resp.status} ${resp.statusText}`)
                         }
 
                         dial()
                         const resp = await fetch('/start', {
                                 method: 'POST',
                         })
+
                         if (resp.status !== 202) {
-                                throw new Error(`Unexpected HTTP Status ${resp.status} ${resp.statusText} ${resp.message}`)
+                                const errMsg = await resp.text()
+                                appendGameLog(errMsg)
+                                throw new Error(`Unexpected HTTP Status ${resp.status} ${resp.statusText}`)
                         }
                 } catch (err) {
                         console.error(`Start failed: ${err.message}`)
                                 method: 'POST',
                         })
                         if (resp.status !== 200) {
-                                throw new Error(`Unexpected HTTP Status ${resp.status} ${resp.statusText} ${resp.message}`)
+                                const errMsg = await resp.text()
+                                appendGameLog(errMsg)
+                                throw new Error(`Unexpected HTTP Status ${resp.status} ${resp.statusText}`)
                         }
                 } catch (err) {
                         console.error(`Start failed: ${err.message}`)
                                 method: 'POST',
                         })
                         if (resp.status !== 200) {
-                                throw new Error(`Unexpected HTTP Status ${resp.status} ${resp.statusText} ${resp.message}`)
+                                const errMsg = await resp.text()
+                                appendGameLog(errMsg)
+                                throw new Error(`Unexpected HTTP Status ${resp.status} ${resp.statusText}`)
                         }
                 } catch (err) {
                         console.error(`Start failed: ${err.message}`)