-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
b513467
commit a84d6d0
Showing
1 changed file
with
163 additions
and
67 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |