npm install && npm run demo
- Live demo.
ParcelJS was considered because of zero setup. All "out of the box" build options work perfectly fine for this project.
ParcelJS compiles the code into dist/
which is .gitignore-d.
- A cell is implemented as simple
<div>
with::before
pseudo-element responsible for the ball (if any). - Cell styling is foreseen for following states:
<div class="cell" ball="...">
for a ball cell; it might have extraselected
class as well.<div class="cell" intention="...">
for an intended ball cell (the ball will be spawn at next move).<div class="cell" trace="...">
for a cell that resides on the trace from selected one to hovered one.- The value for
ball
,intention
andtrace
attributes defines the color.- All the colors are combined into a palette; see
styles/playground/_palette.scss
.
- All the colors are combined into a palette; see
- Each cell is represented as an instance of
Cell
class; seescripts/cell.ts
.- The instance of
Cell
class is tied to its HTML<div>
. - It contains the state data (ball color, intention color, trace color) as well.
- Updating state parameters updates the HTML. Think "MVVM" pattern.
- The instance of
- All the cells are stored in the (instance of)
Playground
class. - The runtime data (like score, history etc.) are stored in the instance of the
Runtime
class.
So in order to understand what's currently on we need all cells and the runtime instance. This pair of parameters is often passed to multiple functions.
Both (instances of) Playground
and Runtime
classes are used by the Gameplay
class.
- It uses the FSM for game states and transitions between them. See
scripts/gameplay/fsm/
. It contains text schema and the FSM implementation. - There are following concepts used:
- Opearations mutate the state/runtime in "atomic" (often undoable) manner.
- Actions are sequences of operations wrapped into some logic. They are FSM entities (see below).
- UI handlers invoke some FSM "entry points". They are the way to run the flow again after automaton hit the stable (final) state.
Mouse click handlers are perfect examples here.
- The gameplay data (score + cell setup) is stored on each move in the local storage. Reloading the page automatically loads last game.
The FSM is realized as map of states which have unique name and are functions per se.
Once runned they might return
- Another "state" or its name. This means the state is transitional and automaton continues the flow.
- Nothing /
void
. This means that the sate is stable (final) and automaton stops here.
The FSM states might be invoked from the outer world (e.g., by gameplay.init()
or by mouse click). FSM runs from invoked state to the stable (final) one (unless looped).
For current purposes there was no need to implement asynchronous FSM flow. However it's good practice in general because it's pretty easy to hit RangeError: "Maximum call size exceeded"
with cyclic transitions.
In order to find shortest path from selected cell to hovered one the modified Dijkstra's algorithm is used. See scripts/gameplay/trace-utils.ts
.
- Instead of assigning the minimal tentative distance (sic) to each node the shortest path is assigned. The principle remains (path length is semantically equal to the "distance" as each edge has weight of 1).
- The "neighbors" are pre-sorted by distance to destination cell (it might look expensive but we perform that operation only when necessary and for maximum four cells).
- After any path to destination cell has been found the altorithm won't continue with paths which are already of same length or longer (because they cannot deliver shorter solution).
Thosee optimizations made finding the shortest path pretty fast. Better observed on non-mobile devices (mouse needed).
npm run prod
generates production-ready code in the dist/
. It contains the index.html
, the JS and CSS files (both minified). Being served from local folder, the index.html
should work in browser.
Credits: Roman Melnyk, https://melnyk.site