1
0

Added basic game state

This commit is contained in:
2020-05-12 18:24:03 -04:00
parent ff172e11d7
commit 4b2790da59
16 changed files with 233 additions and 2 deletions

View File

@@ -137,5 +137,8 @@
"@schematics/angular:directive": { "@schematics/angular:directive": {
"prefix": "app" "prefix": "app"
} }
},
"cli": {
"defaultCollection": "@ngrx/schematics"
} }
} }

10
package-lock.json generated
View File

@@ -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": { "@ngrx/store": {
"version": "9.1.2", "version": "9.1.2",
"resolved": "https://registry.npmjs.org/@ngrx/store/-/store-9.1.2.tgz", "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-9.1.2.tgz",

View File

@@ -22,6 +22,8 @@
"@angular/platform-browser": "^9.1.6", "@angular/platform-browser": "^9.1.6",
"@angular/platform-browser-dynamic": "^9.1.6", "@angular/platform-browser-dynamic": "^9.1.6",
"@angular/router": "^9.1.6", "@angular/router": "^9.1.6",
"@ngrx/effects": "^9.1.2",
"@ngrx/schematics": "^9.1.2",
"@ngrx/store": "^9.1.2", "@ngrx/store": "^9.1.2",
"core-js": "^2.4.1", "core-js": "^2.4.1",
"rxjs": "^6.5.4", "rxjs": "^6.5.4",

View File

@@ -1,3 +1,11 @@
<mat-toolbar color="primary"> <mat-toolbar color="primary">
<mat-toolbar-row>
<span>Rock Paper Scissors</span> <span>Rock Paper Scissors</span>
</mat-toolbar-row>
</mat-toolbar> </mat-toolbar>
<div class="app-body">
<!-- score-card component -->
<!-- game-card (reactive form) component -->
<!-- game-history component -->
</div>

View File

@@ -5,8 +5,10 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatToolbarModule } from '@angular/material/toolbar'; import { MatToolbarModule } from '@angular/material/toolbar';
import { StoreModule } from '@ngrx/store'; import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { GameStoreModule } from './store';
@NgModule({ @NgModule({
declarations: [ declarations: [
@@ -18,6 +20,8 @@ import { AppComponent } from './app.component';
MatButtonModule, MatButtonModule,
MatToolbarModule, MatToolbarModule,
StoreModule.forRoot({}, {}), StoreModule.forRoot({}, {}),
EffectsModule.forRoot([]),
GameStoreModule
], ],
providers: [], providers: [],
bootstrap: [AppComponent] bootstrap: [AppComponent]

View File

@@ -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'
);

View File

@@ -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
) { }
}

View File

@@ -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());
}
}

View File

@@ -0,0 +1,5 @@
export interface Game {
playerChoice: string,
computerChoice: string,
result: 'win' | 'lose' | 'error';
}

View File

@@ -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 { }

View File

@@ -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);
});
});
});

View File

@@ -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;

View File

@@ -0,0 +1,6 @@
import { createFeatureSelector, createSelector } from '@ngrx/store';
import * as fromGame from './game.reducer';
export const selectGameState = createFeatureSelector<fromGame.State>(
fromGame.gameFeatureKey
);

View File

@@ -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)
)
}
}

View File

@@ -0,0 +1 @@
export * from './game.facade';

1
src/app/store/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from './game/game.module';