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

Commit

Permalink
Add per-user high scores
Browse files Browse the repository at this point in the history
  • Loading branch information
DonaldKellett committed Jan 14, 2021
1 parent a34fcaf commit 12a0439
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 28 deletions.
31 changes: 17 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,34 @@

The classic Snake game, right in your terminal

## Manually downloading and installing the game
## Installation

1. Ensure `git` and `node` (`10.x.x` or later) are installed on your system
Please see the [project page](https://donaldkellett.github.io/csnaketerm) for details.

## Installing and running the game from source

Note that this option does not install the corresponding `man` pages for this game.

### On Unix systems

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.
1. `git clone https://github.com/DonaldKellett/csnaketerm.git`
1. `cd csnaketerm`
1. `make` - this actually does nothing, so feel free to skip this step
1. `sudo make install`

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

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

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.
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.

### On Windows

TODO

## Wishlist
## Contributing

- [x] Create packages for latest stable Debian and its downstream distributions
- [x] Create packages for CentOS Stream 8 ~~and CentOS Linux 7~~
- [x] Create package for openSUSE
- [ ] Add functionality to save per-user highscores
- [x] Create packages for Arch and downstream distributions (?)
- [ ] Create Nix package for NixOS (?)
- [ ] Package for Windows 10 (?)
- [ ] Create Homebrew formula for macOS (?)
- [ ] Create a server to track all-time highscores and add functionality to upload user scores to server (opt-in) (???)
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).

## License

Expand Down
131 changes: 121 additions & 10 deletions csnaketerm
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ const readline = require('readline')
const util = require('util')
const timeout = util.promisify(setTimeout)
const mod = (a, b) => ((a % b) + b) % b
const path = require('path')

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

