Skip to content

Commit

Permalink
Merge pull request #9 from devshareacademy/simon
Browse files Browse the repository at this point in the history
contains the prototype code for the game simon
  • Loading branch information
scottwestover authored Jan 14, 2024
2 parents 6c4c76d + 1b7df96 commit 605ab96
Show file tree
Hide file tree
Showing 17 changed files with 1,530 additions and 0 deletions.
4 changes: 4 additions & 0 deletions games/simon/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/dist
/node_modules
yarn-error.log
.DS_Store
14 changes: 14 additions & 0 deletions games/simon/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"eslint.format.enable": true,
"eslint.options": {
"overrideConfigFile": "config/.eslintrc"
},
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"cSpell.words": [
"devshareacademy",
"spritesheet"
]
}
49 changes: 49 additions & 0 deletions games/simon/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Phaser 3 TypeScript - Simon

A basic Phaser 3 TypeScript prototype of the game Simon.

## Local Development

### Requirements

[Node.js](https://nodejs.org) and [Yarn](https://yarnpkg.com/) are required to install dependencies and run scripts via `yarn`.

[Vite](https://vitejs.dev/) is required to bundle and serve the web application. This is included as part of the projects dev dependencies.

### Available Commands

| Command | Description |
|---------|-------------|
| `yarn install --frozen-lockfile` | Install project dependencies |
| `yarn start` | Build project and open web server running project |
| `yarn build` | Builds code bundle for production |
| `yarn lint` | Uses ESLint to lint code |

### Writing Code

After cloning the repo, run `yarn install --frozen-lockfile` from your project directory. Then, you can start the local development
server by running `yarn start`.

After starting the development server with `yarn start`, you can edit any files in the `src` folder
and parcel will automatically recompile and reload your server (available at `http://localhost:8080`
by default).

### Deploying Code

After you run the `yarn build` command, your code will be built into a single bundle located at
`dist/*` along with any other assets you project depended.

If you put the contents of the `dist` folder in a publicly-accessible location (say something like `http://myserver.com`),
you should be able to open `http://myserver.com/index.html` and play your game.

### Static Assets

Any static assets like images or audio files should be placed in the `public` folder. It'll then be served at `http://localhost:8080/path-to-file-your-file/file-name.file-type`.

## Ideas for improvement

* add simple score mechanic
* add high local high scores
* create npm package for the simon game class
* add support for mobile
* add title & game over screens
9 changes: 9 additions & 0 deletions games/simon/config/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"root": true,
"extends": "@devshareacademy/eslint-config",
"parserOptions": {
"project": "./tsconfig.json"
},
"rules": {},
"ignorePatterns": ["node_modules", "dist"]
}
11 changes: 11 additions & 0 deletions games/simon/config/vite.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineConfig } from 'vite';

