From a639ae47676878a3f7cc15d0136fc2a386757341 Mon Sep 17 00:00:00 2001 From: Tim Slatcher Date: Sat, 2 Nov 2019 15:48:57 +0000 Subject: [PATCH] add basic stats --- projects/app/src/App.tsx | 21 +++ projects/app/src/index.css | 1 + projects/app/src/pages/stats/StatsAnalysis.ts | 122 ++++++++++++++++++ projects/app/src/pages/stats/StatsPage.css | 20 +++ projects/app/src/pages/stats/StatsPage.tsx | 89 +++++++++++++ projects/shared/src/analysis/api.ts | 4 +- tslint.json | 3 +- 7 files changed, 257 insertions(+), 3 deletions(-) create mode 100644 projects/app/src/pages/stats/StatsAnalysis.ts create mode 100644 projects/app/src/pages/stats/StatsPage.css create mode 100644 projects/app/src/pages/stats/StatsPage.tsx diff --git a/projects/app/src/App.tsx b/projects/app/src/App.tsx index 9f9c944..f75caf1 100644 --- a/projects/app/src/App.tsx +++ b/projects/app/src/App.tsx @@ -12,6 +12,7 @@ import { HandPage } from "./pages/HandPage"; import { HomePage } from "./pages/HomePage"; import { LeaguePage } from "./pages/LeaguePage"; import { SeasonPage } from "./pages/SeasonPage"; +import { StatsPage } from "./pages/stats/StatsPage"; import { VsPage } from "./pages/VsPage"; export interface AppProps { @@ -37,12 +38,26 @@ export class App extends React.PureComponent { gameLoader={new SeasonGameLoader(this.props.api, props.match.params.seasonId)} /> ); + const seasonStatsPage = ( + props: RouteComponentProps<{ leagueId: string; seasonId: string }>, + ) => ( + + ); const leagueHistoryPage = (props: RouteComponentProps<{ leagueId: string }>) => ( ); + const leagueStatsPage = (props: RouteComponentProps<{ leagueId: string }>) => ( + + ); const playerHistoryPage = (props: RouteComponentProps<{ playerId: string }>) => ( { + { path="/league/:leagueId/season/:seasonId/game/:gameId/hand/:handId" render={handPage} /> + { + public initialState(): Stats { + return initialStats(); + } + + public analyze(current: Stats, hand: IHand): Stats { + const handResult = getHandResult(hand); + if (!handResult.valid) { + return current; + } + current.hands++; + for (const score of handResult.scores) { + current.scoreSumSquared += Math.pow(score, 2); + } + for (const playerHand of hand.playerHands) { + if (playerHand.chargedAh) { + current.ahCharges++; + } + if (playerHand.chargedJd) { + current.jdCharges++; + } + if (playerHand.chargedTc) { + current.tcCharges++; + } + if (playerHand.chargedQs) { + current.qsCharges++; + } + if (playerHand.tookJd && playerHand.tookTc) { + current.tj++; + } + if (playerHand.tookQs && playerHand.tookTc) { + current.tq++; + } + if (playerHand.tookQs && playerHand.tookTc && playerHand.tookJd) { + current.tqj++; + } + if (playerHand.tookQs && playerHand.hearts === 12) { + current.antiruns++; + } + if (playerHand.tookQs && playerHand.hearts === 13) { + current.runs++; + } + if (playerHand.chargedQs && playerHand.tookQs) { + current.tookOwnQsCharge++; + } + if (playerHand.chargedJd && playerHand.tookJd) { + current.tookOwnJdCharge++; + } + if (playerHand.chargedTc && playerHand.tookTc) { + current.tookOwnTcCharge++; + } + } + current.handScoreStdDev = Math.sqrt(current.scoreSumSquared / current.hands); + return current; + } +} + +export class StatsGameAnalysis implements IGameAnalysis { + public initialState(): Stats { + return initialStats(); + } + + public analyze(current: Stats, game: IGame): Stats { + if (!game.players || game.players.length === 0) { + return current; + } + if (game.players.some(p => p == null) || game.hands == null || game.hands.length === 0) { + return current; + } + + analyzeHands(game.hands, new StatsHandAnalysis(), current); + + return current; + } +} diff --git a/projects/app/src/pages/stats/StatsPage.css b/projects/app/src/pages/stats/StatsPage.css new file mode 100644 index 0000000..ce3ce29 --- /dev/null +++ b/projects/app/src/pages/stats/StatsPage.css @@ -0,0 +1,20 @@ +.stats-page { + margin: 20px; + + & b { + font-weight: 600; + } + + & ul { + list-style: circle; + margin: 20px 40px; + } + + & li { + line-height: 30px; + } + + & .th-card { + margin-right: 2px; + } +} diff --git a/projects/app/src/pages/stats/StatsPage.tsx b/projects/app/src/pages/stats/StatsPage.tsx new file mode 100644 index 0000000..13e48cd --- /dev/null +++ b/projects/app/src/pages/stats/StatsPage.tsx @@ -0,0 +1,89 @@ +import { analyzeGames } from "@turbo-hearts-scores/shared"; +import * as React from "react"; +import { GameLoader } from "../../api/gameLoader"; +import { Card } from "../components/Card"; +import { Stats, StatsGameAnalysis } from "./StatsAnalysis"; + +interface StatsPageProps { + gameLoader: GameLoader; +} + +interface StatsPageState { + stats: undefined | Stats; +} + +export class StatsPage extends React.PureComponent { + public state: StatsPageState = { + stats: undefined, + }; + + public async componentDidMount() { + this.fetchGames(); + } + + public render() { + if (this.state.stats !== undefined) { + return this.renderStats(this.state.stats); + } + return null; + } + + private renderStats(stats: Stats) { + return ( +
+

+ In {stats.hands} hands: +

+
    +
  • A player ran {this.renderPercent(stats.runs, stats.hands)} of the time.
  • +
  • A player antiran {this.renderPercent(stats.antiruns, stats.hands)} of the time.
  • +
  • + A player was + 'd in {this.renderPercent(stats.tq, stats.hands)}. +
  • +
  • + A player took + in {this.renderPercent(stats.tj, stats.hands)}. +
  • +
  • + A player took + + in {this.renderPercent(stats.tqj, stats.hands)}. +
  • +
  • + The was charged in{" "} + {this.renderPercent(stats.qsCharges, stats.hands)}, and was taken by the charger{" "} + {this.renderPercent(stats.tookOwnQsCharge, stats.qsCharges)} of the time. +
  • +
  • + The was charged in{" "} + {this.renderPercent(stats.jdCharges, stats.hands)}, and was taken by the charger{" "} + {this.renderPercent(stats.tookOwnJdCharge, stats.jdCharges)} of the time. +
  • +
  • + The was charged in{" "} + {this.renderPercent(stats.tcCharges, stats.hands)}, and was taken by the charger{" "} + {this.renderPercent(stats.tookOwnTcCharge, stats.tcCharges)} of the time. +
  • +
  • + The was charged in{" "} + {this.renderPercent(stats.ahCharges, stats.hands)}. +
  • +
  • + The score standard deviation was {stats.handScoreStdDev.toFixed(1)}. +
  • +
+
+ ); + } + + private renderPercent(p: number, n: number) { + return {(p / n * 100).toFixed(1)}%; + } + + private async fetchGames() { + const allGames = await this.props.gameLoader.loadGames(); + const stats = analyzeGames(allGames, new StatsGameAnalysis()); + this.setState({ stats }); + } +} diff --git a/projects/shared/src/analysis/api.ts b/projects/shared/src/analysis/api.ts index 895b05c..7071222 100644 --- a/projects/shared/src/analysis/api.ts +++ b/projects/shared/src/analysis/api.ts @@ -17,8 +17,8 @@ export function analyzeGames(games: IGame[], analysis: IGameAnalysis): R { return current; } -export function analyzeHands(hands: IHand[], analysis: IHandAnalysis): R { - let current = analysis.initialState(); +export function analyzeHands(hands: IHand[], analysis: IHandAnalysis, initialState?: R): R { + let current = initialState === undefined ? analysis.initialState() : initialState; for (const hand of hands) { current = analysis.analyze(current, hand); } diff --git a/tslint.json b/tslint.json index fd4ed6b..3f7f883 100644 --- a/tslint.json +++ b/tslint.json @@ -24,6 +24,7 @@ "ban-keywords", "check-format" ] - } + }, + "forin": false } }