From 4b2790da59809a3a2eb4f5d1f10793c14d8bee66 Mon Sep 17 00:00:00 2001 From: Andrew Kemp Date: Tue, 12 May 2020 18:24:03 -0400 Subject: [PATCH] Added basic game state --- angular.json | 3 ++ package-lock.json | 10 ++++++ package.json | 2 ++ src/app/app.component.html | 12 +++++-- src/app/app.module.ts | 4 +++ src/app/store/game/game.actions.ts | 22 ++++++++++++ src/app/store/game/game.effects.ts | 43 ++++++++++++++++++++++ src/app/store/game/game.facade.ts | 23 ++++++++++++ src/app/store/game/game.model.ts | 5 +++ src/app/store/game/game.module.ts | 23 ++++++++++++ src/app/store/game/game.reducer.spec.ts | 13 +++++++ src/app/store/game/game.reducer.ts | 47 +++++++++++++++++++++++++ src/app/store/game/game.selectors.ts | 6 ++++ src/app/store/game/game.service.ts | 20 +++++++++++ src/app/store/game/index.ts | 1 + src/app/store/index.ts | 1 + 16 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 src/app/store/game/game.actions.ts create mode 100644 src/app/store/game/game.effects.ts create mode 100644 src/app/store/game/game.facade.ts create mode 100644 src/app/store/game/game.model.ts create mode 100644 src/app/store/game/game.module.ts create mode 100644 src/app/store/game/game.reducer.spec.ts create mode 100644 src/app/store/game/game.reducer.ts create mode 100644 src/app/store/game/game.selectors.ts create mode 100644 src/app/store/game/game.service.ts create mode 100644 src/app/store/game/index.ts create mode 100644 src/app/store/index.ts diff --git a/angular.json b/angular.json index 577b6f5..19a9582 100644 --- a/angular.json +++ b/angular.json @@ -137,5 +137,8 @@ "@schematics/angular:directive": { "prefix": "app" } + }, + "cli": { + "defaultCollection": "@ngrx/schematics" } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4d36b85..a6d7666 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1683,6 +1683,16 @@ } } }, + "@ngrx/effects": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-9.1.2.tgz", + "integrity": "sha512-H9jbGUzP5izk9Ap8BQJicO1+xheyDyHBbvv6b1NkaRHpDizhPOSBjoFWExFfsejXo0dafaIsu6aI+y+Fp+LSsg==" + }, + "@ngrx/schematics": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@ngrx/schematics/-/schematics-9.1.2.tgz", + "integrity": "sha512-oa2fSjoaYlTmBPJW45EQlaeTGIJZ5wKXQFDGUBlJQNtZf/El2CXmGyo0VWzxQ40y2GCdt4gCXeDHZWCchUFCLA==" + }, "@ngrx/store": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-9.1.2.tgz", diff --git a/package.json b/package.json index a0bc9e7..608f17e 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,8 @@ "@angular/platform-browser": "^9.1.6", "@angular/platform-browser-dynamic": "^9.1.6", "@angular/router": "^9.1.6", + "@ngrx/effects": "^9.1.2", + "@ngrx/schematics": "^9.1.2", "@ngrx/store": "^9.1.2", "core-js": "^2.4.1", "rxjs": "^6.5.4", diff --git a/src/app/app.component.html b/src/app/app.component.html index bc5157a..bbc6764 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,3 +1,11 @@ - Rock Paper Scissors - \ No newline at end of file + + Rock Paper Scissors + + + +
+ + + +
\ No newline at end of file diff --git a/src/app/app.module.ts b/src/app/app.module.ts index efa8b97..dcd047a 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -5,8 +5,10 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { MatButtonModule } from '@angular/material/button'; import { MatToolbarModule } from '@angular/material/toolbar'; import { StoreModule } from '@ngrx/store'; +import { EffectsModule } from '@ngrx/effects'; import { AppComponent } from './app.component'; +import { GameStoreModule } from './store'; @NgModule({ declarations: [ @@ -18,6 +20,8 @@ import { AppComponent } from './app.component'; MatButtonModule, MatToolbarModule, StoreModule.forRoot({}, {}), + EffectsModule.forRoot([]), + GameStoreModule ], providers: [], bootstrap: [AppComponent] diff --git a/src/app/store/game/game.actions.ts b/src/app/store/game/game.actions.ts new file mode 100644 index 0000000..af0408c --- /dev/null +++ b/src/app/store/game/game.actions.ts @@ -0,0 +1,22 @@ +import { createAction, props } from '@ngrx/store'; + +import { Game } from './game.model'; + +export const playMatch = createAction( + '[Game] Play Match', + props<{ playerChoice: string }>() +); + +export const playMatchSuccess = createAction( + '[Game] Play Match Success', + props<{ game: Game }>() +); + +export const playMatchFailure = createAction( + '[Game] Play Match Failure', + props<{ game: Game, error: any }>() +); + +export const resetMatch = createAction( + '[Game] Reset Match' +); diff --git a/src/app/store/game/game.effects.ts b/src/app/store/game/game.effects.ts new file mode 100644 index 0000000..fbf61e7 --- /dev/null +++ b/src/app/store/game/game.effects.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@angular/core'; +import { Actions, createEffect, ofType } from '@ngrx/effects'; + +import { of } from 'rxjs'; +import { catchError, map, exhaustMap, tap } from 'rxjs/operators'; + +import * as GameActions from './game.actions'; +import { GameService } from './game.service'; +import { Game } from './game.model'; + +@Injectable() +export class GameEffects { + + playMatch$ = createEffect(() => + this.actions$.pipe( + ofType(GameActions.playMatch), + map(action => action.playerChoice), + exhaustMap(playerChoice => + this.gameApi.playMatch(playerChoice).pipe( + map(game => GameActions.playMatchSuccess({ game })), + catchError(serverError => of(GameActions.playMatchFailure({ + game: { playerChoice, computerChoice: '', result: 'error' } as Game, + error: serverError + }))) + ) + ) + ) + ); + + errorToast$ = createEffect(() => + this.actions$.pipe( + ofType(GameActions.playMatchFailure), + map(action => action.error), + tap(e => console.error(e)) // TODO: replace with material toast service + ), { dispatch: false } + ); + + constructor( + private readonly gameApi: GameService, + private readonly actions$: Actions + ) { } + +} diff --git a/src/app/store/game/game.facade.ts b/src/app/store/game/game.facade.ts new file mode 100644 index 0000000..2aed9be --- /dev/null +++ b/src/app/store/game/game.facade.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@angular/core'; +import { Store, select } from '@ngrx/store'; + +import * as gameActions from './game.actions'; + +@Injectable() +export class GameFacade { + // gameHistory$ = this.store.pipe(select(getGameHistory)); + // scores$ = this.store.pipe(select(getScores)); + // gameStatus$ = this.store.pipe(select(getGameStatus)); + + constructor( + private readonly store: Store + ) { } + + play(playerChoice: string): void { + this.store.dispatch(gameActions.playMatch({ playerChoice })); + } + + reset(): void { + this.store.dispatch(gameActions.resetMatch()); + } +} diff --git a/src/app/store/game/game.model.ts b/src/app/store/game/game.model.ts new file mode 100644 index 0000000..9afb4b9 --- /dev/null +++ b/src/app/store/game/game.model.ts @@ -0,0 +1,5 @@ +export interface Game { + playerChoice: string, + computerChoice: string, + result: 'win' | 'lose' | 'error'; +} \ No newline at end of file diff --git a/src/app/store/game/game.module.ts b/src/app/store/game/game.module.ts new file mode 100644 index 0000000..1d9074b --- /dev/null +++ b/src/app/store/game/game.module.ts @@ -0,0 +1,23 @@ +import { NgModule } from '@angular/core'; +import { HttpClientModule } from '@angular/common/http'; + +import { StoreModule } from '@ngrx/store'; +import { EffectsModule } from '@ngrx/effects'; + +import * as fromGame from './game.reducer'; +import { GameEffects } from './game.effects'; +import { GameFacade } from './game.facade'; +import { GameService } from './game.service'; + +@NgModule({ + imports: [ + HttpClientModule, + StoreModule.forFeature(fromGame.gameFeatureKey, fromGame.reducer), + EffectsModule.forFeature([GameEffects]) + ], + providers: [ + GameService, + GameFacade + ] +}) +export class GameStoreModule { } diff --git a/src/app/store/game/game.reducer.spec.ts b/src/app/store/game/game.reducer.spec.ts new file mode 100644 index 0000000..a30d651 --- /dev/null +++ b/src/app/store/game/game.reducer.spec.ts @@ -0,0 +1,13 @@ +import { reducer, initialState } from './game.reducer'; + +describe('Game Reducer', () => { + describe('an unknown action', () => { + it('should return the previous state', () => { + const action = {} as any; + + const result = reducer(initialState, action); + + expect(result).toBe(initialState); + }); + }); +}); diff --git a/src/app/store/game/game.reducer.ts b/src/app/store/game/game.reducer.ts new file mode 100644 index 0000000..67378cb --- /dev/null +++ b/src/app/store/game/game.reducer.ts @@ -0,0 +1,47 @@ +import { Action, createReducer, on } from '@ngrx/store'; + +import * as GameActions from './game.actions'; +import { Game } from './game.model'; + +export const gameFeatureKey = 'game'; + +export interface State { + gameHistory: Game[], + gameStatus: 'pre' | 'pending' | 'post' +} + +export const initialState: State = { + gameHistory: [], + gameStatus: 'pre' +}; + + +export const reducer = createReducer( + initialState, + + on(GameActions.playMatch, state => ({ + ...state, + gameStatus: 'pending' + })), + on(GameActions.playMatchSuccess, (state, action) => ({ + ...state, + gameStatus: 'post', + gameHistory: [ + ...state.gameHistory, + action.game + ] + })), + on(GameActions.playMatchFailure, (state, action) => ({ + gameStatus: 'post', + gameHistory: [ + ...state.gameHistory, + action.error + ] + })), + on(GameActions.resetMatch, (state, action) => ({ + gameStatus: 'pre' + })) + +); + +export const getHasGameHistory = (state: State) => state.gameHistory.length > 0; \ No newline at end of file diff --git a/src/app/store/game/game.selectors.ts b/src/app/store/game/game.selectors.ts new file mode 100644 index 0000000..859744e --- /dev/null +++ b/src/app/store/game/game.selectors.ts @@ -0,0 +1,6 @@ +import { createFeatureSelector, createSelector } from '@ngrx/store'; +import * as fromGame from './game.reducer'; + +export const selectGameState = createFeatureSelector( + fromGame.gameFeatureKey +); diff --git a/src/app/store/game/game.service.ts b/src/app/store/game/game.service.ts new file mode 100644 index 0000000..6118b87 --- /dev/null +++ b/src/app/store/game/game.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; + +import { map } from 'rxjs/operators'; + +import { Game } from './game.model'; + +const API_BASE = 'http://localhost:3000/api'; + +@Injectable() +export class GameService { + + constructor(private http: HttpClient) { } + + playMatch(playerChoice: string) { + return this.http.post(`${API_BASE}/match`, { choice: playerChoice }).pipe( + map(response => response as Game) + ) + } +} diff --git a/src/app/store/game/index.ts b/src/app/store/game/index.ts new file mode 100644 index 0000000..53bbb7a --- /dev/null +++ b/src/app/store/game/index.ts @@ -0,0 +1 @@ +export * from './game.facade'; \ No newline at end of file diff --git a/src/app/store/index.ts b/src/app/store/index.ts new file mode 100644 index 0000000..d987bea --- /dev/null +++ b/src/app/store/index.ts @@ -0,0 +1 @@ +export * from './game/game.module'; \ No newline at end of file