schachnovelle 1.0.0-beta-6 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ and this project (tries to) adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+ <!-- ### Added
9
+ ### Changed
10
+ ### Removed -->
11
+
12
+ ## [1.0.1] - 2020-12-16
13
+ ### Added
14
+ - Changelog
15
+
16
+ ## [1.0.0] - 2020-12-16
17
+ ### Added
18
+ - Promotion to different pieces
19
+
20
+ ### Changed
21
+ - Clear the console instead of adding to the end of the line
22
+
23
+ ### Removed
package/README.md CHANGED
@@ -23,10 +23,14 @@ or npm
23
23
  ...And play!
24
24
 
25
25
  ## Features
26
- Currently only supports basic moves and captures. There is no check detection yet. Play with your head! For now...
26
+ There is no check detection yet. Play with your head! For now...
27
27
 
28
- You can play against yourself or a friend on the same keyboard. I'm working on more functionality and learning as I go along.
28
+ You can play against yourself or a friend on the same keyboard.
29
29
 
30
- Long term, I want to connect this to the [lichess](https://lichess.org/) API, so you can play others from the comfort of the command line!
30
+ ## Roadmap
31
31
 
32
- _[Touch you later](http://touchystudios.com/)_
32
+ I'm slowly working on connecting this to the [lichess](https://lichess.org/) API, so you can play others from the comfort of the command line!
33
+
34
+ ## Changelog
35
+
36
+ _[Changelog](CHANGELOG.md)_
package/index.js CHANGED
@@ -1,42 +1,4 @@
1
1
  #!/usr/bin/env node
2
- const { spawn, exec } = require('child_process');
3
-
4
2
  const ui = require('./ui')
5
3
 
6
- ui.init()
7
- // const server = spawn('node', ['server'])
8
- // const ui = spawn('node', ['ui'])
9
-
10
- // server.stdout.on('data', (data) => {
11
- // console.log(`server: ${data}`);
12
- // });
13
-
14
- // server.stderr.on('data', (data) => {
15
- // console.error(`server error: ${data}`);
16
- // });
17
-
18
- // server.on('exit', (code, signal) => {
19
- // console.log(`child process exited with code ${code} and signal ${signal}`);
20
- // })
21
-
22
- // server.on('close', (code) => {
23
- // console.log(`child process exited with code ${code}`);
24
- // });
25
-
26
- // ui.stdout.on('data', (data) => {
27
- // console.log(data);
28
- // });
29
-
30
- // ui.stderr.on('data', (data) => {
31
- // console.error(`ui error: ${data}`);
32
- // });
33
-
34
- // ui.on('exit', (code, signal) => {
35
- // console.log(`ui process exited with code ${code} and signal ${signal}`);
36
- // })
37
-
38
- // ui.on('close', (code) => {
39
- // console.log(`ui exited with code ${code}`);
40
- // });
41
-
42
-
4
+ ui.init()
package/package.json CHANGED
@@ -1,18 +1,34 @@
1
1
  {
2
2
  "name": "schachnovelle",
3
- "version": "1.0.0-beta-6",
3
+ "version": "1.1.0",
4
4
  "description": "Chess to play on the command line",
5
5
  "main": "index.js",
6
+ "scripts": {
7
+ "play": "node index.js",
8
+ "serve": "node server/index.js"
9
+ },
6
10
  "bin": {
7
- "schachnovelle": "index.js"
11
+ "schachnovelle": "index.js",
12
+ "schachnovelle.server": "server/index.js"
13
+ },
14
+ "engines": {
15
+ "node": ">=14.15.0"
8
16
  },
17
+ "engineStrict": true,
9
18
  "author": "Manus <manusnijhoff@gmail.com>",
10
19
  "license": "MIT",
11
20
  "dependencies": {
21
+ "axios": "^0.20.0",
12
22
  "chalk": "^4.0.0",
13
23
  "dotenv": "^8.2.0",
14
24
  "enquirer": "^2.3.5",
15
25
  "express": "^4.17.1",
16
- "node-fetch": "^2.6.0"
26
+ "express-session": "^1.17.1",
27
+ "memorystore": "^1.6.4",
28
+ "ndjson": "^2.0.0",
29
+ "node-fetch": "^2.6.0",
30
+ "open": "^7.3.0",
31
+ "pug": "^3.0.0",
32
+ "simple-oauth2": "^4.1.0"
17
33
  }
18
34
  }
package/server/api.js ADDED
@@ -0,0 +1,132 @@
1
+ const axios = require('axios')
2
+ const https = require('https')
3
+ const ndjson = require('ndjson')
4
+
5
+ module.exports = {
6
+
7
+ /**
8
+ * get the user's information
9
+ * @param {*} token
10
+ */
11
+ getUserInfo (token) {
12
+ return new Promise((resolve, reject) => {
13
+ axios.get('/api/account', {
14
+ baseURL: 'https://lichess.org/',
15
+ headers: { 'Authorization': 'Bearer ' + token.access_token }
16
+ })
17
+ .then((response) => {
18
+ resolve(response)
19
+ })
20
+ .catch((error) => {
21
+ reject(error)
22
+ })
23
+ })
24
+ },
25
+
26
+ /**
27
+ * Create an event stream
28
+ * @param {*} token
29
+ */
30
+ createEventStream (token) {
31
+ const options = {
32
+ headers: {
33
+ 'Authorization': 'Bearer ' + token.access_token
34
+ }
35
+ }
36
+
37
+ return new Promise((resolve, reject) => {
38
+ const req = https.get('https://lichess.org/api/stream/event', options)
39
+
40
+ req
41
+ .on('response', (stream) => {
42
+ const { statusCode } = stream
43
+
44
+ if (statusCode >= 200 && statusCode < 300) {
45
+ resolve(stream)
46
+ }
47
+ })
48
+ .on('error', (error) => {
49
+ reject(error)
50
+ })
51
+ })
52
+ },
53
+
54
+ createGameStream (token, gameId) {
55
+ const options = {
56
+ headers: {
57
+ 'Authorization': 'Bearer ' + token.access_token
58
+ }
59
+ }
60
+
61
+ return new Promise((resolve, reject) => {
62
+ const req = https.get(`https://lichess.org/api/board/game/stream/${gameId}`, options)
63
+
64
+ req
65
+ .on('response', (stream) => {
66
+ const { statusCode } = stream
67
+
68
+ if (statusCode >= 200 && statusCode < 300) {
69
+ resolve(stream)
70
+ }
71
+ })
72
+ .on('error', (error) => {
73
+ reject(error)
74
+ })
75
+ })
76
+ },
77
+
78
+ // Commence play after the user has been identified
79
+ startPlay (req, res) {
80
+ return new Promise(async (resolve, reject) => {
81
+ try {
82
+ const stream = await this.createEventStream(req.session.accessToken)
83
+ stream
84
+ .pipe(ndjson.parse())
85
+ .on('data', (data) => {
86
+ // Always write the data type to the stream
87
+ process.stdout.write(data.type)
88
+
89
+ // Hook into events from event stream!
90
+ if (data.type === 'gameStart') {
91
+ req.session.currentGame = data.game
92
+ this.openGameStream(req)
93
+ } else {
94
+ console.log('!!! unhandled data type !!!')
95
+ console.log(data)
96
+ console.log('!!! end unhandled data type !!!')
97
+ }
98
+ })
99
+ } catch (error) {
100
+ reject(error)
101
+ } finally {
102
+ resolve({ status: 200 })
103
+ }
104
+ })
105
+ },
106
+
107
+ // Tryout to follow a game
108
+ async openGameStream (req) {
109
+ try {
110
+ const stream = await this.createGameStream(req.session.accessToken, req.session.currentGame.id)
111
+
112
+ stream
113
+ .pipe(ndjson.parse())
114
+ .on('data', (data) => {
115
+ // Write data type to stdout
116
+ process.stdout.write(data.type)
117
+
118
+ if (data.type === 'gameFull') {
119
+ req.session.currentGame.data = data
120
+ } else if (data.type === 'gameState') {
121
+ req.session.currentGame.data.gameState = data
122
+ } else {
123
+ console.log('UNUSED DATA TYPE', data.type)
124
+ }
125
+ })
126
+ } catch (error) {
127
+ console.error(error)
128
+ }
129
+ }
130
+
131
+
132
+ }
package/server/index.js CHANGED
@@ -1,19 +1,444 @@
1
1
  require('dotenv').config()
2
2
 
3
+ //
4
+ //
5
+ // Imports
3
6
  const express = require('express')
4
- const app = express()
5
- const port = process.env.SN_PORT
7
+ // Storage
8
+ const open = require('open')
9
+ const session = require('express-session')
10
+ const MemoryStore = require('memorystore')(session)
11
+ // Lichess
12
+ const lichess = require('./api')
6
13
 
7
- app.use(express.json())
14
+ /* Create your lichess OAuth app on https://lichess.org/account/oauth/app/create
15
+ * Homepage URL: http://localhost:3000
16
+ * Callback URL: http://localhost:3000/callback
17
+ */
8
18
 
9
- app.post('/move', (req, res) => {
10
- const response = 'move is ' + req.body.move
11
- console.log(response)
12
- res.json({
13
- message: response
19
+
20
+
21
+ //
22
+ //
23
+ // Server + Auth config
24
+ const port = 3000
25
+ const host = process.env.NODE_ENV === 'production' ? 'https://schachnovelle.online' : 'http://localhost'
26
+ const clientId = process.env.LICHESS_ID
27
+ const clientSecret = process.env.LICHESS_SECRET
28
+ const redirectUri = `http://localhost:${port}/callback`
29
+
30
+ // Scopes
31
+ const scopes = [
32
+ 'preference:read',
33
+ 'challenge:read',
34
+ 'board:play',
35
+ 'bot:play'
36
+ ];
37
+
38
+
39
+
40
+ //
41
+ //
42
+ // Lichess
43
+ const tokenHost = 'https://oauth.lichess.org/';
44
+ const tokenPath = '/oauth';
45
+ const authorizePath = '/oauth/authorize';
46
+
47
+ const oAuthConfig = {
48
+ client: {
49
+ id: clientId,
50
+ secret: clientSecret
51
+ },
52
+ auth: {
53
+ tokenHost,
54
+ tokenPath,
55
+ authorizePath
56
+ }
57
+ }
58
+
59
+ const { AuthorizationCode } = require('simple-oauth2')
60
+ const client = new AuthorizationCode(oAuthConfig)
61
+
62
+
63
+
64
+ //
65
+ //
66
+ // Initialize the server
67
+ const app = express();
68
+
69
+
70
+
71
+ //
72
+ //
73
+ // Set things
74
+ app.set('view engine', 'pug')
75
+
76
+
77
+
78
+ //
79
+ //
80
+ // Session
81
+ app.use(session({
82
+ cookie: { maxAge: 86400000 },
83
+ store: new MemoryStore({
84
+ checkPeriod: 86400000 // prune expired entries every 24h
85
+ }),
86
+ resave: false,
87
+ secret: 'keyboard cat',
88
+ saveUninitialized: true
89
+ }))
90
+
91
+
92
+
93
+ //
94
+ //
95
+ // Show the "log in with lichess" button
96
+ app.get('/', (req, res) => {
97
+ if (req.session.userInfo) {
98
+ res.redirect('/welcome')
99
+ } else {
100
+ res.render('../views/index')
101
+ }
102
+ })
103
+
104
+
105
+
106
+ //
107
+ //
108
+ // Initial page redirecting to Lichess
109
+ app.get('/auth', (req, res) => {
110
+ const state = Math.random().toString(36).substring(2)
111
+
112
+ const authorizationUri = client.authorizeURL({
113
+ redirect_uri: redirectUri,
114
+ scope: scopes.join(' '),
115
+ state
14
116
  })
117
+
118
+ res.redirect(authorizationUri)
119
+ })
120
+
121
+
122
+ //
123
+ //
124
+ // callback page
125
+ app.get('/callback', async (req, res) => {
126
+ const tokenParams = {
127
+ code: req.query.code,
128
+ redirect_uri: redirectUri,
129
+ scope: scopes.join(' ')
130
+ }
131
+
132
+ try {
133
+ const result = await client.getToken(tokenParams)
134
+ const accessToken = client.createToken(result)
135
+
136
+ // Save token to the session
137
+ req.session.accessToken = accessToken.token.token
138
+
139
+ res.redirect('/welcome')
140
+ } catch (error) {
141
+ console.log('Access Token Error', error.message)
142
+ }
143
+ })
144
+
145
+
146
+
147
+ //
148
+ //
149
+ // Welcome page
150
+ app.get('/welcome', async (req, res) => {
151
+ if (req.session.accessToken) {
152
+ try {
153
+ const userInfo = await lichess.getUserInfo(req.session.accessToken)
154
+ req.session.userInfo = userInfo.data
155
+ } catch (error) {
156
+ console.error(error)
157
+ } finally {
158
+ if (req.session.userInfo) {
159
+ lichess.startPlay(req, res)
160
+ .then((response) => {
161
+ if (response.status === 200) {
162
+ // This goes to the UI
163
+ process.stdout.write('streamReady')
164
+ // This to the browser
165
+ res.render('../views/welcome', { user: req.session.userInfo })
166
+ } else {
167
+ res.render('../views/error', response)
168
+ }
169
+ })
170
+ .catch((error) => {
171
+ console.error(error)
172
+ res.render('../views/error', error)
173
+ })
174
+ } else {
175
+ res.render('../views/error')
176
+ }
177
+ }
178
+
179
+ } else {
180
+ res.redirect('/')
181
+ }
15
182
  })
16
183
 
17
- app.listen(port, () => {
18
- console.log('listening at http://localhost:', port)
19
- })
184
+ app.listen(port, async () => {
185
+ await open(`${host}:${port}`)
186
+ });
187
+
188
+
189
+
190
+ // SAMPLE DATA
191
+
192
+ // { type: 'gameStart', game: { id: 'Wmv3QOtD' } }
193
+ // {
194
+ // id: 'Wmv3QOtD',
195
+ // variant: { key: 'standard', name: 'Standard', short: 'Std' },
196
+ // clock: { initial: 600000, increment: 3000 },
197
+ // speed: 'rapid',
198
+ // perf: { name: 'Rapid' },
199
+ // rated: false,
200
+ // createdAt: 1612916752568,
201
+ // white: { id: 'manegame', name: 'manegame', title: null, rating: 1155 },
202
+ // black: {
203
+ // id: 'schachnovelle-online',
204
+ // name: 'schachnovelle-online',
205
+ // title: null,
206
+ // rating: 1500,
207
+ // provisional: true
208
+ // },
209
+ // initialFen: 'startpos',
210
+ // type: 'gameFull',
211
+ // state: {
212
+ // type: 'gameState',
213
+ // moves: '',
214
+ // wtime: 600000,
215
+ // btime: 600000,
216
+ // winc: 3000,
217
+ // binc: 3000,
218
+ // wdraw: false,
219
+ // bdraw: false,
220
+ // status: 'started'
221
+ // }
222
+ // }
223
+ // {
224
+ // type: 'gameState',
225
+ // moves: 'e2e4',
226
+ // wtime: 600000,
227
+ // btime: 600000,
228
+ // winc: 3000,
229
+ // binc: 3000,
230
+ // wdraw: false,
231
+ // bdraw: false,
232
+ // status: 'started'
233
+ // }
234
+ // {
235
+ // type: 'gameState',
236
+ // moves: 'e2e4 e7e5',
237
+ // wtime: 600000,
238
+ // btime: 600000,
239
+ // winc: 3000,
240
+ // binc: 3000,
241
+ // wdraw: false,
242
+ // bdraw: false,
243
+ // status: 'started'
244
+ // }
245
+ // {
246
+ // type: 'gameState',
247
+ // moves: 'e2e4 e7e5 g1f3',
248
+ // wtime: 599590,
249
+ // btime: 600000,
250
+ // winc: 3000,
251
+ // binc: 3000,
252
+ // wdraw: false,
253
+ // bdraw: false,
254
+ // status: 'started'
255
+ // }
256
+ // {
257
+ // type: 'gameState',
258
+ // moves: 'e2e4 e7e5 g1f3 c7c6',
259
+ // wtime: 599590,
260
+ // btime: 597200,
261
+ // winc: 3000,
262
+ // binc: 3000,
263
+ // wdraw: false,
264
+ // bdraw: false,
265
+ // status: 'started'
266
+ // }
267
+ // {
268
+ // type: 'gameState',
269
+ // moves: 'e2e4 e7e5 g1f3 c7c6 f1c4',
270
+ // wtime: 598580,
271
+ // btime: 597200,
272
+ // winc: 3000,
273
+ // binc: 3000,
274
+ // wdraw: false,
275
+ // bdraw: false,
276
+ // status: 'started'
277
+ // }
278
+ // {
279
+ // type: 'gameState',
280
+ // moves: 'e2e4 e7e5 g1f3 c7c6 f1c4 d7d5',
281
+ // wtime: 598580,
282
+ // btime: 595560,
283
+ // winc: 3000,
284
+ // binc: 3000,
285
+ // wdraw: false,
286
+ // bdraw: false,
287
+ // status: 'started'
288
+ // }
289
+ // {
290
+ // type: 'gameState',
291
+ // moves: 'e2e4 e7e5 g1f3 c7c6 f1c4 d7d5 c4d5',
292
+ // wtime: 592540,
293
+ // btime: 595560,
294
+ // winc: 3000,
295
+ // binc: 3000,
296
+ // wdraw: false,
297
+ // bdraw: false,
298
+ // status: 'started'
299
+ // }
300
+ // {
301
+ // type: 'gameState',
302
+ // moves: 'e2e4 e7e5 g1f3 c7c6 f1c4 d7d5 c4d5 c6d5',
303
+ // wtime: 592540,
304
+ // btime: 595370,
305
+ // winc: 3000,
306
+ // binc: 3000,
307
+ // wdraw: false,
308
+ // bdraw: false,
309
+ // status: 'started'
310
+ // }
311
+ // {
312
+ // type: 'gameState',
313
+ // moves: 'e2e4 e7e5 g1f3 c7c6 f1c4 d7d5 c4d5 c6d5 f3e5',
314
+ // wtime: 591460,
315
+ // btime: 595370,
316
+ // winc: 3000,
317
+ // binc: 3000,
318
+ // wdraw: false,
319
+ // bdraw: false,
320
+ // status: 'started'
321
+ // }
322
+ // {
323
+ // type: 'gameState',
324
+ // moves: 'e2e4 e7e5 g1f3 c7c6 f1c4 d7d5 c4d5 c6d5 f3e5 d5e4',
325
+ // wtime: 591460,
326
+ // btime: 594720,
327
+ // winc: 3000,
328
+ // binc: 3000,
329
+ // wdraw: false,
330
+ // bdraw: false,
331
+ // status: 'started'
332
+ // }
333
+ // {
334
+ // type: 'gameState',
335
+ // moves: 'e2e4 e7e5 g1f3 c7c6 f1c4 d7d5 c4d5 c6d5 f3e5 d5e4 g2g3',
336
+ // wtime: 578720,
337
+ // btime: 594720,
338
+ // winc: 3000,
339
+ // binc: 3000,
340
+ // wdraw: false,
341
+ // bdraw: false,
342
+ // status: 'started'
343
+ // }
344
+ // {
345
+ // type: 'gameState',
346
+ // moves: 'e2e4 e7e5 g1f3 c7c6 f1c4 d7d5 c4d5 c6d5 f3e5 d5e4 g2g3 d8d2',
347
+ // wtime: 578720,
348
+ // btime: 597720,
349
+ // winc: 3000,
350
+ // binc: 3000,
351
+ // wdraw: false,
352
+ // bdraw: false,
353
+ // status: 'started'
354
+ // }
355
+ // {
356
+ // type: 'gameState',
357
+ // moves: 'e2e4 e7e5 g1f3 c7c6 f1c4 d7d5 c4d5 c6d5 f3e5 d5e4 g2g3 d8d2 e1f1',
358
+ // wtime: 565650,
359
+ // btime: 597720,
360
+ // winc: 3000,
361
+ // binc: 3000,
362
+ // wdraw: false,
363
+ // bdraw: false,
364
+ // status: 'started'
365
+ // }
366
+ // {
367
+ // type: 'gameState',
368
+ // moves: 'e2e4 e7e5 g1f3 c7c6 f1c4 d7d5 c4d5 c6d5 f3e5 d5e4 g2g3 d8d2 e1f1 e4e3',
369
+ // wtime: 565650,
370
+ // btime: 595360,
371
+ // winc: 3000,
372
+ // binc: 3000,
373
+ // wdraw: false,
374
+ // bdraw: false,
375
+ // status: 'started'
376
+ // }
377
+ // {
378
+ // type: 'gameState',
379
+ // moves: 'e2e4 e7e5 g1f3 c7c6 f1c4 d7d5 c4d5 c6d5 f3e5 d5e4 g2g3 d8d2 e1f1 e4e3 c2c3',
380
+ // wtime: 556810,
381
+ // btime: 595360,
382
+ // winc: 3000,
383
+ // binc: 3000,
384
+ // wdraw: false,
385
+ // bdraw: false,
386
+ // status: 'started'
387
+ // }
388
+ // {
389
+ // type: 'gameState',
390
+ // moves: 'e2e4 e7e5 g1f3 c7c6 f1c4 d7d5 c4d5 c6d5 f3e5 d5e4 g2g3 d8d2 e1f1 e4e3 c2c3 c8h3',
391
+ // wtime: 556810,
392
+ // btime: 587450,
393
+ // winc: 3000,
394
+ // binc: 3000,
395
+ // wdraw: false,
396
+ // bdraw: false,
397
+ // status: 'started'
398
+ // }
399
+ // {
400
+ // type: 'gameState',
401
+ // moves: 'e2e4 e7e5 g1f3 c7c6 f1c4 d7d5 c4d5 c6d5 f3e5 d5e4 g2g3 d8d2 e1f1 e4e3 c2c3 c8h3 f1g1',
402
+ // wtime: 547270,
403
+ // btime: 587450,
404
+ // winc: 3000,
405
+ // binc: 3000,
406
+ // wdraw: false,
407
+ // bdraw: false,
408
+ // status: 'started'
409
+ // }
410
+ // {
411
+ // type: 'gameState',
412
+ // moves: 'e2e4 e7e5 g1f3 c7c6 f1c4 d7d5 c4d5 c6d5 f3e5 d5e4 g2g3 d8d2 e1f1 e4e3 c2c3 c8h3 f1g1 h3g2',
413
+ // wtime: 547270,
414
+ // btime: 581970,
415
+ // winc: 3000,
416
+ // binc: 3000,
417
+ // wdraw: false,
418
+ // bdraw: false,
419
+ // status: 'started'
420
+ // }
421
+ // {
422
+ // type: 'gameState',
423
+ // moves: 'e2e4 e7e5 g1f3 c7c6 f1c4 d7d5 c4d5 c6d5 f3e5 d5e4 g2g3 d8d2 e1f1 e4e3 c2c3 c8h3 f1g1 h3g2 b2b3',
424
+ // wtime: 543850,
425
+ // btime: 581970,
426
+ // winc: 3000,
427
+ // binc: 3000,
428
+ // wdraw: false,
429
+ // bdraw: false,
430
+ // status: 'started'
431
+ // }
432
+ // { type: 'gameFinish', game: { id: 'Wmv3QOtD' } }
433
+ // {
434
+ // type: 'gameState',
435
+ // moves: 'e2e4 e7e5 g1f3 c7c6 f1c4 d7d5 c4d5 c6d5 f3e5 d5e4 g2g3 d8d2 e1f1 e4e3 c2c3 c8h3 f1g1 h3g2 b2b3 d2f2',
436
+ // wtime: 543850,
437
+ // btime: 574250,
438
+ // winc: 3000,
439
+ // binc: 3000,
440
+ // wdraw: false,
441
+ // bdraw: false,
442
+ // status: 'mate',
443
+ // winner: 'black'
444
+ // }