-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathserver.go
294 lines (266 loc) · 7.66 KB
/
server.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
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
// server.go
//
// Copyright (C) 2024 Vilhjálmur Þorsteinsson / Miðeind ehf.
//
// This file implements a compact HTTP server that receives
// JSON encoded requests and returns JSON encoded responses.
package skrafl
import (
"encoding/json"
"fmt"
"net/http"
"sort"
"unicode"
)
// A class describing incoming /moves requests
type MovesRequest struct {
Locale string `json:"locale"`
BoardType string `json:"board_type"`
Board []string `json:"board"`
Rack string `json:"rack"`
Limit int `json:"limit"`
}
// A kludge to be able to marshal a Move with its score
type MoveWithScore struct {
json.Marshaler
Move Move
Score int
}
func (m *MoveWithScore) MarshalJSON() ([]byte, error) {
// Let the move marshal itself, but adding the score
return m.Move.Marshal(m.Score)
}
// The JSON response header
type HeaderJson struct {
Version string `json:"version"`
Count int `json:"count"`
Moves []MoveWithScore `json:"moves"`
}
// Map a requested locale string to a dictionary and tile set
func decodeLocale(locale string, boardType string) (*Dawg, *TileSet) {
// Obtain the first three characters of locale
locale3 := locale
if len(locale) > 3 {
locale3 = locale[0:3]
}
var dictionary string
if locale == "" || locale == "en_US" || locale == "en-US" {
// U.S. English
dictionary = "otcwl"
} else if locale == "en" || locale3 == "en_" || locale3 == "en-" {
// U.K. English (SOWPODS)
dictionary = "sowpods"
} else if locale == "is" || locale3 == "is_" || locale3 == "is-" {
// Icelandic
dictionary = "ice"
} else if locale == "pl" || locale3 == "pl_" || locale3 == "pl-" {
// Polish
dictionary = "osps"
} else if locale == "nb" || locale3 == "nb_" || locale3 == "nb-" {
// Norwegian (Bokmål)
dictionary = "nsf"
} else if locale == "nn" || locale3 == "nn_" || locale3 == "nn-" {
// Norwegian (Nynorsk)
dictionary = "nynorsk"
} else if locale == "no" || locale3 == "no_" || locale3 == "no-" {
// Generic Norwegian - we assume Bokmål
dictionary = "nsf"
} else {
// Default to U.S. English for other locales
dictionary = "otcwl"
}
var tileSet *TileSet
var dawg *Dawg
switch dictionary {
case "otcwl":
dawg = OtcwlDictionary
if boardType == "explo" {
tileSet = NewEnglishTileSet
} else {
tileSet = EnglishTileSet
}
case "sowpods":
dawg = SowpodsDictionary
if boardType == "explo" {
tileSet = NewEnglishTileSet
} else {
tileSet = EnglishTileSet
}
case "ice":
dawg = IcelandicDictionary
tileSet = NewIcelandicTileSet
case "osps":
dawg = OspsDictionary
tileSet = PolishTileSet
case "nsf":
dawg = NorwegianBokmålDictionary
tileSet = NorwegianTileSet
case "nynorsk":
dawg = NorwegianNynorskDictionary
tileSet = NorwegianTileSet
default:
// Should not happen
panic(fmt.Sprintf("Unknown dictionary: %v", dictionary))
}
return dawg, tileSet
}
// Handle an incoming /moves request
func HandleMovesRequest(w http.ResponseWriter, req MovesRequest) {
// Set the board type, dictionary and tile set
boardType := req.BoardType
if boardType != "standard" && boardType != "explo" {
msg := "Invalid board type. Must be 'standard' or 'explo'.\n"
http.Error(w, msg, http.StatusBadRequest)
return
}
// Map the request's locale to a dawg and a tile set
locale := req.Locale
dawg, tileSet := decodeLocale(locale, boardType)
rackRunes := []rune(req.Rack)
if len(rackRunes) == 0 || len(rackRunes) > RackSize {
msg := "Invalid rack.\n"
http.Error(w, msg, http.StatusBadRequest)
return
}
if len(req.Board) != BoardSize {
msg := fmt.Sprintf("Invalid board. Must be %v rows.\n", BoardSize)
http.Error(w, msg, http.StatusBadRequest)
return
}
board := NewBoard(boardType)
for r, rowString := range req.Board {
row := []rune(rowString)
if len(row) != BoardSize {
msg := fmt.Sprintf(
"Invalid board row (#%v). Must be %v characters long.\n",
r,
BoardSize,
)
http.Error(w, msg, http.StatusBadRequest)
return
}
for c, letter := range row {
if letter != '.' && letter != ' ' {
meaning := letter
score := 0
// Uppercase letters represent
// blank tiles that have been assigned a letter;
// convert these to lowercase letters and
// give them a score of 0
if unicode.IsUpper(letter) {
meaning = unicode.ToLower(letter)
letter = '?'
} else {
score = tileSet.Scores[letter]
}
if !tileSet.Contains(letter) {
msg := fmt.Sprintf("Invalid letter '%c' at %v,%v.\n", letter, r, c)
http.Error(w, msg, http.StatusBadRequest)
return
}
t := &Tile{
Letter: letter,
Meaning: meaning,
Score: score,
}
if ok := board.PlaceTile(r, c, t); !ok {
// Should not happen, and if it does, it's a serious bug,
// so no point in continuing
panic(fmt.Sprintf("Square already occupied: %v,%v", r, c))
}
}
}
}
// The board must either be empty or have a tile in the start square
if board.NumTiles > 0 && !board.HasStartTile() {
msg := "The start square must be occupied.\n"
http.Error(w, msg, http.StatusBadRequest)
return
}
// Parse the incoming rack string
rack := NewRack(rackRunes, tileSet)
if rack == nil {
msg := "Rack contains invalid letter.\n"
http.Error(w, msg, http.StatusBadRequest)
return
}
// Create a fresh GameState object, then find the valid moves
exchangeForbidden := tileSet.Size-board.NumTiles-2*RackSize < RackSize
state := NewState(
dawg,
tileSet,
board,
rack,
exchangeForbidden,
)
// Generate all valid moves and calculate their scores
moves := state.GenerateMoves()
movesWithScores := make([]MoveWithScore, len(moves))
for i, move := range moves {
movesWithScores[i] = MoveWithScore{
Move: move,
Score: move.Score(state),
}
}
// Sort the movesWithScores list in descending order by Score
sort.Slice(movesWithScores, func(i, j int) bool {
return movesWithScores[i].Score > movesWithScores[j].Score
})
// If a limit is specified, use that as a cap on the number of moves returned
if req.Limit > 0 {
movesWithScores = movesWithScores[0:min(req.Limit, len(movesWithScores))]
}
// Return the result as JSON, written to the http.ResponseWriter w
result := HeaderJson{
Version: "1.0",
Count: len(movesWithScores),
Moves: movesWithScores,
}
if err := json.NewEncoder(w).Encode(result); err != nil {
// Unable to generate valid JSON
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
// Prepare an error/false response
var OK_FALSE_RESPONSE = map[string]bool{"ok": false}
type WordCheckRequest struct {
Locale string `json:"locale"`
Word string `json:"word"`
Words []string `json:"words"`
}
type WordCheckResultPair [2]interface{}
// Handle a /wordcheck request
func HandleWordCheckRequest(w http.ResponseWriter, req WordCheckRequest) {
words := req.Words
// Sanity check the word list: we should never need to
// check more than 16 words (major-axis word plus
// up to 15 cross-axis words)
if len(words) == 0 || len(words) > BoardSize+1 {
json.NewEncoder(w).Encode(OK_FALSE_RESPONSE)
return
}
// Obtain the correct DAWG for the given locale
dawg, _ := decodeLocale(req.Locale, "explo")
// Check the words against the dictionary
allValid := true
valid := make([]WordCheckResultPair, len(words))
for i, word := range words {
wordLen := len([]rune(word))
if wordLen == 0 || wordLen > BoardSize {
// This word is empty or too long, something is wrong
json.NewEncoder(w).Encode(OK_FALSE_RESPONSE)
return
}
found := dawg.Find(word)
valid[i] = WordCheckResultPair{word, found}
if !found {
allValid = false
}
}
result := map[string]interface{}{
"word": req.Word, // Presently not used
"ok": allValid,
"valid": valid,
}
json.NewEncoder(w).Encode(result)
}