if (process.argv.length > 3) {
console.log(USAGE)
Expand All @@ -142,20 +144,90 @@ readline.emitKeypressEvents(process.stdin)
if (process.stdin.isTTY)
process.stdin.setRawMode(true)

let highScores
let highScoresFilePath

async function initialize() {
if (typeof process.env.HOME !== 'string') {
console.error('Fatal error: Expected environment variable HOME to be defined')
process.exit(1)
}
if (!path.isAbsolute(process.env.HOME)) {
console.error('Fatal error: Expected environment variable HOME to be an absolute path')
process.exit(1)
}
highScoresFilePath = path.join(process.env.HOME, '.csnaketerm')
if (!fs.existsSync(highScoresFilePath)) {
highScores = {
'Unconfined': {
'Easy': 0,
'Medium': 0,
'Hard': 0,
'Insane': 0
},
'Walled': {
'Easy': 0,
'Medium': 0,
'Hard': 0,
'Insane': 0
},
'Labyrinth': {
'Easy': 0,
'Medium': 0,
'Hard': 0,
'Insane': 0
}
}
} else {
try {
highScores = await fs.promises.readFile(highScoresFilePath)
highScores = highScores.toString().trim()
if (!HIGH_SCORES_FILE_FORMAT.test(highScores))
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.`)
highScores = highScores.split`,`.map(n => +n)
highScores = {
'Unconfined': {
'Easy': highScores[0],
'Medium': highScores[1],
'Hard': highScores[2],
'Insane': highScores[3]
},
'Walled': {
'Easy': highScores[4],
'Medium': highScores[5],
'Hard': highScores[6],
'Insane': highScores[7]
},
'Labyrinth': {
'Easy': highScores[8],
'Medium': highScores[9],
'Hard': highScores[10],
'Insane': highScores[11]
}
}
} catch (err) {
console.error(err)
process.exit(1)
}
}
mainMenu()
}

async function mainMenu() {
console.clear()
console.log(`csnaketerm, v${VERSION}`)
console.log('The classic Snake game, right in your terminal')
console.log('Choose an action by pressing the corresponding key:\n')
console.log(' S: Start')
console.log(' I: Instructions')
console.log(' H: High Scores')
console.log(' Q: Quit')
process.stdin.resume()
process.stdin.once('keypress', async (str, key) => {
process.stdin.pause()
if (key && key.ctrl && key.name === 'c') {
console.clear()
process.exit()
process.exit(1)
}
switch (str) {
case 's':
Expand All @@ -166,9 +238,19 @@ async function mainMenu() {
case 'I':
instructionMenu()
break
case 'h':
case 'H':
highScoresMenu()
break
case 'q':
case 'Q':
console.clear()
try {
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']}`)
} catch (err) {
console.error(err)
process.exit(1)
}
process.exit()
default:
console.log(INVALID_OPTION)
Expand All @@ -190,7 +272,7 @@ async function instructionMenu() {
process.stdin.pause()
if (key && key.ctrl && key.name === 'c') {
console.clear()
process.exit()
process.exit(1)
}
mainMenu()
})
Expand All @@ -208,7 +290,7 @@ async function mazeSelectionMenu() {
process.stdin.pause()
if (key && key.ctrl && key.name === 'c') {
console.clear()
process.exit()
process.exit(1)
}
switch (str) {
case 'u':
Expand All @@ -231,6 +313,27 @@ async function mazeSelectionMenu() {
})
}

async function highScoresMenu() {
console.clear()
console.log('High Scores')
console.log('='.repeat(TERM_COLS))
for (let maze in highScores) {
console.log(maze)
for (let difficulty in highScores[maze])
console.log(` ${difficulty}: ${highScores[maze][difficulty]}`)
}
console.log('\nPress any key to return to the main menu')
process.stdin.resume()
process.stdin.once('keypress', async (str, key) => {
process.stdin.pause()
if (key && key.ctrl && key.name === 'c') {
console.clear()
process.exit(1)
}
mainMenu()
})
}

async function difficultySelectionMenu(maze) {
console.clear()
console.log('Select a difficulty by pressing the corresponding key:\n')
Expand All @@ -244,7 +347,7 @@ async function difficultySelectionMenu(maze) {
process.stdin.pause()
if (key && key.ctrl && key.name === 'c') {
console.clear()
process.exit()
process.exit(1)
}
switch (str) {
case 'e':
Expand Down Expand Up @@ -338,6 +441,7 @@ function isPellet(maze, cell) {

async function startGame(maze, difficulty) {
console.clear()
let mazeStr = maze
switch (maze) {
case 'Unconfined':
maze = parseMaze(UNCONFINED)
Expand Down Expand Up @@ -384,7 +488,7 @@ async function startGame(maze, difficulty) {
process.stdin.on('keypress', async (str, key) => {
if (key && key.ctrl && key.name === 'c') {
console.clear()
process.exit()
process.exit(1)
}
switch (snakeDirPrev) {
case DIR_UP:
Expand Down Expand Up @@ -436,7 +540,7 @@ async function startGame(maze, difficulty) {
if (!isPassable(maze, nextHeadPos)) {
process.stdin.removeAllListeners('keypress')
clearInterval(refreshInterval)
gameOverScreen(score)
gameOverScreen(mazeStr, difficulty, score)
return
}
if (isPellet(maze, nextHeadPos)) {
Expand All @@ -456,22 +560,29 @@ async function startGame(maze, difficulty) {
}, tickDurationMs)
}

async function gameOverScreen(score) {
async function gameOverScreen(maze, difficulty, score) {
console.clear()
console.log('Game Over')
console.log('='.repeat(TERM_COLS))
let isNewHighScore = score > highScores[maze][difficulty]
if (isNewHighScore)
console.log('You achieved a new high score!')
console.log(`You scored: ${score}`)
if (!isNewHighScore)
console.log(`Your high score: ${highScores[maze][difficulty]}`)
if (isNewHighScore)
highScores[maze][difficulty] = score

console.log('\nPress any key to return to the main menu')
process.stdin.resume()
process.stdin.once('keypress', async (str, key) => {
process.stdin.pause()
if (key && key.ctrl && key.name === 'c') {
console.clear()
process.exit()
process.exit(1)
}
mainMenu()
})
}

mainMenu()
initialize()
4 changes: 2 additions & 2 deletions csnaketerm.6
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.TH csnaketerm 6 "January 2021" "0.1.0"
.TH csnaketerm 6 "January 2021" "0.2.0"
.SH NAME
csnaketerm - The classic Snake game, right in your terminal
.SH SYNOPSIS
Expand All @@ -21,4 +21,4 @@ Display the current version
.SH NOTES
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.
.SH SEE ALSO
.I https://en.wikipedia.org/wiki/Snake_(video_game_genre)
.I https://donaldkellett.github.io/csnaketerm
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "csnaketerm",
"version": "0.1.0",
"version": "0.2.0",
"description": "The classic Snake game, right in your terminal",
"main": "index.js",
"scripts": {
Expand Down

0 comments on commit 12a0439

Please sign in to comment.