Skip to content

Commit

Permalink
Feature/score comparison (#209)
Browse files Browse the repository at this point in the history
* format score comparison files with Prettier

* added score comparison
  • Loading branch information
kdblocher committed Aug 6, 2023
1 parent e6a63e5 commit 9d6c33a
Show file tree
Hide file tree
Showing 9 changed files with 1,378 additions and 568 deletions.
638 changes: 424 additions & 214 deletions src/components/Analyses.tsx

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions src/components/core/BidSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { readonlyRecord as RR, readonlyArray as RA, } from "fp-ts"
import { ContractBid, eqContractBid, levels, strains } from "../../model/bridge"
import StrainTableView, { StrainSpan } from "./StrainTableView"
import { pipe } from "fp-ts/lib/function"
import { Button } from '@fluentui/react-components';

interface BidSelectorButtonProps {
selected: boolean,
children: JSX.Element
setSelected: (selected: boolean) => void
}
const BidSelectorButton = ({ children, selected, setSelected }: BidSelectorButtonProps) =>
<Button style={{minWidth: "50px", width: "50px"}} onClick={() => setSelected(!selected)} appearance={selected ? "primary" : "secondary"}>{children}</Button>

interface BidSelectorProps {
bids: ReadonlyArray<ContractBid>,
setSelectedBid: (bid: ContractBid, selected: boolean) => void
}
export const BidSelector = ({ bids, setSelectedBid }: BidSelectorProps) => {
const contractBidsEnabledByLevel =
pipe(levels,
RA.map(level => pipe(strains,
RA.map(strain => ({ level, strain }) as ContractBid),
RA.map(bid => [bid.strain, pipe(bids, RA.exists(selectedBid => eqContractBid.equals(bid, selectedBid)))] as const),
RR.fromEntries)))
return (<StrainTableView
table={{rows: contractBidsEnabledByLevel}}
renderColHeader={() => undefined}
renderRowHeader={() => undefined}
renderCell={(selected, s, i) =>
<BidSelectorButton selected={selected} setSelected={selected => setSelectedBid({ level: i + 1, strain: s}, selected)}>
<>{i + 1}<StrainSpan className={s} /></>
</BidSelectorButton>}
/>)
}

export default BidSelector
48 changes: 48 additions & 0 deletions src/components/core/StrainTableView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import styled from 'styled-components';
import { readonlyRecord as RR } from 'fp-ts';
import { Strain, strains } from '../../model/bridge';

const suitBase = `
&.S::before { content: "♠"; color: #0000FF }
&.H::before { content: "♥"; color: #FF0000 }
&.D::before { content: "♦"; color: #FFA500 }
&.C::before { content: "♣"; color: #32CD32 }
`

export const StrainSpan = styled.span `
${suitBase}
&.N::before { content: "NT"; color: #000000; font-size: 12px; }
`

type StrainRow<T> = RR.ReadonlyRecord<Strain, T>
export interface StrainTable<T> {
rows: ReadonlyArray<StrainRow<T>>
}

interface StrainTableProps<T> {
table: StrainTable<T>
renderColHeader: ((strain: Strain, index: number) => JSX.Element | undefined)
renderRowHeader: (row: StrainRow<T>, index: number) => JSX.Element | undefined
renderCell: (value: T, strain: Strain, rowIndex: number) => JSX.Element | undefined
}

export const StrainTableView = <T extends any>({ table, renderColHeader, renderRowHeader, renderCell }: StrainTableProps<T>) => {
return (
<table>
<thead>
<tr>
<th></th>
{strains.map((s, i) => renderColHeader ? renderColHeader(s, i) : <th style={{fontWeight: "normal", verticalAlign: "middle"}} key={i}><StrainSpan className={s} /></th>)}
</tr>
</thead>
<tbody>
{table.rows.map((r, i) => <tr key={i}>
<td>{renderRowHeader(r, i)}</td>
{strains.map((s, j) => <td key={j}>{renderCell(r[s], s, i)}</td>)}
</tr>)}
</tbody>
</table>
)
}

export default StrainTableView
57 changes: 57 additions & 0 deletions src/components/stats/ScoreComparison.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { ContractBid, Direction, eqContractBid, ordContractBid } from "../../model/bridge";
import { readonlyArray as RA, readonlyNonEmptyArray as RNEA } from "fp-ts";
import { Scores, compareScores } from "../../model/stats"
import Percentage from "../core/Percentage";
import BidSelector from "../core/BidSelector";
import { useCallback, useMemo, useState } from "react";
import { pipe } from "fp-ts/lib/function";
import styled from "styled-components";
import { BidView } from "../core/BidPath";
import { serializedBidL } from "../../model/serialization";

interface ScoreComparisonProps {
contractBid: ContractBid
scores: Scores
}
const defaultDir : Direction = "N"

export const Columns = styled.div `
display: flex;
flex-direction: row;
`

export const Column = styled.div `
width: 35%;
`

export const ScoreList = styled.ul `
list-style-type: none;
`

const ScoreComparison = ({ contractBid, scores }: ScoreComparisonProps) => {
const [bids, setBids] = useState<RNEA.ReadonlyNonEmptyArray<ContractBid>>([contractBid])
const setSelectedBid = useCallback((bid: ContractBid, selected: boolean) =>
setBids(pipe(
bids,
selected ? RA.union(eqContractBid)([bid]) : RA.filter(b => !eqContractBid.equals(bid, b)),
RA.append(contractBid),
RNEA.uniq(eqContractBid),
RNEA.sort(ordContractBid)))
, [bids, contractBid])
const comparison = useMemo(() => compareScores(scores)(pipe(bids, RNEA.map(b => ([defaultDir, b])))), [bids, scores])
const length = scores.length
return (<section>
<h4>Compare Contracts</h4>
<Columns>
<BidSelector bids={bids} setSelectedBid={setSelectedBid} />
<ScoreList>{length && Object.entries(comparison).map(([contract, count], i) =>
<li key={i}>
{contract === "tie" ? "(ties)" : <BidView bid={serializedBidL.reverseGet(contract)} />}
: {count} (<Percentage numerator={count} denominator={length} />)
</li>)}
</ScoreList>
</Columns>
</section>)
}

export default ScoreComparison
73 changes: 53 additions & 20 deletions src/components/stats/StatsDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,66 @@
import { option as O, readonlyRecord as RR } from 'fp-ts';
import { flow, pipe } from 'fp-ts/lib/function';
import { get } from '../../lib/object';
import {
option as O,
readonlyRecord as RR,
readonlyNonEmptyArray as RNEA,
} from "fp-ts";
import { flow, pipe } from "fp-ts/lib/function";
import { get } from "../../lib/object";
import { Generation } from "../../model/job";
import { SerializedBidPath } from "../../model/serialization";
import {
SerializedBidPath,
serializedBidPathL,
} from "../../model/serialization";
import SolutionStats from "./SolutionStats";
import ScoreComparison from "./ScoreComparison";
import { ContractBid } from "../../model/bridge";
import styled from "styled-components";

export const Columns = styled.div`
display: flex;
flex-direction: row;
`;

export const Column = styled.div`
width: 35%;
`;

interface StatsDetailsProps {
path: SerializedBidPath
generation: Generation
onClose: () => void
path: SerializedBidPath;
generation: Generation;
onClose: () => void;
}
const StatsDetails = ({ path, generation, onClose }: StatsDetailsProps) => {
const stats = pipe(
generation,
O.fromNullable,
O.chain(flow(
get('solutionStats'),
RR.lookup(path))),
O.toNullable)
O.chain(flow(get("solutionStats"), RR.lookup(path))),
O.toNullable
);
const solveCount = pipe(
stats,
O.fromNullable,
O.map(get('count')),
O.chain(O.fromPredicate(len => len > 0)),
O.toNullable)
return (<div>
{solveCount && <h3>{solveCount} solutions found</h3>}
{stats && <SolutionStats stats={stats} />}
</div>)
}
O.map(get("count")),
O.chain(O.fromPredicate((len) => len > 0)),
O.toNullable
);
const contractBid = pipe(
path,
serializedBidPathL.reverseGet,
RNEA.last
) as ContractBid;
return (
<div>
{solveCount && <h3>{solveCount} solutions found</h3>}
<Columns>
<Column>{stats && <SolutionStats stats={stats} />}</Column>
<div>
{stats?.scores && (
<ScoreComparison contractBid={contractBid} scores={stats.scores} />
)}
</div>
</Columns>
</div>
);
};

export default StatsDetails
export default StatsDetails;
Loading

0 comments on commit 9d6c33a

Please sign in to comment.