Skip to content
This repository was archived by the owner on Sep 18, 2022. It is now read-only.

Commit 12a0439

Browse files
committed
Add per-user high scores
1 parent a34fcaf commit 12a0439

File tree

5 files changed

+142
-28
lines changed

5 files changed

+142
-28
lines changed

README.md

+17-14
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,34 @@
22

33
The classic Snake game, right in your terminal
44

5-
## Manually downloading and installing the game
5+
## Installation
66

7-
1. Ensure `git` and `node` (`10.x.x` or later) are installed on your system
7+
Please see the [project page](https://donaldkellett.github.io/csnaketerm) for details.
8+
9+
## Installing and running the game from source
10+
11+
Note that this option does not install the corresponding `man` pages for this game.
12+
13+
### On Unix systems
14+
15+
1. Ensure `git` and `node` are installed on your system. This game is known to work with Node 10 and later so older versions of Node may or may not work.
816
1. `git clone https://github.com/DonaldKellett/csnaketerm.git`
917
1. `cd csnaketerm`
10-
1. `make` - this actually does nothing, so feel free to skip this step
1118
1. `sudo make install`
1219

1320
You should then be able to run the game by invoking `csnaketerm` in your terminal.
1421

1522
To uninstall: `cd` to the root of this repo and run `sudo make uninstall`.
1623

17-
If you are uncomfortable installing the game system-wide using `sudo`, skip the last two steps and invoke the game as `./csnaketerm` instead. Delete your clone of this repo once done.
24+
If you are uncomfortable installing the game system-wide using `sudo`, skip the last step and invoke the game as `./csnaketerm` instead. Delete your clone of this repo once done.
25+
26+
### On Windows
27+
28+
TODO
1829

19-
## Wishlist
30+
## Contributing
2031

21-
- [x] Create packages for latest stable Debian and its downstream distributions
22-
- [x] Create packages for CentOS Stream 8 ~~and CentOS Linux 7~~
23-
- [x] Create package for openSUSE
24-
- [ ] Add functionality to save per-user highscores
25-
- [x] Create packages for Arch and downstream distributions (?)
26-
- [ ] Create Nix package for NixOS (?)
27-
- [ ] Package for Windows 10 (?)
28-
- [ ] Create Homebrew formula for macOS (?)
29-
- [ ] Create a server to track all-time highscores and add functionality to upload user scores to server (opt-in) (???)
32+
Feel free to open issues and pull requests as you see fit, though the final decision on addressing which issues and accepting which pull requests is reserved for the author of this game. Of course, if there are issues or pull requests you'd like to incorporate that end up rejected by the author, you are free to fork this project and create your own variant of this game subject to the terms of the GPL (see the License section for details).
3033

3134
## License
3235

csnaketerm

+121-10
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ const readline = require('readline')
66
const util = require('util')
77
const timeout = util.promisify(setTimeout)
88
const mod = (a, b) => ((a % b) + b) % b
9+
const path = require('path')
910

10-
const VERSION = '0.1.0'
11+
const VERSION = '0.2.0'
1112
const USAGE = `Usage: csnaketerm [ -h | --help | -v | --version ]
1213
The classic Snake game, right in your terminal
1314
Invoke without options to play the game (best played on an 80x24 terminal)
@@ -116,6 +117,7 @@ const LABYRINTH = `#############################################################
116117
# # # # # # # # # # #
117118
# # # # # #
118119
############################################################################ ##`
120+
const HIGH_SCORES_FILE_FORMAT = /^((0|[1-9]\d*)(\,(0|[1-9]\d*)){11})$/
119121

120122
if (process.argv.length > 3) {
121123
console.log(USAGE)
@@ -142,20 +144,90 @@ readline.emitKeypressEvents(process.stdin)
142144
if (process.stdin.isTTY)
143145
process.stdin.setRawMode(true)
144146

147+
let highScores
148+
let highScoresFilePath
149+
150+
async function initialize() {
151+
if (typeof process.env.HOME !== 'string') {
152+
console.error('Fatal error: Expected environment variable HOME to be defined')
153+
process.exit(1)
154+
}
155+
if (!path.isAbsolute(process.env.HOME)) {
156+
console.error('Fatal error: Expected environment variable HOME to be an absolute path')
157+
process.exit(1)
158+
}
159+
highScoresFilePath = path.join(process.env.HOME, '.csnaketerm')
160+
if (!fs.existsSync(highScoresFilePath)) {
161+
highScores = {
162+
'Unconfined': {
163+
'Easy': 0,
164+
'Medium': 0,
165+
'Hard': 0,
166+
'Insane': 0
167+
},
168+
'Walled': {
169+
'Easy': 0,
170+
'Medium': 0,
171+
'Hard': 0,
172+
'Insane': 0
173+
},
174+
'Labyrinth': {
175+
'Easy': 0,
176+
'Medium': 0,
177+
'Hard': 0,
178+
'Insane': 0
179+
}
180+
}
181+
} else {
182+
try {
183+
highScores = await fs.promises.readFile(highScoresFilePath)
184+
highScores = highScores.toString().trim()
185+
if (!HIGH_SCORES_FILE_FORMAT.test(highScores))
186+
throw new Error(`Fatal error: The user data located at ${highScoresFilePath} appears to be corrupted. Deleting the file and restarting the game is the simplest solution, but note that you will lose all in-game progress.`)
187+
highScores = highScores.split`,`.map(n => +n)
188+
highScores = {
189+
'Unconfined': {
190+
'Easy': highScores[0],
191+
'Medium': highScores[1],
192+
'Hard': highScores[2],
193+
'Insane': highScores[3]
194+
},
195+
'Walled': {
196+
'Easy': highScores[4],
197+
'Medium': highScores[5],
198+
'Hard': highScores[6],
199+
'Insane': highScores[7]
200+
},
201+
'Labyrinth': {
202+
'Easy': highScores[8],
203+
'Medium': highScores[9],
204+
'Hard': highScores[10],
205+
'Insane': highScores[11]
206+
}
207+
}
208+
} catch (err) {
209+
console.error(err)
210+
process.exit(1)
211+
}
212+
}
213+
mainMenu()
214+
}
215+
145216
async function mainMenu() {
146217
console.clear()
147218
console.log(`csnaketerm, v${VERSION}`)
148219
console.log('The classic Snake game, right in your terminal')
149220
console.log('Choose an action by pressing the corresponding key:\n')
150221
console.log(' S: Start')
151222
console.log(' I: Instructions')
223+
console.log(' H: High Scores')
152224
console.log(' Q: Quit')
153225
process.stdin.resume()
154226
process.stdin.once('keypress', async (str, key) => {
155227
process.stdin.pause()
156228
if (key && key.ctrl && key.name === 'c') {
157229
console.clear()
158-
process.exit()
230+
process.exit(1)
159231
}
160232
switch (str) {
161233
case 's':
@@ -166,9 +238,19 @@ async function mainMenu() {
166238
case 'I':
167239
instructionMenu()
168240
break
241+
case 'h':
242+
case 'H':
243+
highScoresMenu()
244+
break
169245
case 'q':
170246
case 'Q':
171247
console.clear()
248+
try {
249+
await fs.promises.writeFile(highScoresFilePath, `${highScores['Unconfined']['Easy']},${highScores['Unconfined']['Medium']},${highScores['Unconfined']['Hard']},${highScores['Unconfined']['Insane']},${highScores['Walled']['Easy']},${highScores['Walled']['Medium']},${highScores['Walled']['Hard']},${highScores['Walled']['Insane']},${highScores['Labyrinth']['Easy']},${highScores['Labyrinth']['Medium']},${highScores['Labyrinth']['Hard']},${highScores['Labyrinth']['Insane']}`)
250+
} catch (err) {
251+
console.error(err)
252+
process.exit(1)
253+
}
172254
process.exit()
173255
default:
174256
console.log(INVALID_OPTION)
@@ -190,7 +272,7 @@ async function instructionMenu() {
190272
process.stdin.pause()
191273
if (key && key.ctrl && key.name === 'c') {
192274
console.clear()
193-
process.exit()
275+
process.exit(1)
194276
}
195277
mainMenu()
196278
})
@@ -208,7 +290,7 @@ async function mazeSelectionMenu() {
208290
process.stdin.pause()
209291
if (key && key.ctrl && key.name === 'c') {
210292
console.clear()
211-
process.exit()
293+
process.exit(1)
212294
}
213295
switch (str) {
214296
case 'u':
@@ -231,6 +313,27 @@ async function mazeSelectionMenu() {
231313
})
232314
}
233315

316+
async function highScoresMenu() {
317+
console.clear()
318+
console.log('High Scores')
319+
console.log('='.repeat(TERM_COLS))
320+
for (let maze in highScores) {
321+
console.log(maze)
322+
for (let difficulty in highScores[maze])
323+
console.log(` ${difficulty}: ${highScores[maze][difficulty]}`)
324+
}
325+
console.log('\nPress any key to return to the main menu')
326+
process.stdin.resume()
327+
process.stdin.once('keypress', async (str, key) => {
328+
process.stdin.pause()
329+
if (key && key.ctrl && key.name === 'c') {
330+
console.clear()
331+
process.exit(1)
332+
}
333+
mainMenu()
334+
})
335+
}
336+
234337
async function difficultySelectionMenu(maze) {
235338
console.clear()
236339
console.log('Select a difficulty by pressing the corresponding key:\n')
@@ -244,7 +347,7 @@ async function difficultySelectionMenu(maze) {
244347
process.stdin.pause()
245348
if (key && key.ctrl && key.name === 'c') {
246349
console.clear()
247-
process.exit()
350+
process.exit(1)
248351
}
249352
switch (str) {
250353
case 'e':
@@ -338,6 +441,7 @@ function isPellet(maze, cell) {
338441

339442
async function startGame(maze, difficulty) {
340443
console.clear()
444+
let mazeStr = maze
341445
switch (maze) {
342446
case 'Unconfined':
343447
maze = parseMaze(UNCONFINED)
@@ -384,7 +488,7 @@ async function startGame(maze, difficulty) {
384488
process.stdin.on('keypress', async (str, key) => {
385489
if (key && key.ctrl && key.name === 'c') {
386490
console.clear()
387-
process.exit()
491+
process.exit(1)
388492
}
389493
switch (snakeDirPrev) {
390494
case DIR_UP:
@@ -436,7 +540,7 @@ async function startGame(maze, difficulty) {
436540
if (!isPassable(maze, nextHeadPos)) {
437541
process.stdin.removeAllListeners('keypress')
438542
clearInterval(refreshInterval)
439-
gameOverScreen(score)
543+
gameOverScreen(mazeStr, difficulty, score)
440544
return
441545
}
442546
if (isPellet(maze, nextHeadPos)) {
@@ -456,22 +560,29 @@ async function startGame(maze, difficulty) {
456560
}, tickDurationMs)
457561
}
458562

459-
async function gameOverScreen(score) {
563+
async function gameOverScreen(maze, difficulty, score) {
460564
console.clear()
461565
console.log('Game Over')
462566
console.log('='.repeat(TERM_COLS))
567+
let isNewHighScore = score > highScores[maze][difficulty]
568+
if (isNewHighScore)
569+
console.log('You achieved a new high score!')
463570
console.log(`You scored: ${score}`)
571+
if (!isNewHighScore)
572+
console.log(`Your high score: ${highScores[maze][difficulty]}`)
573+
if (isNewHighScore)
574+
highScores[maze][difficulty] = score
464575

465576
console.log('\nPress any key to return to the main menu')
466577
process.stdin.resume()
467578
process.stdin.once('keypress', async (str, key) => {
468579
process.stdin.pause()
469580
if (key && key.ctrl && key.name === 'c') {
470581
console.clear()
471-
process.exit()
582+
process.exit(1)
472583
}
473584
mainMenu()
474585
})
475586
}
476587

477-
mainMenu()
588+
initialize()

csnaketerm.6

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.TH csnaketerm 6 "January 2021" "0.1.0"
1+
.TH csnaketerm 6 "January 2021" "0.2.0"
22
.SH NAME
33
csnaketerm - The classic Snake game, right in your terminal
44
.SH SYNOPSIS
@@ -21,4 +21,4 @@ Display the current version
2121
.SH NOTES
2222
This is a standalone program designed to be invoked directly, and as such, its behavior when standard input/output is redirected or piped to or from the program is unspecified.
2323
.SH SEE ALSO
24-
.I https://en.wikipedia.org/wiki/Snake_(video_game_genre)
24+
.I https://donaldkellett.github.io/csnaketerm

package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "csnaketerm",
3-
"version": "0.1.0",
3+
"version": "0.2.0",
44
"description": "The classic Snake game, right in your terminal",
55
"main": "index.js",
66
"scripts": {

0 commit comments

Comments
 (0)