export default defineConfig({
build: {
rollupOptions: {
output: {
entryFileNames: 'assets/js/[name]-[hash].js',
},
},
},
});
27 changes: 27 additions & 0 deletions games/simon/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Phaser 3 - Simon</title>
<style>
html,
body,
.container {
margin: 0px;
height: 100vh;
width: 100vw;
overflow: hidden;
background: #d7d7d7;
display: flex;
flex-direction: column;
align-items: center;
}
</style>
</head>
<body>
<div class="container" id="game-container">
<h1>Phaser 3 - Simon</h1>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
40 changes: 40 additions & 0 deletions games/simon/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "@devshareacademy/phaser-3-simon-prototype",
"version": "1.0.0",
"description": "A basic Phaser 3 Typescript prototype for the game Simon.",
"scripts": {
"start": "vite --config config/vite.config.js",
"build": "tsc && vite build --config config/vite.config.js",
"serve": "vite preview --config config/vite.config.js",
"lint": "eslint ./src --ext .ts,.tsx --config ./config/.eslintrc"
},
"author": "scottwestover",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/devshareacademy/phaser-3-typescript-games-and-examples.git"
},
"homepage": "https://github.com/devshareacademy/phaser-3-typescript-games-and-examples",
"devDependencies": {
"@devshareacademy/eslint-config": "0.0.16",
"@devshareacademy/prettier-config": "0.0.4",
"@devshareacademy/tsconfig": "0.0.3",
"@typescript-eslint/eslint-plugin": "5.22.0",
"@typescript-eslint/parser": "5.22.0",
"eslint": "8.14.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-prettier": "4.0.0",
"prettier": "2.6.2",
"typescript": "4.6.4",
"vite": "2.9.13"
},
"dependencies": {
"phaser": "3.55.2"
},
"resolutions": {},
"prettier": "@devshareacademy/prettier-config",
"volta": {
"node": "16.15.0",
"yarn": "1.22.11"
}
}
13 changes: 13 additions & 0 deletions games/simon/project-task.todo
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
☐ project setup
☐ create buttons
☐ add hover events with alpha
☐ core simon logic
☐ generate a random element in the sequence
☐ method to handle player input
☐ track player input until sequence match, or mismatch
☐ reset game
☐ connect simon to core scene
☐ add simple state for tracking game state
☐ play a sequence
☐ change game alpha and play sound
☐ wait for player input
Binary file added games/simon/public/assets/audio/simonSound1.mp3
Binary file not shown.
Binary file added games/simon/public/assets/audio/simonSound2.mp3
Binary file not shown.
Binary file added games/simon/public/assets/audio/simonSound3.mp3
Binary file not shown.
Binary file added games/simon/public/assets/audio/simonSound4.mp3
Binary file not shown.
144 changes: 144 additions & 0 deletions games/simon/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import Phaser from 'phaser';
import Simon from './simon';
import { sleep } from './utils';

const ASSET_KEYS = {
SOUND1: 'SOUND1',
SOUND2: 'SOUND2',
SOUND3: 'SOUND3',
SOUND4: 'SOUND4',
} as const;

type GameState = keyof typeof GAME_STATE;

const GAME_STATE = {
INITIAL: 'INITIAL',
PLAYING_PATTERN: 'PLAYING_PATTERN',
WAITING_FOR_INPUT: 'WAITING_FOR_INPUT',
DONE: 'DONE',
} as const;

class Game extends Phaser.Scene {
#gameState!: GameState;
#simonGame!: Simon;
#buttons!: Phaser.GameObjects.Rectangle[];

constructor() {
super({ key: 'Game' });
}

public init() {
this.#gameState = GAME_STATE.INITIAL;
this.#buttons = [];
this.#simonGame = new Simon();
}

public preload(): void {
this.load.audio(ASSET_KEYS.SOUND1, 'assets/audio/simonSound1.mp3');
this.load.audio(ASSET_KEYS.SOUND2, 'assets/audio/simonSound2.mp3');
this.load.audio(ASSET_KEYS.SOUND3, 'assets/audio/simonSound3.mp3');
this.load.audio(ASSET_KEYS.SOUND4, 'assets/audio/simonSound4.mp3');
}

public create(): void {
// game width =
const redButton = this.#createButton(20, 20, 0xdb0a8b, 0xf535aa, 0, ASSET_KEYS.SOUND1);
const greenButton = this.#createButton(230, 20, 0x08c418, 0x16f529, 1, ASSET_KEYS.SOUND2);
const yellowButton = this.#createButton(20, 230, 0xe6e600, 0xffff33, 2, ASSET_KEYS.SOUND3);
const blueButton = this.#createButton(230, 230, 0x0066cc, 0x1589ff, 3, ASSET_KEYS.SOUND4);
this.#buttons = [redButton, greenButton, yellowButton, blueButton];

// play sequence for player to remember and transition to wait for player input
this.#playSequence().catch(() => {
// do nothing
});
}

