-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathapp.ts
244 lines (217 loc) · 11.9 KB
/
app.ts
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
import {autoinject} from 'aurelia-framework';
import {EventAggregator} from 'aurelia-event-aggregator';
import {Settings, ExpiringMessage, PuzzleAnswer} from "./lib/index.d"
import {MAX_EXPIRATION_SECONDS} from "./lib/lib"
import {AuMsgWindowResized, AuMsgAboutPanelState, AuMsgFeedbackPanelState, AuMsgRemoteCallState, AuMsgNewGameRequest, AuMsgCheckAnswer, AuMsgHintRequest} from './messages';
import {LicensePlatePuzzle} from "./lib/license-plate-puzzle"
import {LicensePlateGameClient} from "license-plate-game-api"
import type {LicensePlateGameAPI} from "license-plate-game-api"
function minutesToMilliseconds(minutes: number) {
return minutes * 60 * 1000
}
// The display periods for the types of ExpiringMessage.remote_request_status.
const EXPIRATION_SECONDS = {
REQUEST: 30,
OK: 5,
ERROR: MAX_EXPIRATION_SECONDS, // errors must be cleared explicitly
}
// The main component of the license plate game.
// This contains most of the game state.
@autoinject
export class App {
settings: Settings
in_process_count: number
about_panel_is_open: boolean
feedback_panel_is_open: boolean
current_game: LicensePlatePuzzle | undefined
elapsed_seconds: number
remote_request_id: number
puzzle_answers: PuzzleAnswer[]
hint: LicensePlateGameAPI.HintResponse | undefined
constructor(private ea: EventAggregator) {
this.settings = {check_answer_on_enter_key: true}
this.in_process_count = 0
this.remote_request_id = 0
this.elapsed_seconds = 0
this.about_panel_is_open = false
this.feedback_panel_is_open = false
ea.subscribe(AuMsgAboutPanelState, (msg: AuMsgAboutPanelState) => {
this.about_panel_is_open = msg.is_open
})
ea.subscribe(AuMsgFeedbackPanelState, (msg: AuMsgFeedbackPanelState) => {
this.feedback_panel_is_open = msg.is_open
})
ea.subscribe(AuMsgNewGameRequest, (msg: AuMsgNewGameRequest) => {
this.userRequestedStartNewGame(msg.request)
})
ea.subscribe(AuMsgRemoteCallState, (msg: AuMsgRemoteCallState) => {
if (msg.message.remote_request_status === "request") {
this.in_process_count++
} else {
this.in_process_count--
}
})
ea.subscribe(AuMsgCheckAnswer, (msg: AuMsgCheckAnswer) => {
this.userRequestedCheckAnswer(msg.callback)
})
ea.subscribe(AuMsgHintRequest, (msg: AuMsgHintRequest) => {
this.userRequestedHint(msg.callback)
})
this.userRequestedStartNewGame({})
this.keepAlive()
window.onresize = () => {
const size = {width: window.innerWidth, height: window.innerHeight}
this.ea.publish(new AuMsgWindowResized(size))
}
}
// Request the uptime of the server every 15 minutes.
keepAlive() {
setTimeout(() => {
LicensePlateGameClient.requestUpTime()
this.keepAlive()
}, minutesToMilliseconds(15))
}
// Add a system message for a request to the server.
initiateRemoteRequest(message: ExpiringMessage) : void {
this.ea.publish(new AuMsgRemoteCallState(message))
}
// Add a system message for a response from the server.
completedRemoteRequest(message: ExpiringMessage) : void {
this.ea.publish(new AuMsgRemoteCallState(message))
}
// Update the local elapsed_seconds variable when the time is updated.
notifyElapsedTimeUpdated(puzzle: LicensePlatePuzzle) {
if (this.current_game) {
this.current_game.elapsed_seconds = puzzle.elapsed_seconds
this.elapsed_seconds = puzzle.elapsed_seconds
}
}
// Request a new game, from the server.
// Request either a random game (the default), or a game specified by the user.
// @param request
// If a game is already in progress, then the game_id, elapsed_seconds, and previous_puzzle_grade_level fields are populated by this function from that game.
userRequestedStartNewGame(request: LicensePlateGameAPI.NewGameRequest) {
const getNextGradeLevel = (current_game: LicensePlatePuzzle) => {
const average_grade_of_answers = this.estimateGradeLevelOfAnswers()
if (average_grade_of_answers != null) {
return average_grade_of_answers
} else {
const reduced_grade = Math.max(this.current_game.grade_level - 1, 0)
return reduced_grade
}
}
if (this.current_game) {
request.game_id = this.current_game.game_id
request.elapsed_seconds = this.elapsed_seconds
request.previous_puzzle_grade_level = getNextGradeLevel(this.current_game)
this.current_game.stop()
this.current_game = undefined
}
this.elapsed_seconds = 0
this.puzzle_answers = []
this.hint = undefined
const requested_text = request.user_selected_puzzle
const promise = LicensePlateGameClient.requestNewGame(request)
const remote_request_id = `new-game-${this.remote_request_id++}`
this.initiateRemoteRequest({text: "requesting new game", message_type: "new-game-remote-request", remote_request_status: "request", remote_request_id, expiration_secs: EXPIRATION_SECONDS.REQUEST})
this.feedback_panel_is_open = false
promise.then((new_game_response) => {
if (new_game_response.solutions_count > 0) {
new_game_response.puzzle_seed = new_game_response.puzzle_seed.toLocaleUpperCase()
request.completion_callback?.(null, new_game_response)
this.completedRemoteRequest({remote_request_id, text: `starting new game with: ${new_game_response.puzzle_seed}`, message_type: "new-game-remote-request", remote_request_status: "ok", expiration_secs: EXPIRATION_SECONDS.OK})
const new_game = new LicensePlatePuzzle(new_game_response, this.notifyElapsedTimeUpdated.bind(this))
this.current_game = new_game
this.current_game.elapsed_seconds = 0
} else {
const text = `There are no answers for: ${new_game_response.puzzle_seed}`
request.completion_callback?.(text)
this.completedRemoteRequest({remote_request_id, text, message_type: "new-game-remote-request", remote_request_status: "error", expiration_secs: EXPIRATION_SECONDS.ERROR})
}
return undefined
}, (error) => {
let error_text = error.message || error.statusText || "unknown failure"
request.completion_callback?.(error_text)
this.completedRemoteRequest({remote_request_id, text: `new game for: "${requested_text}" failed: ${error_text}`, message_type: "new-game-remote-request", remote_request_status: "error", expiration_secs: EXPIRATION_SECONDS.ERROR})
})
}
// true if the current_game.answer_text has not already been submitted as an answer.
currentWordIsANewAnswer() {
const answer_text_uppercase = this.current_game.answer_text.toLocaleUpperCase()
const found = this.puzzle_answers.find((puzzle_answer) => {return puzzle_answer.answer_text === answer_text_uppercase})
return !found
}
// Request a check of an answer, from the server.
userRequestedCheckAnswer(completion_callback?: LicensePlateGameAPI.ClientCompletionCallback) {
if (this.current_game) {
if (this.currentWordIsANewAnswer()) {
this.current_game.answer_text = this.current_game.answer_text
const {game_id, puzzle_seed, elapsed_seconds, answer_text} = this.current_game
const request: LicensePlateGameAPI.CheckAnswerRequest = {game_id, puzzle_seed, elapsed_seconds, answer_text}
const promise = LicensePlateGameClient.requestCheckAnswer(request)
const remote_request_id = `check-answer-${this.remote_request_id++}`
this.initiateRemoteRequest({remote_request_id, text: "requesting answer check", message_type: "check-answer-remote-request", remote_request_status: "request", expiration_secs: EXPIRATION_SECONDS.REQUEST})
promise.then((graded_answer) => {
completion_callback?.(null, graded_answer)
this.completedRemoteRequest({remote_request_id, text: `received answer check for: ${answer_text}`, message_type: "check-answer-remote-request", remote_request_status: "ok", expiration_secs: EXPIRATION_SECONDS.OK})
const puzzle_answer = <PuzzleAnswer><unknown> graded_answer
puzzle_answer.attempt_number = this.puzzle_answers.length + 1
puzzle_answer.answer_text = puzzle_answer.answer_text.toLocaleUpperCase()
this.puzzle_answers.push(puzzle_answer)
return undefined
}, (error) => {
let error_text = error.message || error.statusText || "unknown failure"
request.completion_callback?.(error_text)
this.completedRemoteRequest({remote_request_id, text: `check answer for: ${answer_text} failed: ${error_text}`, message_type: "check-answer-remote-request", remote_request_status: "error", expiration_secs: EXPIRATION_SECONDS.ERROR})
return undefined
})
} else {
completion_callback?.(`You have already submitted "${this.current_game.answer_text}" as an answer.`)
}
} else {
completion_callback?.("There is no active game...")
}
}
// Request a hint for a single solution, from the server.
userRequestedHint(completion_callback?: LicensePlateGameAPI.ClientCompletionCallback) {
if (this.current_game) {
const {game_id, puzzle_seed, elapsed_seconds} = this.current_game
const request: LicensePlateGameAPI.HintRequest = {game_id, puzzle_seed, elapsed_seconds}
const promise = LicensePlateGameClient.requestHint(request)
const remote_request_id = `get-hint-${this.remote_request_id++}`
this.initiateRemoteRequest({remote_request_id, text: "requesting hint", message_type: "hint-remote-request", remote_request_status: "request", expiration_secs: EXPIRATION_SECONDS.REQUEST})
promise.then((hint) => {
completion_callback?.(null, hint)
this.completedRemoteRequest({remote_request_id, text: `received hint for: ${puzzle_seed}`, message_type: "hint-remote-request", remote_request_status: "ok", expiration_secs: EXPIRATION_SECONDS.OK})
this.hint = hint
return undefined
}, (error) => {
let error_text = error.message || error.statusText || "unknown failure"
completion_callback?.(error_text)
this.completedRemoteRequest({remote_request_id, text: `hint for: ${puzzle_seed} failed: ${error_text}`, message_type: "hint-remote-request", remote_request_status: "error", expiration_secs: EXPIRATION_SECONDS.ERROR})
return undefined
})
}
}
// Estimate the grade level of the puzzle answers submitted so far.
estimateGradeLevelOfAnswers() {
const count = this.puzzle_answers.length
let wrong_answer_count = 0
let summed_grades = 0
if (count) {
this.puzzle_answers.forEach((puzzle_answer) => {
if (puzzle_answer.grade_level != null) {
summed_grades += puzzle_answer.grade_level
} else {
wrong_answer_count++
}
})
const average_grade_before_penalties = Math.trunc(summed_grades / count)
summed_grades -= (wrong_answer_count * average_grade_before_penalties)
const adjusted_average_grade = Math.trunc(summed_grades / count)
return Math.max(adjusted_average_grade, 0)
} else {
return undefined
}
}
}