Merge pull request #290 from FullScreenShenanigans/examples-fullscreensaver

Added FullScreenSaver as code and finished implementing dist outputs
This commit is contained in:
Josh Goldberg
2022-08-15 07:32:46 -04:00
committed by GitHub
136 changed files with 13301 additions and 4775 deletions
+1 -1
View File
@@ -3,5 +3,5 @@
**/*.json
**/dist
**/node_modules
**/webpack.config.js
**/webpack.config.*
packages/shenanigans-manager/setup/**/*
+1 -1
View File
@@ -16,7 +16,7 @@
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": ["./packages/*/tsconfig.json"]
"project": ["./examples/*/tsconfig.json", "./packages/*/tsconfig.json"]
},
"plugins": ["@typescript-eslint", "simple-import-sort"],
"rules": {
+1 -1
View File
@@ -55,7 +55,7 @@ a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at me@joshuakgoldberg.com. All
reported by contacting the project team at git@joshuakgoldberg.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
+3
View File
@@ -11,3 +11,6 @@ runs:
cache: "yarn"
- run: yarn install --frozen-lockfile
shell: bash
# Occasionally, GitHub actions seems to persist old files from previous runs
- run: yarn clean
shell: bash
+16
View File
@@ -0,0 +1,16 @@
name: Dist
on:
push:
branches:
- main
pull_request:
jobs:
compile:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/prepare
- run: yarn hydrate
- run: yarn dist
+3 -2
View File
@@ -4,6 +4,7 @@
*.map
*.tsbuildinfo
node_modules/
packages/*/lib/
packages/*/test/
*/*/dist/
*/*/lib/
*/*/test/
!packages/shenanigans-manager/setup/**/*
+8 -2
View File
@@ -1,6 +1,6 @@
{
"dictionaries": ["typescript"],
"ignorePaths": ["lib", "node_modules", "**/*.d.ts", "**/*.js", "**/*.tsbuildinfo"],
"dictionaries": ["css", "html", "softwareTerms", "typescript"],
"ignorePaths": ["dist", "lib", "node_modules", "**/*.d.ts", "**/*.js", "**/*.tsbuildinfo"],
"words": [
"actorhittr",
"areaspawnr",
@@ -28,11 +28,13 @@
"flagswappr",
"fpsanalyzr",
"frametickr",
"fullscreensaver",
"fullscreenshenanigans",
"Gameboy",
"gameplay",
"genrand",
"Gitter",
"Goombas",
"Grapher",
"Graphr",
"groupholdr",
@@ -64,6 +66,8 @@
"pixelrendr",
"Playr",
"quadskeepr",
"redeclared",
"redeclaring",
"Rendr",
"sceneplayr",
"Screenr",
@@ -80,7 +84,9 @@
"TTFI",
"unspawn",
"unspawned",
"unspawns",
"userwrappr",
"walkthrough",
"worldseedr",
"Wrappr",
"Writr",
+1 -1
View File
@@ -58,7 +58,7 @@ Once built, there will be up to three directories in this package containing cod
1. `src/`: The "source of truth" containing TypeScript source files.
2. `lib/`: JavaScript source files, declaration files, and source maps generated by TypeScript from `src/`.
3. `dist/`: Bundled, minified versions of JavaScript files generated by WebPack from `lib/` -- if `package.json` contains a `"dist": true` under `"shenanigans"`.
3. `dist/`: Bundled, minified versions of JavaScript files generated by [Webpack](https://webpack.js.org) from `lib/` -- if `package.json` contains a `"dist": true` under `"shenanigans"`.
### `shenanigans-manager`
@@ -88,6 +88,18 @@ Now, after a player dies, you'll be able to check the best high score for the ga
FSS.itemsHolder.getItem("highScore");
```
Now that we have both the `"highScore"` and `"score"` data working, redeclare an `itemsHolder` property on the `FullScreenSaver` class with an explicit type parameter:
```ts
/**
* Cache-based wrapper around localStorage.
*/
public readonly itemsHolder: ItemsHoldr<{
highScore: number;
score: number;
}>;
```
Great.
Those browser console look ups aren't good enough for our game though.
Next up, we'll create a text visualization of current points and high scores.
@@ -19,7 +19,20 @@ Go ahead and add that group to `src/sections/Groups.ts` last so text gets drawn
public readonly groupNames = ["Players", "Squares", "Text"];
```
Add a corresponding `Text` definition as a child of `Actor` in the `src/sections/Objects.ts` `inheritance`, along with character names such as `CharA` and `Menu` under it:
Add the `Text` name to the `groupHolder` property redeclared on the `FullScreenSaver` class:
```ts
/**
* General storage abstraction for keyed containers of items.
*/
public readonly groupHolder: GroupHoldr<{
Squares: Actor;
Players: Actor;
Text: Actor;
}>;
```
Additionally, add a corresponding `Text` definition as a child of `Actor` in the `src/sections/Objects.ts` `inheritance`, along with character names such as `CharA` and `Menu` under it:
```ts
public readonly inheritance: ClassInheritance = {
@@ -242,4 +255,9 @@ public start(player: Actor) {
```
Glorious: text appears as soon as the player is created, and updates for their new scores.
We're done here!
The game seems pretty complete for a demo at this point.
The last step for us to take will be to add tests.
Fun!
> 👉 [12. Tests](./12.%20Tests.md)
+165
View File
@@ -0,0 +1,165 @@
# Tests
EightBittr projects managed by `shenanigans-manager` come with infrastructure for setting up tests.
The `yarn run test` command starts with a `yarn run test:setup` that creates a `test/` directory.
Each package receives a `test/index.html` file to run tests:
- Open `test/index.html` in a browser to run tests in that browser
- `yarn run test` includes a `yarn run test:run` command to run tests in "headless" mode
Tests are written in [Mocha](https://github.com/mochajs/mocha) and [Chai](https://github.com/chaijs/chai).
Their files are written using alongside source files under `src/` and named `*.test.ts?`.
## Defining Tests
Most tests retrieve actors from the game in some way and run assertions on them.
The `groupHolder` property of game instances can be used to retrieve actors and the defined groups of them.
Let's add a test to verify that the game starts with exactly one square.
Create a new `FullScreenSaver.test.ts` file as a sibling to `FullScreenSaver.ts`.
Add a `describe` block with an `it` test:
```ts
import { expect } from "chai";
import { createFullScreenSaver } from "./fakes.test";
import { FullScreenSaver } from "./FullScreenSaver";
describe("FullScreenSaver", () => {
it("starts with one square", () => {
// Arrange
const game = new FullScreenSaver({
height: 320,
width: 480,
});
// Assert
expect(game.groupHolder.getGroup("Squares")).to.have.length(1);
});
});
```
Then, run `yarn test` in your terminal.
You should see output in the terminal including a test success:
```plaintext
FullScreenSaver
✅ starts with one square
```
Open `test/index.html` in a browser.
You should also see a Mocha UI indicating tests passing there too.
Perfect.
Let's add some more tests!
## Simulating Inputs
Testing the result of user interactions is typically done by simulating `game.inputWriter` events.
Let's add a test that pressing a key down spawns a player:
```ts
it("adds a player when a directional input is pressed", () => {
// Arrange
const game = new FullScreenSaver({
height: 320,
width: 480,
});
// Act
game.inputWriter.callEvent("onkeydown", "left");
// Assert
expect(game.groupHolder.getGroup("Players")).to.have.length(1);
});
```
## Fakes
Most game logic that gets tested involves some actions that happen over time.
We'll need our tests to be able to programmatically "tick" the game clock.
EightBittr projects typically use [Sinon's fake timers](https://sinonjs.org/releases/latest/fake-timers) to do so.
Create a new `fakes.test.ts` file alongside the other test file.
In this file, we're going to add a `createFullScreenSaver` function to create a new `FullScreenSaver` instance with test-specific `components.frameTicker` settings:
```ts
import { useFakeTimers } from "sinon-timers-repeatable";
import { FullScreenSaver } from "./FullScreenSaver";
export const createFullScreenSaver = () => {
const clock = useFakeTimers();
const game = new FullScreenSaver({
components: {
frameTicker: {
timing: {
cancelFrame: clock.clearTimeout,
getTimestamp: () => clock.now,
requestFrame: (callback) =>
clock.setTimeout(() => {
callback(clock.now);
}, 1),
},
},
},
height: 320,
width: 480,
});
game.frameTicker.play();
return { clock, game };
};
```
You can replace existing `const game` with calls to the `createFullScreenSaver` function:
```ts
const { game } = createFullScreenSaver();
```
Now that our games under test use mocked timing, we can tick forward the clock manually in tests.
We can tick the clock forward by a number of frames by using `game.frameTicker.getInterval()` to get how many clock ticks it take to move the game forward one maintenance cycle.
We can also call `game.physics` to move actors around.
This is useful if we want to, say, move the player out of the way of solids to check what happens when they survive for long enough to create a square
Add a test for the case of the player surviving 150 ticks to the `FullScreenSaver.test.ts` file, and one test for the case of the player not surviving:
```ts
it("increases to two squares 150 ticks after a player is added", () => {
// Arrange
const { clock, game } = createFullScreenSaver();
game.inputWriter.callEvent("onkeydown", "left");
// (move the player out of the way)
game.physics.shiftActors(game.groupHolder.getGroup("Players"), 9001, 9001);
// Act
clock.tick(150 * game.frameTicker.getInterval());
// Assert
expect(game.groupHolder.getGroup("Squares")).to.have.length(2);
});
it("stops adding squares after the player is hit", () => {
// Arrange
const { clock, game } = createFullScreenSaver();
game.inputWriter.callEvent("onkeydown", "left");
// Act
clock.tick(300 * game.frameTicker.getInterval());
// Assert
expect(game.groupHolder.getGroup("Players")).to.have.length(0);
expect(game.groupHolder.getGroup("Squares")).to.have.length(1);
});
```
At this point, the game is not only pretty complete for a demo, it's also stable and well-tested.
Awesome stuff!
Give yourself a pat on the back.
You've completed the EightBittr walkthrough! 🏆
@@ -20,7 +20,10 @@ import { ClassInheritance, ClassProperties } from "objectmakr";
import { FullScreenSaver } from "../FullScreenSaver";
export class Objects<Game extends EightBittr> extends ObjectsBase<Game> {
/**
* Raw ObjectMakr factory settings.
*/
export class Objects<Game extends FullScreenSaver> extends ObjectsBase<Game> {
public readonly inheritance: ClassInheritance = {
Actor: {
Square: {},
@@ -30,14 +33,14 @@ export class Objects<Game extends EightBittr> extends ObjectsBase<Game> {
public readonly onMake = "onMake";
public readonly properties: ClassProperties = {
Actor: {
onMake: this.game.actors.process.bind(this.game.actors),
},
Square: {
height: 64,
groupType: "Squares",
width: 64,
},
Actor: {
onMake: this.game.actors.process.bind(this.game.actors),
},
};
}
```
@@ -51,6 +54,9 @@ import { EightBittr, EightBittrConstructorSettings } from "eightbittr";
import { Objects } from "./sections/Objects";
export class FullScreenSaver extends EightBittr {
/**
* Raw ObjectMakr factory settings.
*/
@member(Objects)
public readonly objects: Objects<this>;
@@ -88,6 +94,18 @@ Add this new section as a member to your `FullScreenSaver` class too:
public readonly groups: Groups<this>;
```
Additionally, you'll want to tell the `FullScreenSaver` class that its `groupHolder` property stores an array of `Actor`s under the group name `"Squares"`.
Do so by importing `GroupHoldr` from `groupholdr` modules and redeclaring the `groupHolder` property on `FullScreenSaver` with an explicit type argument:
```ts
/**
* General storage abstraction for keyed containers of items.
*/
public readonly groupHolder: GroupHoldr<{
Squares: Actor;
}>
```
Now that the `Square` _exists_, we'll want to give it some sprite info to be _rendered_.
## Graphics
@@ -15,7 +15,7 @@ public constructor(settings: EightBittrConstructorSettings) {
const square = this.actors.add("Square");
this.physics.setMid(square, this.mapScreener.height / 2, this.mapScreener.width / 2);
this.physics.setMid(square, this.mapScreener.width / 2, this.mapScreener.height / 2);
}
```
@@ -40,6 +40,7 @@ export class Maintenance<Game extends FullScreenSaver> extends MaintenanceBase<G
*/
public readonly maintain = (actor: Actor) => {
this.game.physics.shiftBoth(actor, actor.xVelocity, actor.yVelocity);
return undefined;
};
public readonly maintainers: [string, ActorMaintainer][] = [["Squares", this.maintain]];
@@ -61,7 +62,7 @@ Now, when you refresh the game, the starting square should slowly fly up and to
## Wall Bouncing
We can check on each square's maintenance tick whether it's passed any of the screen walls and reverse its velocity if so.
Add the following code to the `maintain` function to do so:
Add the following code to the `maintain` function just after the `shiftBoth` to do so:
```ts
if (actor.top < 0) {
@@ -71,7 +72,7 @@ if (actor.top < 0) {
if (actor.bottom > this.game.mapScreener.height) {
actor.yVelocity *= -1;
this.game.physics.shiftVert(actor, this.game.mapScreener.height - actor.bottom);
this.game.physics.shiftVertical(actor, this.game.mapScreener.height - actor.bottom);
}
if (actor.left < 0) {
@@ -81,7 +82,7 @@ if (actor.left < 0) {
if (actor.right > this.game.mapScreener.width) {
actor.xVelocity *= -1;
this.game.physics.shiftHoriz(actor, this.game.mapScreener.width - actor.right);
this.game.physics.shiftHorizontal(actor, this.game.mapScreener.width - actor.right);
}
```
@@ -7,24 +7,26 @@ We'll create that player when the left key is pressed for the first time.
Creating the player Actor definition follows similar steps to the squares:
1. Create a new object declaration in `Objects`:
1. Create a new object declaration in `Objects` for a `Player` actor type:
```ts
public readonly inheritance: ClassInheritance = {
Actor: {
Player: {},
Square: {},
// ...
},
};
```
```ts
public readonly properties: ClassProperties = {
// ...
Player: {
height: 32,
groupType: "Players",
width: 32,
},
// ...
```
2. Create a group for the players in `Groups`:
@@ -33,7 +35,16 @@ Creating the player Actor definition follows similar steps to the squares:
public readonly groupNames = ["Players", "Squares"];
```
3. Define a new color (light green) and sprite data (a square) for the new type in `Graphics`:
3. Add to the `groupHolder: GroupHoldr` type argument in the `FullScreenSaver` class:
```ts
public readonly groupHolder: GroupHoldr<{
Squares: Actor;
Players: Actor;
}>;
```
4. Define a new color (light green) and sprite data (a square) for the new type in `Graphics`:
```ts
public readonly library = {
@@ -102,6 +113,16 @@ export class Players extends Section<FullScreenSaver> {
That `id: "player1"` will allow the new player Actor to be retrieved by id later on.
The above code creates a player Actor on top of the first square Actor in the game with that id and some horizontal velocity.
Add the new `Players` section as a property of `FullScreenSaver`:
```ts
/**
* Creates and updates player Actors.
*/
@member(Players)
public readonly players: Players;
```
Lastly, add an entry to the `Maintenance` section for players to be maintained as well:
```ts
@@ -24,7 +24,7 @@ export class Quadrants<Game extends FullScreenSaver> extends QuadrantsBase<Game>
/**
* Groups that should have their quadrants updated.
*/
public readonly activeGroupNames: string[] = ["Players", "Squares"];
public readonly activeGroupNames = ["Players", "Squares"];
}
```
@@ -57,7 +57,7 @@ export class Collisions<Game extends FullScreenSaver> extends CollisionsBase<Gam
/**
* Names of groups that should be checked for collisions.
*/
public readonly collidingGroupNames: string[] = ["Players"];
public readonly collidingGroupNames = ["Players"];
}
```
@@ -93,7 +93,7 @@ The game will now know when a player is touching a solid, but it won't know what
## Collision Handling
Add an equivalent `hitCallbackGenerators` later in the class to kill the player:
Add an equivalent `hitCallbackGenerators` later in the `Collisions` class to kill the player:
```ts
/**
@@ -10,6 +10,10 @@ We'll do this by adding a function called whenever a square is added to the game
Create a new standalone `Squares` section at `src/sections/Squares.ts` with an `addSquare` member function that takes in a coordinates and velocity to create a new square:
```ts
import { Section } from "eightbittr";
import { FullScreenSaver } from "../FullScreenSaver";
/**
* Creates square Actors in the game.
*/
@@ -44,7 +48,6 @@ public constructor(settings: EightBittrConstructorSettings) {
this.quadsKeeper.resetQuadrants();
this.squares.addSquare(
"Square",
this.mapScreener.width / 2,
this.mapScreener.height / 2,
2,

Some files were not shown because too many files have changed in this diff Show More