diff --git a/src/components/core/BidSelector.tsx b/src/components/core/BidSelector.tsx new file mode 100644 index 0000000..ce2cd56 --- /dev/null +++ b/src/components/core/BidSelector.tsx @@ -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) => + + +interface BidSelectorProps { + bids: ReadonlyArray, + 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 ( undefined} + renderRowHeader={() => undefined} + renderCell={(selected, s, i) => + setSelectedBid({ level: i + 1, strain: s}, selected)}> + <>{i + 1} + } + />) +} + +export default BidSelector \ No newline at end of file diff --git a/src/components/core/StrainTableView.tsx b/src/components/core/StrainTableView.tsx new file mode 100644 index 0000000..f33f261 --- /dev/null +++ b/src/components/core/StrainTableView.tsx @@ -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 = RR.ReadonlyRecord +export interface StrainTable { + rows: ReadonlyArray> +} + +interface StrainTableProps { + table: StrainTable + renderColHeader: ((strain: Strain, index: number) => JSX.Element | undefined) + renderRowHeader: (row: StrainRow, index: number) => JSX.Element | undefined + renderCell: (value: T, strain: Strain, rowIndex: number) => JSX.Element | undefined +} + +export const StrainTableView = ({ table, renderColHeader, renderRowHeader, renderCell }: StrainTableProps) => { + return ( + + + + + {strains.map((s, i) => renderColHeader ? renderColHeader(s, i) : )} + + + + {table.rows.map((r, i) => + + {strains.map((s, j) => )} + )} + +
{renderRowHeader(r, i)}{renderCell(r[s], s, i)}
+ ) +} + +export default StrainTableView \ No newline at end of file diff --git a/src/components/stats/ScoreComparison.tsx b/src/components/stats/ScoreComparison.tsx new file mode 100644 index 0000000..b7a5853 --- /dev/null +++ b/src/components/stats/ScoreComparison.tsx @@ -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>([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 (
+

Compare Contracts

+ + + {length && Object.entries(comparison).map(([contract, count], i) => +
  • + {contract === "tie" ? "(ties)" : } + : {count} () +
  • )} +
    +
    +
    ) +} + +export default ScoreComparison \ No newline at end of file diff --git a/src/components/stats/StatsDetails.tsx b/src/components/stats/StatsDetails.tsx index 64849ca..502de99 100644 --- a/src/components/stats/StatsDetails.tsx +++ b/src/components/stats/StatsDetails.tsx @@ -1,9 +1,28 @@ -import { option as O, readonlyRecord as RR } from "fp-ts"; +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; @@ -24,10 +43,22 @@ const StatsDetails = ({ path, generation, onClose }: StatsDetailsProps) => { O.chain(O.fromPredicate((len) => len > 0)), O.toNullable ); + const contractBid = pipe( + path, + serializedBidPathL.reverseGet, + RNEA.last + ) as ContractBid; return (
    {solveCount &&

    {solveCount} solutions found

    } - {stats && } + + {stats && } +
    + {stats?.scores && ( + + )} +
    +
    ); }; diff --git a/src/model/analyze.ts b/src/model/analyze.ts index bc1599b..9c483b7 100644 --- a/src/model/analyze.ts +++ b/src/model/analyze.ts @@ -9,6 +9,7 @@ import { readonlyTuple, refinement, semigroup, + eq, } from "fp-ts"; import { constant, flow, pipe } from "fp-ts/lib/function"; import { ordAscending, ordDescending } from "../lib"; @@ -17,6 +18,7 @@ import { contractBids, ContractModifier, Direction, + eqContractBid, getIsVulnerable, ordContractBid, Strain, @@ -74,7 +76,11 @@ export const transpose = ( ) ); -type OptimalBid = ContractBid | "Pass"; +export type OptimalBid = ContractBid | "Pass"; +export const eqOptimalBid: eq.Eq = { + equals: (a, b) => + (!(a === "Pass" || b === "Pass") && eqContractBid.equals(a, b)) || a === b, +}; const initialContractBid: refinement.Refinement = ( b ): b is ContractBid => b !== "Pass"; @@ -83,10 +89,9 @@ const initialBids = pipe( readonlyArray.sort(ordContractBid), readonlyArray.prepend("Pass") ); -const ordInitialBidsAscending = ordAscending(initialBids); const ordInitialBidsDescending = ordDescending(initialBids); -type ContractScorePair = readonly [OptimalBid, Score]; +export type ContractScorePair = readonly [OptimalBid, Score]; const getDirectionScores = (counts: TrickCountsByStrain) => (isVulnerable: boolean) => pipe( @@ -110,8 +115,8 @@ const getDirectionScores = ) ); -const getAllScores = - (table: TrickCountsByDirectionThenStrain) => (vulnerability: Vulnerability) => +export const getAllScores = + (vulnerability: Vulnerability) => (table: TrickCountsByDirectionThenStrain) => pipe( table, RR.mapWithIndex((direction, counts) => diff --git a/src/model/bridge.ts b/src/model/bridge.ts index c9f94dc..a68d7b8 100644 --- a/src/model/bridge.ts +++ b/src/model/bridge.ts @@ -144,9 +144,10 @@ export const ordContractBid: ord.Ord = ord ord.contramap((c) => c.strain) ) ); +export const levels = RNEA.makeBy((level) => level + 1)(7); export const contractBids: ReadonlyArray = pipe( apply.sequenceS(RA.Apply)({ - level: RA.makeBy(7, (level) => level + 1), + level: levels, strain: strains, }), RA.sort(ordContractBid) diff --git a/src/model/stats.ts b/src/model/stats.ts index 85ef14b..4876424 100644 --- a/src/model/stats.ts +++ b/src/model/stats.ts @@ -5,15 +5,27 @@ import { readonlyNonEmptyArray as RNEA, readonlyRecord as RR, readonlyTuple, - semigroup, + semigroup as S, + ord, + number, + tuple, } from "fp-ts"; import { sequenceT } from "fp-ts/lib/Apply"; import { flow, pipe } from "fp-ts/lib/function"; import * as iso from "monocle-ts/Iso"; import { DoubleDummyTable } from "../workers/dds.worker"; -import { flattenNestedCounts } from "./analyze"; +import { + ContractScorePair, + OptimalBid, + eqOptimalBid, + flattenNestedCounts, + getAllScores, + transpose, +} from "./analyze"; import { Direction, directions, Strain, strains } from "./bridge"; +import { Score, eqScore } from "./score"; +import { serializedContractBidL } from "./serialization"; const SerializedKeyL: iso.Iso = iso.iso( ([d, s]) => d + s, @@ -59,20 +71,116 @@ const toRow = (ddt: DoubleDummyTable) => ({ outerKey: direction, innerKey: strain, value: count }) => [`${direction}${strain}`, count] as const ), - RR.fromFoldable(semigroup.first(), RA.Foldable) + RR.fromFoldable(S.first(), RA.Foldable) + ); + +export type Scores = RNEA.ReadonlyNonEmptyArray< + RR.ReadonlyRecord> +>; + +export const getScores = ( + ddt: RNEA.ReadonlyNonEmptyArray +): Scores => + pipe( + ddt, + RNEA.map(flow(transpose, getAllScores("Neither"))) + // aq.from, + // ct => ct.count(), + // ct => ct.object() as RR.ReadonlyRecord + ); + +type BidScorePair = readonly [string, Score]; +export type ScoreCompare = RR.ReadonlyRecord | { tie: number }; + +const ordScore: ord.Ord = ord.reverse( + ord.fromCompare((a, b) => number.Ord.compare(a, b)) +); +const ordScores: ord.Ord = pipe( + ordScore, + ord.contramap(([_, score]) => score) +); + +const tallyScore = (results: ReadonlyArray): ScoreCompare => { + const orderedScores = pipe(results, RA.sort(ordScores), (x) => x); + if ( + !orderedScores.length || + (orderedScores.length > 1 && + eqScore.equals(orderedScores[0][1], orderedScores[1][1])) + ) { + return { tie: 1 }; + } else { + return { [orderedScores[0][0]]: 1 }; + } +}; +const sumCompare: S.Semigroup = RR.getUnionSemigroup( + number.SemigroupSum +); +const zeroCompare = (bids: RNEA.ReadonlyNonEmptyArray): ScoreCompare => + pipe( + bids, + RNEA.map((b: string) => [b, 0] as const), + RR.fromEntries ); +const serializeOptimalBid = (bid: OptimalBid): string => + bid === "Pass" ? "Pass" : serializedContractBidL.get(bid); + +export const compareScores = + (scores: Scores) => + (dirsAndBids: RNEA.ReadonlyNonEmptyArray<[Direction, OptimalBid]>) => + pipe( + scores, + RNEA.map((score) => + pipe( + dirsAndBids, + RNEA.map(([dir, bid]) => + pipe( + score[dir], + RA.filter(([contract]) => + bid === "Pass" + ? contract === "Pass" + : eqOptimalBid.equals(bid, contract) + ), + RNEA.fromReadonlyArray, + O.fold( + () => ({}), + RNEA.groupBy(([contract]) => serializeOptimalBid(contract)) + ), + RR.map((x) => x[0]), + RR.map(([_, score]) => score) + ) + ), + RNEA.map(RR.toReadonlyArray), + RA.chain((x) => x), + tallyScore + ) + ), + RNEA.foldMap(sumCompare)((x: ScoreCompare) => x), + (s) => + sumCompare.concat( + zeroCompare( + pipe(dirsAndBids, RNEA.map(flow(tuple.snd, serializeOptimalBid))) + ), + s + ) + ); + export interface Stats { count: number; + scores: Scores; average: DoubleDummyTable; stdev: DoubleDummyTable; } -export const getStats = flow( - RNEA.map(toRow), - aq.from, - (ct): Stats => ({ - count: ct.totalRows(), - average: pipe(ct, aggregate(aq.op.mean)), - stdev: pipe(ct, aggregate(aq.op.stdev)), - }) -); + +export const getStats = (ddt: RNEA.ReadonlyNonEmptyArray) => + pipe( + ddt, + RNEA.map(toRow), + aq.from, + (ct): Stats => ({ + count: ct.totalRows(), + scores: pipe(ddt, getScores), + average: pipe(ct, aggregate(aq.op.mean)), + stdev: pipe(ct, aggregate(aq.op.stdev)), + }) + ); diff --git a/src/reducers/profile.ts b/src/reducers/profile.ts index 973b092..1fb204c 100644 --- a/src/reducers/profile.ts +++ b/src/reducers/profile.ts @@ -161,7 +161,7 @@ const slice = createSlice({ } o.generation.solutionStats[ solveJob.details.context.bidPath - ] = stats; + ] = castDraft(stats); } ) )