forked from FujiNetWIFI/servers
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.go
266 lines (208 loc) · 6.39 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
package main
import (
"fmt"
"log"
"os"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
)
// A sync.Map is used to save the state at the end of a request without needing synchronization
// If a request errors out in the middle, it will not save the state, avoiding an invalid,
// partially updated state.
// A mutex is used so a given table can only be accessed by a single request at a time
var stateMap sync.Map
var tables []GameTable = []GameTable{}
var tableMutex KeyedMutex
type KeyedMutex struct {
mutexes sync.Map // Zero value is empty and ready for use
}
func (m *KeyedMutex) Lock(key string) func() {
key = strings.ToLower(key)
value, _ := m.mutexes.LoadOrStore(key, &sync.Mutex{})
mtx := value.(*sync.Mutex)
mtx.Lock()
return func() { mtx.Unlock() }
}
func main() {
log.Print("Starting server...")
// Set environment flags
UpdateLobby = os.Getenv("GO_PROD") == "1"
if UpdateLobby {
log.Printf("This instance will update the lobby at " + LOBBY_ENDPOINT_UPSERT)
gin.SetMode(gin.ReleaseMode)
}
// Determine port for HTTP service.
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
log.Printf("Listing on port %s", port)
router := gin.Default()
router.GET("/view", apiView)
router.GET("/state", apiState)
router.POST("/state", apiState)
router.GET("/move/:move", apiMove)
router.POST("/move/:move", apiMove)
router.GET("/leave", apiLeave)
router.POST("/leave", apiLeave)
router.GET("/tables", apiTables)
router.GET("/updateLobby", apiUpdateLobby)
// router.GET("/REFRESHLOBBY", apiRefresh)
initializeGameServer()
initializeTables()
router.Run(":" + port)
}
// Api Request steps
// 1. Get state
// 2. Game Logic
// 3. Save state
// 4. Return client centric state
// request pattern
// 1. get state (locks the state)
// A. Start a function that updates table state
// B. Defer unlocking the state until the current "state updating" function is complete
// C. If state is not nil, perform logic
// 2. Serialize and return results
// Executes a move for the client player, if that player is currently active
func apiMove(c *gin.Context) {
state, unlock := getState(c)
func() {
defer unlock()
if state != nil {
// Access check - only move if the client is the active player
if state.clientPlayer == state.ActivePlayer {
move := strings.ToUpper(c.Param("move"))
state.performMove(move)
saveState(state)
state = state.createClientState()
}
}
}()
serializeResults(c, state)
}
// Steps forward and returns the updated state
func apiState(c *gin.Context) {
hash := c.Query("hash")
state, unlock := getState(c)
func() {
defer unlock()
if state != nil {
if state.clientPlayer >= 0 {
state.runGameLogic()
saveState(state)
}
state = state.createClientState()
}
}()
// Check if passed in hash matches the state
if state != nil && len(hash) > 0 && hash == state.hash {
serializeResults(c, "1")
return
}
serializeResults(c, state)
}
// Drop from the specified table
func apiLeave(c *gin.Context) {
state, unlock := getState(c)
func() {
defer unlock()
if state != nil {
if state.clientPlayer >= 0 {
state.clientLeave()
state.updateLobby()
saveState(state)
}
}
}()
serializeResults(c, "bye")
}
// Returns a view of the current state without causing it to change. For debugging side-by-side with a client
func apiView(c *gin.Context) {
state, unlock := getState(c)
func() {
defer unlock()
if state != nil {
state = state.createClientState()
}
}()
serializeResults(c, state)
}
// Returns a list of real tables with player/slots for the client
// If passing "dev=1", will return developer testing tables instead of the live tables
func apiTables(c *gin.Context) {
returnDevTables := c.Query("dev") == "1"
tableOutput := []GameTable{}
for _, table := range tables {
value, ok := stateMap.Load(table.Table)
if ok {
state := value.(*GameState)
if (returnDevTables && !state.registerLobby) || (!returnDevTables && state.registerLobby) {
humanPlayerSlots, humanPlayerCount := state.getHumanPlayerCountInfo()
table.CurPlayers = humanPlayerCount
table.MaxPlayers = humanPlayerSlots
tableOutput = append(tableOutput, table)
}
}
}
serializeResults(c, tableOutput)
}
// Forces an update of all tables to the lobby - useful for adhoc use if the Lobby restarts or loses info
func apiUpdateLobby(c *gin.Context) {
for _, table := range tables {
value, ok := stateMap.Load(table.Table)
if ok {
state := value.(*GameState)
state.updateLobby()
}
}
serializeResults(c, "Lobby Updated")
}
// Gets the current game state for the specified table and adds the player id of the client to it
func getState(c *gin.Context) (*GameState, func()) {
table := c.Query("table")
if table == "" {
table = "default"
}
table = strings.ToLower(table)
player := c.Query("player")
// Lock by the table so to avoid multiple threads updating the same table state
unlock := tableMutex.Lock(table)
// Load state
value, ok := stateMap.Load(table)
var state *GameState
if ok {
stateCopy := *value.(*GameState)
state = &stateCopy
state.setClientPlayerByName(player)
}
return state, unlock
}
func saveState(state *GameState) {
stateMap.Store(state.table, state)
}
func initializeTables() {
// Create the real servers (hard coded for now)
createTable("The Basement", "basement", 0, true)
createTable("The Den", "den", 0, true)
createTable("AI Room - 2 bots", "ai2", 2, true)
createTable("AI Room - 4 bots", "ai4", 4, true)
createTable("AI Room - 6 bots", "ai6", 6, true)
// For client developers, create hidden tables for each # of bots (for ease of testing with a specific # of players in the game)
// These will not update the lobby
for i := 1; i < 8; i++ {
createTable(fmt.Sprintf("Dev Room - %d bots", i), fmt.Sprintf("dev%d", i), i, false)
}
}
func createTable(serverName string, table string, botCount int, registerLobby bool) {
state := createGameState(botCount, registerLobby)
state.table = table
state.serverName = serverName
saveState(state)
state.updateLobby()
tables = append([]GameTable{{Table: table, Name: serverName}}, tables...)
if UpdateLobby {
time.Sleep(time.Millisecond * time.Duration(100))
}
}