Skip to content

Commit

Permalink
feat(spoils): add result history
Browse files Browse the repository at this point in the history
  • Loading branch information
angrybacon committed Oct 6, 2023
1 parent b513467 commit a84d6d0
Showing 1 changed file with 163 additions and 67 deletions.
230 changes: 163 additions & 67 deletions src/components/SpoilsCalculator/SpoilsCalculator.tsx
Original file line number Diff line number Diff line change
@@ -1,86 +1,182 @@
import { FunctionComponent, useState } from 'react';
import { Box, Button, TextField } from '@mui/material';
import { ChangeEvent, FunctionComponent, useEffect, useState } from 'react';
import {
Box,
Button,
Divider,
TextField,
Unstable_Grid2 as Grid,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography,
} from '@mui/material';

interface Input {
copies?: number;
deck?: number;
life?: number;
samples?: number;
}

interface Result {
average: string;
deathes: string;
id: number;
input: Input;
}

const INITIAL_INPUT: Input = { copies: 4, deck: 53, life: 20, samples: 10000 };

export const SpoilsCalculator: FunctionComponent<unknown> = () => {
const [deckSize, setDeckSize] = useState(0);
const [cardsLeft, setCardsLeft] = useState(0);
const [lifeTotal, setLifeTotal] = useState(0);
const [avgLifeLoss, setAvgLifeLoss] = useState(0);
const [deathPct, setDeathPct] = useState(0);
const [input, setInput] = useState(INITIAL_INPUT);
const [isDisabled, setIsDisabled] = useState(true);
const [results, setResults] = useState<Result[]>([]);

/** In-place Durstenfeld shuffle. */
const shuffle = (array: number[]): void => {
for (let i = array.length - 1; i > 0; i -= 1) {
const j = Math.floor(Math.random() * (i + 1));
const temp = array[i];
array[i] = array[j];
array[j] = temp;
useEffect(() => {
setIsDisabled(!Object.values(input).every(Boolean));
}, [input]);

const onChange =
(key: keyof Input) =>
({ target }: ChangeEvent<HTMLInputElement>) =>
setInput((previous) => ({
...previous,
[key]: parseInt(target.value, 10) || undefined,
}));

/**
* Return a randomized array of numbers corresponding to a randomized deck.
* Implements a simple Durstenfeld shuffle.
*/
const shuffle = (size: number): number[] => {
const deck = [...Array(size).keys()];
for (let left = deck.length - 1; left > 0; left -= 1) {
const right = Math.floor(Math.random() * (left + 1));
[deck[left], deck[right]] = [deck[right] as number, deck[left] as number];
}
return deck;
};

const calculate = (): void => {
const numIters = 10000;
let avg = 0.0;
let deathCounter = 0.0;
for (let i = 0; i < numIters; i += 1) {
let cardsSeen = 0;
let deathFlag = false;
const deck = [...Array(deckSize).keys()];
shuffle(deck);
while (cardsSeen < deckSize) {
cardsSeen += 1;
if (cardsSeen > lifeTotal) {
deathFlag = true;
const onCompute = (): void => {
if (input.copies && input.deck && input.life && input.samples) {
setIsDisabled(true);
let average = 0;
let deathes = 0;
for (let sample = 0; sample < input.samples; sample += 1) {
const deck = shuffle(input.deck);
let candidate = 0;
while (candidate < input.deck) {
if ((deck[candidate] as number) < input.copies) {
average += candidate;
break;
}
candidate += 1;
}
if (deck[cardsSeen] < cardsLeft) {
avg += cardsSeen;
break;
if (candidate >= input.life) {
deathes += 1;
}
}
if (deathFlag) {
deathCounter += 1;
}
const result = {
average: (average / input.samples).toLocaleString(undefined, {
minimumFractionDigits: 1,
maximumFractionDigits: 1,
}),
deathes: (deathes / input.samples).toLocaleString(undefined, {
minimumFractionDigits: 2,
style: 'percent',
}),
input,
};
setResults((previous) => [
{ ...result, id: previous.length },
...previous,
]);
setIsDisabled(false);
}
avg /= numIters;
setAvgLifeLoss(avg);
setDeathPct((deathCounter / numIters) * 100);
};

return (
<Box>
<TextField
label="Deck size"
id="deck-size"
value={deckSize}
onChange={(t) => setDeckSize(+t)}
/>
<TextField
label="Cards left"
id="cards-left"
value={cardsLeft}
onChange={(t) => setCardsLeft(+t)}
/>
<TextField
label="Life total"
id="life-total"
value={lifeTotal}
onChange={(t) => setLifeTotal(+t)}
/>
<Button variant="contained" onClick={calculate}>
<Grid container spacing={2}>
<Grid xs={6} sm={3}>
<TextField
fullWidth
label="Deck size"
onChange={onChange('deck')}
type="number"
value={input.deck}
/>
</Grid>
<Grid xs={6} sm={3}>
<TextField
fullWidth
label="Copies left"
onChange={onChange('copies')}
type="number"
value={input.copies}
/>
</Grid>
<Grid xs={6} sm={3}>
<TextField
fullWidth
label="Life total"
onChange={onChange('life')}
type="number"
value={input.life}
/>
</Grid>
<Grid xs={6} sm={3}>
<TextField
fullWidth
label="Samples"
onChange={onChange('samples')}
type="number"
value={input.samples}
/>
</Grid>
</Grid>
<Button
disableElevation
disabled={isDisabled}
fullWidth
onClick={onCompute}
sx={{ my: 3 }}
variant="contained"
>
Calculate
</Button>
<TextField
disabled
label="Avg. life loss"
id="avg-life-loss"
value={avgLifeLoss}
/>
<TextField
disabled
label="Death Percent"
id="death-pct"
value={deathPct}
/>
{results.length ? (
<TableContainer sx={({ mixins }) => mixins.barf}>
<Divider />
<Table>
<TableHead>
<TableRow>
<TableCell>Input</TableCell>
<TableCell>Average Life Loss</TableCell>
<TableCell>Death Likelihood</TableCell>
</TableRow>
</TableHead>
<TableBody>
{results.map(({ average, deathes, id, input }) => (
<TableRow key={id}>
<TableCell>
{input.copies} out of {input.deck} with {input.life} life
</TableCell>
<TableCell>{average}</TableCell>
<TableCell>{deathes}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
) : (
<Typography sx={{ color: 'text.secondary', fontStyle: 'italic' }}>
Input your board state and compute to see the success rate.
</Typography>
)}
</Box>
);
};

0 comments on commit a84d6d0

Please sign in to comment.