Skip to content

Commit

Permalink
code and docs cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
psnider committed Mar 29, 2022
1 parent d8744d6 commit 072f522
Show file tree
Hide file tree
Showing 46 changed files with 1,603 additions and 25,637 deletions.
109 changes: 103 additions & 6 deletions ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,29 @@

This is a web-app for a license-plate word game.
This was built with the [Aurelia](https://aurelia.io/) application framework.
_The server is under development and is not open source._

Here is a [development deployment](http://radiant-hamlet-54079.herokuapp.com/).
This server may be sleeping, in which case it will take 20 seconds to restart.

## Install and Build

You must have node.js v14+ installed.
Then use git to get a copy of this repo,
and install and build in the normal way...
```
npm install
npm run build
```

You may run the dummy-server locally so you can work with the web-app.
```
npm run start-server
```
_The actual game server is under development and is not open source._
Note that the dummy-server only provides data of the proper type for a given contect, but it doesn't actually provide answers.

## Overview

This video shows an example puzzle session:

https://user-images.githubusercontent.com/940931/160249859-00d26f0b-ba52-48e5-a1b5-80cbb583d4ec.mov
Expand All @@ -15,13 +33,13 @@ https://user-images.githubusercontent.com/940931/160249859-00d26f0b-ba52-48e5-a1
- Words must contain the original three characters, in the order in which they were given.
- Words may consist of only letters.
Punctuation is not supported yet.
For example, words such as "don't", "check-in", or "full moon" are not accepted as solutions.
For example, words such as "don't", "check-in", or "full moon" are not accepted as answers.
- Words may only be single words, and not compounds.
- Words may be up to 15 characters long.
Longer words are not supported due to UI contraints.

# Game Features
- The server only provides puzzles with known solutions.
- The server only provides puzzles with known answers.
- All puzzles are graded for difficulty.
- The server provides simple puzzles, until directed by the web-app to provide more difficult ones.
- The server is stateless.
Expand All @@ -37,7 +55,7 @@ Longer words are not supported due to UI contraints.
These messages time-out as appropriate.
- The user may request a new game, optionally specifying the characters for the puzzle.
- The user may request hints.
Each hint randomly shows the pattern for a single known solution.
Each hint randomly shows the pattern for a single known answer.
- Recent answer results and hints are displayed in the bottom portion of the license plate, just below the main text input area.
These messages time-out as appropriate.
- Answers are lised in a separate view displayed over the same license plate image.
Expand All @@ -57,6 +75,7 @@ These messages time-out as appropriate.
- The hamburger menu has inconsistent style.
- The New Game Controls should display a completion messgae for a few seconds immediately after a new game is started.
- There is no way for the user to contest a word.
- Must add coding style management, such as [TypeScript ESLint](https://typescript-eslint.io/).


# Design
Expand Down Expand Up @@ -90,7 +109,7 @@ graph TB
class external,server server
class library,http library
%% code
click editor "https://github.com/psnider/license-plate-game-aurelia/blob/main/src/LicensePlateSolutionEditorFreeEntry.ts"
click editor "https://github.com/psnider/license-plate-game-aurelia/blob/main/src/LicensePlateAnswerEditorFreeEntry.ts"
click current "https://github.com/psnider/license-plate-game-aurelia/blob/main/src/CurrentGameControls.ts"
click new "https://github.com/psnider/license-plate-game-aurelia/blob/main/src/StartNewGameControls.ts"
click answers_view "https://github.com/psnider/license-plate-game-aurelia/blob/main/src/AnswersPanel.ts"
Expand Down Expand Up @@ -126,7 +145,7 @@ graph TB
## UI Components for the above Design Diagram
- UI Elements
Note that the TypeScript is in the sibling file of the same basename.
- [Word Editor](./src/LicensePlateSolutionEditorFreeEntry.html)
- [Word Editor](./src/LicensePlateAnswerEditorFreeEntry.html)
- [Current Game Controls](./src/CurrentGameControls.html)
- [Start New Game Controls](./src/StartNewGameControls.html)
- [Answers Viewer](./src/AnswersPanel.html)
Expand Down Expand Up @@ -173,3 +192,81 @@ This is the 3rd time I tried Aurelia! Both of the previous times, my project wo
- It's easy to confuse Aurelia v1 and v2 documentation.
But this is also true of React JS and Vue.js, and probably even worse. Both have a number of complicating revisions and usage models.

# An Aurelia Primer

I have a few notes about [Aurelia](https://aurelia.io/) here, so you don't have to comb through their documentation just to get started.

Like other frameworks, Aurelia supports making complex elements for a web-app by combining HTML, CSS, and JavaScript. In this project, each component has one file for HTML, one for JavaScript, and possibly one for CSS.
The components are in the src directory.

## An Example of a Simple Aurelia Class
One of the simpler components is [InProcessIndicator](./src/InProcessIndicator.html).
It displays an animated busy indicator, used while network requests are active.

Its HTML consists of an Aurelia-specific _require_ element, which loads the CSS for the component, and a short set of HTML elements:
```html
<template>
<require from="./InProcessIndicator.css"></require>
<div id="request-in-progress" class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>
</template>
```
Each Aurelia component must have a JavaScript class with the name of the class.
For [InProcessIndicator](./src/InProcessIndicator.ts), there's no bound data or any functions, so this is a minimal Aurelia class:
```typescript
export class InProcessIndicator {}
```
Other components can use _InProcessIndicator_ by importing it using the Aurelia-specific _require_ element, and then referencing the component name using the kebab-case form of the name:
```html
<require from="./InProcessIndicator"></require>
<in-process-indicator></in-process-indicator>
```

## Binding Data and Actions to HTML

- String Interpolation
In [AnswersPanel.html](./src/AnswersPanel.html), the value of the _total_answers_score_ class variable is inserted into HTML text.
```html
<div>...for a total score of ${total_answers_score} points.</div>
```
- Data Binding
In [AnswerRow.html](./src/AnswerRow.html), the value of the _boggle_score_ property of its _current_game_ class variable is bound to the _boggle_score_ property of the contained AnswerScores component by using the Aurelia-specific _bind_ modifier. And similarly for _scrabble_score_.
```html
<answer-scores boggle_score.bind="puzzle_answer.boggle_score" scrabble_score.bind="puzzle_answer.scrabble_score" ></answer-scores>
```
And the [AnswerScores.ts](./src/AnswerScores.ts) component accepts these data items by using Aurelia-specific _@bindable_ decorators:
```typescript
export class AnswerScores {
@bindable boggle_score: number
@bindable scrabble_score: number
```
- Action Binding
In [Banner.html](./src/Banner.html), its _openAboutPanel()_ function is bound to the click event for a button.
```HTML
<button click.delegate="openAboutPanel()">About...</button>
```
- CSS binding
In [GameStatusMessagesSignboard.html](./src/GameStatusMessagesSignboard.html), the value of the _current_css_classes_ class variable is added to the CSS classes for the element, and the value of the _color_style_ class variable is added to the CSS style.
```html
<span class.bind="current_css_classes" style.bind="color_style" >${message_text_line}</span>
```
- Conditional inclusion of elements
In [AboutPanel.html](./src/AboutPanel.html), the div marked with the Aurelia-specific _if.bind_ property is only added to the component if the _about_panel_is_open_ class variable is true.
```html
<div if.bind="about_panel_is_open">
```
- Repetition of elements
In [AnswerRow.html](./src/AnswerRow.html), the contents of the div marked with the Aurelia-specific _repeat.for_ property is added once for each entry in its _notes_ class variable. Observe that the syntax of _repeat.for_'s value is:
> variable_scoped_to_HTML of variable_in_class
Here the note variable is only visible within the contents of the div.
```html
<div repeat.for="note of notes" >
<span class="score_tile note_span_tag">${note}</span>
</div>
```
- State Communication
This web-app uses the Aurelia-specific _EventAggregator_, which is a message service, similar to the Node.js EventEmitter.
This allows communication of state change across arbitrary components, which is very helpful, and which I haven't figured out how to do in either ReactJS or Vue.js.
Hopefully, these notes will help you read most of the Aurelia specific code in this web-app.
1 change: 0 additions & 1 deletion dist/app.2a33eb2b9c39d82e4716.bundle.js

This file was deleted.

1 change: 1 addition & 0 deletions dist/app.bb8cb7c14fc576035310.bundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.html
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<!doctype html><html style="height:100%;width:100%;"><head><meta charset="utf-8"><title>License Plate Game</title><meta name="viewport" content="width=device-width,initial-scale=1"><base href="/"><link rel="icon" href="favicon.ico"><script defer="defer" src="/runtime~app.450b9e5fc6deafa566fa.bundle.js"></script><script defer="defer" src="/vendors-7e9c6a9f.1adc9f385b3ac86c2788.bundle.js"></script><script defer="defer" src="/vendors-319a6989.a80424c5fae4edbd79e3.bundle.js"></script><script defer="defer" src="/vendors-4e64aa37.9e9c6bc89f49649635e3.bundle.js"></script><script defer="defer" src="/vendors-3ce50090.b281a0f6725a5f032f6a.bundle.js"></script><script defer="defer" src="/vendors-fcadf5bb.99541182c2a6c79ed0a2.bundle.js"></script><script defer="defer" src="/vendors-cdd60c62.abf102d6ed9d6bb32f06.bundle.js"></script><script defer="defer" src="/app.2a33eb2b9c39d82e4716.bundle.js"></script></head><body aurelia-app="main"></body></html>
<!doctype html><html style="height:100%;width:100%;"><head><meta charset="utf-8"><title>License Plate Game</title><meta name="viewport" content="width=device-width,initial-scale=1"><base href="/"><link rel="icon" href="favicon.ico"><script defer="defer" src="/runtime~app.450b9e5fc6deafa566fa.bundle.js"></script><script defer="defer" src="/vendors-7e9c6a9f.1adc9f385b3ac86c2788.bundle.js"></script><script defer="defer" src="/vendors-319a6989.a80424c5fae4edbd79e3.bundle.js"></script><script defer="defer" src="/vendors-4e64aa37.9e9c6bc89f49649635e3.bundle.js"></script><script defer="defer" src="/vendors-3ce50090.b281a0f6725a5f032f6a.bundle.js"></script><script defer="defer" src="/vendors-fcadf5bb.99541182c2a6c79ed0a2.bundle.js"></script><script defer="defer" src="/vendors-cdd60c62.abf102d6ed9d6bb32f06.bundle.js"></script><script defer="defer" src="/app.bb8cb7c14fc576035310.bundle.js"></script></head><body aurelia-app="main"></body></html>
93 changes: 93 additions & 0 deletions dummy-server/dummy-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import * as process from "process"
import * as express from "express"
import {LicensePlateGameAPI} from "license-plate-game-api"


const server_start_time = Date.now()


const PORT = parseInt(process.env["PORT"])
if (isNaN(PORT)) {
console.error("Must specify the environment variable PORT")
process.exit(1)
}


const predefined_new_games: LicensePlateGameAPI.NewGameResponse[] = [
{puzzle_seed: "ear", max_word_length: 15, game_id: "1", solutions_count: 5473, grade_level: 1},
{puzzle_seed: "xct", max_word_length: 15, game_id: "2", solutions_count: 173, grade_level: 5},
{puzzle_seed: "bqh", max_word_length: 15, game_id: "3", solutions_count: 762, grade_level: 7},
]

let next_new_game_index = 0
export function handleNewGame(req: express.Request, res: express.Response): void {
const i = next_new_game_index
next_new_game_index = (next_new_game_index + 1) % predefined_new_games.length
res.json(predefined_new_games[i])
}


const predefined_answer_checks: LicensePlateGameAPI.CheckAnswerResponse[] = [
{answer_text: "", boggle_score: 5, scrabble_score: 11, word_set_size: 500000, grade_level: 3, notes: []},
{answer_text: "", boggle_score: 4, scrabble_score: 13, word_set_size: 2300000, grade_level: 10, notes: []},
{answer_text: "", boggle_score: 6, scrabble_score: 25, word_set_size: 610000, grade_level: 4, notes: []},
]


export function handleCheckAnswer(req: express.Request, res: express.Response): void {
const i = Math.floor(Math.random() * predefined_answer_checks.length)
const predefined_answer_check = predefined_answer_checks[i]
const response = {...predefined_answer_check, answer_text: req.query.answer_text}
res.send(response)
}

const predefined_hints: {[puzzle_seed: string]: LicensePlateGameAPI.HintResponse} = {
"ear": {solution_pattern_text: "ear???", word_set_size: 33000},
"xct": {solution_pattern_text: "?x?ct???", word_set_size: 560000},
"bqh": {solution_pattern_text: "b?q????h", word_set_size: 1900000},
}

export function handleGetHint(req: express.Request, res: express.Response): void {
const puzzle_seed = (<string> req.query.puzzle_seed).toLocaleLowerCase()
const predefined_hint = predefined_hints[puzzle_seed]
res.send(predefined_hint)
}


export function handleFeedback(req: express.Request, res: express.Response): void {
res.sendStatus(200)
}


export function handleGetUpTime(req: express.Request, res: express.Response): void {
const uptime_seconds = Math.trunc((Date.now() - server_start_time) / 1000)
res.json({uptime_seconds});
}




function createExpressApp() {
const app = express();
app.get("/license_plate_game/new_game", handleNewGame);
app.get("/license_plate_game/check_answer", handleCheckAnswer);
app.get("/license_plate_game/hint", handleGetHint);
app.get("/license_plate_game/uptime", handleGetUpTime);
app.use(express.json());
app.post("/license_plate_game/feedback", handleFeedback);
app.get('/', function(req, res){
res.redirect('/aurelia');
});
app.get('/aurelia', function(req, res){
res.sendFile('index.html', {root: "dist"});
});
app.use(express.static("dist"))
app.listen(PORT, function() {
console.log(`dummy-server listening at http://localhost:${PORT}`)
})
return app
}


createExpressApp()

77 changes: 77 additions & 0 deletions dummy-server/generated/dummy-server.js

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

Loading

0 comments on commit 072f522

Please sign in to comment.