Skip to content

Commit

Permalink
add basic stats
Browse files Browse the repository at this point in the history
  • Loading branch information
Tim Slatcher committed Nov 2, 2019
1 parent ee18809 commit a639ae4
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 3 deletions.
21 changes: 21 additions & 0 deletions projects/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -37,12 +38,26 @@ export class App extends React.PureComponent<AppProps, {}> {
gameLoader={new SeasonGameLoader(this.props.api, props.match.params.seasonId)}
/>
);
const seasonStatsPage = (
props: RouteComponentProps<{ leagueId: string; seasonId: string }>,
) => (
<StatsPage
{...props}
gameLoader={new SeasonGameLoader(this.props.api, props.match.params.seasonId)}
/>
);
const leagueHistoryPage = (props: RouteComponentProps<{ leagueId: string }>) => (
<GameHistoryPage
{...props}
gameLoader={new LeagueGameLoader(this.props.api, props.match.params.leagueId)}
/>
);
const leagueStatsPage = (props: RouteComponentProps<{ leagueId: string }>) => (
<StatsPage
{...props}
gameLoader={new LeagueGameLoader(this.props.api, props.match.params.leagueId)}
/>
);
const playerHistoryPage = (props: RouteComponentProps<{ playerId: string }>) => (
<GameHistoryPage
{...props}
Expand Down Expand Up @@ -78,6 +93,7 @@ export class App extends React.PureComponent<AppProps, {}> {
<Route exact={true} path="/" render={homePage} />
<Route exact={true} path="/league/:leagueId" render={leaguePage} />
<Route exact={true} path="/league/:leagueId/history" render={leagueHistoryPage} />
<Route exact={true} path="/league/:leagueId/stats" render={leagueStatsPage} />
<Route exact={true} path="/league/:leagueId/season/:seasonId" render={seasonPage} />
<Route
exact={true}
Expand All @@ -94,6 +110,11 @@ export class App extends React.PureComponent<AppProps, {}> {
path="/league/:leagueId/season/:seasonId/game/:gameId/hand/:handId"
render={handPage}
/>
<Route
exact={true}
path="/league/:leagueId/season/:seasonId/stats"
render={seasonStatsPage}
/>
<Route exact={true} path="/player/:playerId/history" render={playerHistoryPage} />
<Route
exact={true}
Expand Down
1 change: 1 addition & 0 deletions projects/app/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Theme currently duplicated in .postcssrc.
@import "./pages/GamePage.css";
@import "./pages/HandPage.css";
@import "./pages/VsPage.css";
@import "./pages/stats/StatsPage.css";
@import "./pages/components/HandResult.css";
@import "./pages/components/GameResult.css";
@import "./pages/components/PlayerHand.css";
Expand Down
122 changes: 122 additions & 0 deletions projects/app/src/pages/stats/StatsAnalysis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import {
analyzeHands,
getHandResult,
IGame,
IGameAnalysis,
IHand,
IHandAnalysis,
} from "@turbo-hearts-scores/shared";

export interface Stats {
hands: number;
jdCharges: number;
ahCharges: number;
tcCharges: number;
qsCharges: number;
tq: number;
tj: number;
tqj: number;
tookOwnJdCharge: number;
tookOwnQsCharge: number;
tookOwnTcCharge: number;
runs: number;
antiruns: number;
scoreSumSquared: number;
handScoreStdDev: number;
}

function initialStats() {
return {
ahCharges: 0,
hands: 0,
jdCharges: 0,
scoreSumSquared: 0,
tcCharges: 0,
qsCharges: 0,
tookOwnJdCharge: 0,
tookOwnQsCharge: 0,
tookOwnTcCharge: 0,
tq: 0,
tj: 0,
tqj: 0,
runs: 0,
antiruns: 0,
handScoreStdDev: 0,
};
}

export class StatsHandAnalysis implements IHandAnalysis<Stats> {
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<Stats> {
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;
}
}
20 changes: 20 additions & 0 deletions projects/app/src/pages/stats/StatsPage.css
Original file line number Diff line number Diff line change
@@ -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;
}
}
89 changes: 89 additions & 0 deletions projects/app/src/pages/stats/StatsPage.tsx
Original file line number Diff line number Diff line change
@@ -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<StatsPageProps, StatsPageState> {
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 (
<div className="stats-page">
<h4>
In <b>{stats.hands}</b> hands:
</h4>
<ul>
<li>A player ran {this.renderPercent(stats.runs, stats.hands)} of the time.</li>
<li>A player antiran {this.renderPercent(stats.antiruns, stats.hands)} of the time.</li>
<li>
A player was <Card rank="10" suit="CLUBS" />
<Card rank="Q" suit="SPADES" />'d in {this.renderPercent(stats.tq, stats.hands)}.
</li>
<li>
A player took <Card rank="10" suit="CLUBS" />
<Card rank="J" suit="DIAMONDS" /> in {this.renderPercent(stats.tj, stats.hands)}.
</li>
<li>
A player took <Card rank="10" suit="CLUBS" />
<Card rank="Q" suit="SPADES" />
<Card rank="J" suit="DIAMONDS" /> in {this.renderPercent(stats.tqj, stats.hands)}.
</li>
<li>
The <Card rank="Q" suit="SPADES" /> was charged in{" "}
{this.renderPercent(stats.qsCharges, stats.hands)}, and was taken by the charger{" "}
{this.renderPercent(stats.tookOwnQsCharge, stats.qsCharges)} of the time.
</li>
<li>
The <Card rank="J" suit="DIAMONDS" /> was charged in{" "}
{this.renderPercent(stats.jdCharges, stats.hands)}, and was taken by the charger{" "}
{this.renderPercent(stats.tookOwnJdCharge, stats.jdCharges)} of the time.
</li>
<li>
The <Card rank="10" suit="CLUBS" /> was charged in{" "}
{this.renderPercent(stats.tcCharges, stats.hands)}, and was taken by the charger{" "}
{this.renderPercent(stats.tookOwnTcCharge, stats.tcCharges)} of the time.
</li>
<li>
The <Card rank="A" suit="HEARTS" /> was charged in{" "}
{this.renderPercent(stats.ahCharges, stats.hands)}.
</li>
<li>
The score standard deviation was <b>{stats.handScoreStdDev.toFixed(1)}</b>.
</li>
</ul>
</div>
);
}

private renderPercent(p: number, n: number) {
return <b>{(p / n * 100).toFixed(1)}%</b>;
}

private async fetchGames() {
const allGames = await this.props.gameLoader.loadGames();
const stats = analyzeGames(allGames, new StatsGameAnalysis());
this.setState({ stats });
}
}
4 changes: 2 additions & 2 deletions projects/shared/src/analysis/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ export function analyzeGames<R>(games: IGame[], analysis: IGameAnalysis<R>): R {
return current;
}

export function analyzeHands<R>(hands: IHand[], analysis: IHandAnalysis<R>): R {
let current = analysis.initialState();
export function analyzeHands<R>(hands: IHand[], analysis: IHandAnalysis<R>, initialState?: R): R {
let current = initialState === undefined ? analysis.initialState() : initialState;
for (const hand of hands) {
current = analysis.analyze(current, hand);
}
Expand Down
3 changes: 2 additions & 1 deletion tslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"ban-keywords",
"check-format"
]
}
},
"forin": false
}
}

0 comments on commit a639ae4

Please sign in to comment.