Added basic game state
This commit is contained in:
@@ -137,5 +137,8 @@
|
||||
"@schematics/angular:directive": {
|
||||
"prefix": "app"
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"defaultCollection": "@ngrx/schematics"
|
||||
}
|
||||
}
|
||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
<mat-toolbar color="primary">
|
||||
<mat-toolbar-row>
|
||||
<span>Rock Paper Scissors</span>
|
||||
</mat-toolbar-row>
|
||||
</mat-toolbar>
|
||||
|
||||
<div class="app-body">
|
||||
<!-- score-card component -->
|
||||
<!-- game-card (reactive form) component -->
|
||||
<!-- game-history component -->
|
||||
</div>
|
||||
@@ -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]
|
||||
|
||||
22
src/app/store/game/game.actions.ts
Normal file
22
src/app/store/game/game.actions.ts
Normal 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'
|
||||
);
|
||||
43
src/app/store/game/game.effects.ts
Normal file
43
src/app/store/game/game.effects.ts
Normal 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
|
||||
) { }
|
||||
|
||||
}
|
||||
23
src/app/store/game/game.facade.ts
Normal file
23
src/app/store/game/game.facade.ts
Normal 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());
|
||||
}
|
||||
}
|
||||
5
src/app/store/game/game.model.ts
Normal file
5
src/app/store/game/game.model.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface Game {
|
||||
playerChoice: string,
|
||||
computerChoice: string,
|
||||
result: 'win' | 'lose' | 'error';
|
||||
}
|
||||
23
src/app/store/game/game.module.ts
Normal file
23
src/app/store/game/game.module.ts
Normal 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 { }
|
||||
13
src/app/store/game/game.reducer.spec.ts
Normal file
13
src/app/store/game/game.reducer.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
47
src/app/store/game/game.reducer.ts
Normal file
47
src/app/store/game/game.reducer.ts
Normal 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;
|
||||
6
src/app/store/game/game.selectors.ts
Normal file
6
src/app/store/game/game.selectors.ts
Normal 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
|
||||
);
|
||||
20
src/app/store/game/game.service.ts
Normal file
20
src/app/store/game/game.service.ts
Normal 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
1
src/app/store/game/index.ts
Normal file
1
src/app/store/game/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './game.facade';
|
||||
1
src/app/store/index.ts
Normal file
1
src/app/store/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './game/game.module';
|
||||
Reference in New Issue
Block a user