#createButton(
x: number,
y: number,
color: number,
hoverColor: number,
buttonId: number,
audioAssetKey: string,
): Phaser.GameObjects.Rectangle {
// create Phaser rectangle game object that is a little transparent and that is interactive (allows for player interaction)
const button = this.add.rectangle(x, y, 200, 200, color).setOrigin(0).setAlpha(0.4).setInteractive();

// set custom data attribute to track hover and original color
button.setDataEnabled();
button.data.set({ color, hoverColor, id: buttonId, audioAssetKey }, undefined);

// add event listener for pointer over event (mouse hovers over element), when event fires update color and fill
button.on(Phaser.Input.Events.POINTER_OVER as string, () => {
if (this.#gameState === GAME_STATE.WAITING_FOR_INPUT) {
button.fillColor = hoverColor;
button.setAlpha(1);
}
});

// add event listener for pointer out event (mouse was hovering over element and then leaves), when event fires update color and fill
button.on(Phaser.Input.Events.POINTER_OUT as string, () => {
if (this.#gameState === GAME_STATE.WAITING_FOR_INPUT) {
button.fillColor = color;
button.setAlpha(0.4);
}
});

// add event listener for click events, when event fires update the player moves in the simon game logic
button.on(Phaser.Input.Events.POINTER_DOWN as string, () => {
if (this.#gameState === GAME_STATE.WAITING_FOR_INPUT) {
this.sound.play(audioAssetKey);
this.#simonGame.checkPlayerMove(button.data.values.id as number);

button.fillColor = color;
button.setAlpha(0.4);

// check to see if game is over and go to DONE state
if (this.#simonGame.isGameOver) {
this.#gameState = GAME_STATE.DONE;
console.log('Game is over');
return;
}

// check if the player sequence is complete, if so go to the next round
if (this.#simonGame.isPlayerSequenceComplete) {
this.#gameState = GAME_STATE.PLAYING_PATTERN;
this.#simonGame.generateNextSequenceElement();
this.#playSequence().catch(() => {
// do nothing
});
}
}
});
return button;
}

async #playSequence(): Promise<void> {
await sleep(1000);
const currentSequence = this.#simonGame.sequence;
for (const num of currentSequence) {
this.#buttons[num].fillColor = this.#buttons[num].data.values.hoverColor as number;
this.#buttons[num].setAlpha(1);
this.sound.play(this.#buttons[num].data.values.audioAssetKey as string);
await sleep(1000);
this.#buttons[num].fillColor = this.#buttons[num].data.values.color as number;
this.#buttons[num].setAlpha(0.4);
await sleep(400);
}

this.#gameState = GAME_STATE.WAITING_FOR_INPUT;
}
}

const gameConfig: Phaser.Types.Core.GameConfig = {
type: Phaser.CANVAS,
pixelArt: true,
parent: 'game-container',
width: 450,
height: 450,
backgroundColor: '#5c5b5b',
scene: [Game],
};

const game = new Phaser.Game(gameConfig);
69 changes: 69 additions & 0 deletions games/simon/src/simon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
function getRandomIntInclusive(min: number, max: number): number {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1) + min);
}

enum SimonError {
INVALID_MOVE_GAME_IS_OVER = 'Game has already ended, please reset the game state.',
}

export default class Simon {
#currentSequence: number[] = [];
#playerMoves: number[] = [];
#isGameOver = false;

constructor() {
this.#initializeGame();
}

get sequence(): number[] {
return [...this.#currentSequence];
}

get isGameOver(): boolean {
return this.#isGameOver;
}

get isPlayerSequenceComplete(): boolean {
return this.#playerMoves.length === this.#currentSequence.length;
}

public resetGame(): void {
this.#initializeGame();
}

public generateNextSequenceElement() {
// validate that the game is not already done
if (this.#isGameOver) {
throw new Error(SimonError.INVALID_MOVE_GAME_IS_OVER);
}

this.#currentSequence.push(getRandomIntInclusive(0, 3));
// reset tracked players moves
this.#playerMoves = [];
}

public checkPlayerMove(x: number) {
// validate that the game is not already done
if (this.#isGameOver) {
throw new Error(SimonError.INVALID_MOVE_GAME_IS_OVER);
}

// only allow moves to be added if we have not validated a player sequence
if (this.#playerMoves.length === this.#currentSequence.length) return;

// add new move to the player sequence
this.#playerMoves.push(x);

// update game state if this last move was valid or not
this.#isGameOver =
this.#playerMoves[this.#playerMoves.length - 1] !== this.#currentSequence[this.#playerMoves.length - 1];
}

#initializeGame() {
this.#currentSequence = [];
this.#playerMoves = [];
this.generateNextSequenceElement();
}
}
3 changes: 3 additions & 0 deletions games/simon/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function sleep(milliseconds: number) {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
Loading

0 comments on commit 605ab96

Please sign in to comment.