Displaying Chip value contribution #6 #33

Merged
djwesty merged 10 commits from vutukuri15/6 into main 2025-02-25 11:00:20 -08:00
5 changed files with 235 additions and 61 deletions

View File

@ -6,9 +6,17 @@ import ChipsSelector from "@/components/ChipsSelector";
import ChipDistributionSummary from "@/components/ChipDistributionSummary";
import ChipDetection from "@/components/ChipDetection";
export enum COLORS {
"white",
"red",
"green",
"blue",
"black",
}
const IndexScreen: React.FC = () => {
const [playerCount, setPlayerCount] = useState(2);
const [buyInAmount, setBuyInAmount] = useState<number | null>(null);
const [buyInAmount, setBuyInAmount] = useState<number>(20);
const [numberOfChips, setNumberOfChips] = useState<number>(5);
const [totalChipsCount, setTotalChipsCount] = useState<number[]>([]);

View File

@ -9,7 +9,7 @@ import {
import { MaterialIcons } from "@expo/vector-icons";
interface BuyInSelectorProps {
setBuyInAmount: React.Dispatch<React.SetStateAction<number | null>>;
setBuyInAmount: React.Dispatch<React.SetStateAction<number>>;
}
const defaultBuyInOptions = [10, 25, 50];
@ -26,8 +26,8 @@ const BuyInSelector: React.FC<BuyInSelectorProps> = ({ setBuyInAmount }) => {
setBuyInAmount(numericValue);
} else {
setCustomAmount("");
setBuyInAmountState(null);
setBuyInAmount(null);
setBuyInAmountState(25);
setBuyInAmount(25);
}
};

View File

@ -1,65 +1,198 @@
import React, { useEffect, useMemo, useState } from "react";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { View, Text, StyleSheet } from "react-native";
import { ColorValue } from "react-native";
interface ChipDistributionSummaryProps {
playerCount: number;
buyInAmount: number | null;
buyInAmount: number;
totalChipsCount: number[];
colors?: ColorValue[];
}
const MAX_CHIPS = 500;
const ChipDistributionSummary = ({
playerCount,
buyInAmount,
totalChipsCount,
colors = ["white", "red", "green", "blue", "black"],
}: ChipDistributionSummaryProps) => {
const [chipDistribution, setChipDistribution] = useState<number[]>([]);
const validDenominations: validDenomination[] = [
0.05, 0.1, 0.25, 0.5, 1, 2, 2.5, 5, 10, 20, 50, 100,
];
const [denominations, setDenominations] = useState<validDenomination[]>([]);
const [distributions, setDistributions] = useState<number[]>([]);
useEffect(() => {
if (buyInAmount !== null && playerCount > 0) {
let totalChips = totalChipsCount.reduce((sum, count) => sum + count, 0);
type validDenomination =
| 0.05
| 0.1
| 0.25
| 0.5
| 1
| 2
| 2.5
| 5
| 10
| 20
| 50
| 100;
if (totalChips > MAX_CHIPS) {
const scaleFactor = MAX_CHIPS / totalChips;
totalChipsCount = totalChipsCount.map((count) =>
Math.floor(count * scaleFactor)
);
totalChips = MAX_CHIPS;
}
const distribution = totalChipsCount.map((chipCount) =>
Math.floor(chipCount / playerCount)
);
// Return the closest (but lower) valid denomination to the target
const findFloorDenomination = (target: number): validDenomination => {
let current: validDenomination = validDenominations[0];
validDenominations.forEach((value, index) => {
if (value < target) current = value;
});
return current;
};
setChipDistribution(distribution);
// Bound for the value of the highest chip
// This is somewhat arbitray, but 1/3 to 1/4 is reasonable depending on the number of colors.
const maxDenomination = useMemo(() => {
if (totalChipsCount.length > 3) {
return findFloorDenomination(buyInAmount / 3);
} else {
setChipDistribution([]);
return findFloorDenomination(buyInAmount / 4);
}
}, [buyInAmount, playerCount, totalChipsCount]);
}, [totalChipsCount]);
const hasValidDistribution = useMemo(
() =>
buyInAmount !== null && playerCount > 0 && chipDistribution.length > 0,
[buyInAmount, playerCount, chipDistribution]
// Total value of the pot
const potValue = useMemo(
() => buyInAmount * playerCount,
[buyInAmount, playerCount]
);
// The total value of all chips distributed to a single player. Ideally should be equal to buyInAmount
const totalValue = useMemo(() => {
let value = 0;
for (let i = 0; i < totalChipsCount.length; i++) {
value += distributions[i] * denominations[i];
}
return value;
}, [distributions, denominations]);
// Maximum quantity of each chip color which may be distributed to a single player before running out
const maxPossibleDistribution = useMemo(
() => totalChipsCount.map((v) => Math.floor(v / playerCount)),
[totalChipsCount, playerCount]
);
// Redenominate the chips in case of failure to properly distribute.
// Move the shuffle index to the next lowest denomination, and keep all else same
const redenominate = useCallback(
(
invalidDenomination: validDenomination[],
shuffleIndex: number
): validDenomination[] => {
let moved = false;
const newDenomination: validDenomination[] = [];
for (let i = invalidDenomination.length - 1; i >= 0; i--) {
if (i > shuffleIndex) {
newDenomination.push(invalidDenomination[i]);
} else if (i == shuffleIndex) {
newDenomination.push(invalidDenomination[i]);
} else if (i < shuffleIndex && !moved) {
const nextLowestDenominationIndex = Math.max(
validDenominations.indexOf(invalidDenomination[i]) - 1,
0
);
newDenomination.push(validDenominations[nextLowestDenominationIndex]);
moved = true;
} else {
newDenomination.push(invalidDenomination[i]);
}
}
newDenomination.reverse();
return newDenomination;
},
[]
);
// Dynamically set denominations and distributions from changing inputs
useEffect(() => {
let testDenomination: validDenomination[] = [];
const numColors = totalChipsCount.length;
const testDistribution: number[] = [];
for (let i = 0; i < numColors; ++i) {
testDistribution.push(0);
}
// Start with max denominations, then push on the next adjacent lower denomination
testDenomination.push(maxDenomination);
let currentDenominationIndex: number =
validDenominations.indexOf(maxDenomination);
for (let i = numColors - 2; i >= 0; i = i - 1) {
currentDenominationIndex -= 1;
const currentDemoniation = validDenominations[currentDenominationIndex];
testDenomination.push(currentDemoniation);
}
testDenomination.reverse();
// Distribute the chips using the test denomination
// If distribution fails to equal the buy-in, redenominate and re-try
// Algorithm could be improved with more complexity and optimization
let remainingValue = buyInAmount;
let fail = true;
let failCount = 0;
while (fail && failCount < 1) {
let stop = false;
while (remainingValue > 0 && !stop) {
let distributed = false;
for (let i = numColors - 1; i >= 0; i = i - 1) {
if (testDistribution[i] < maxPossibleDistribution[i]) {
if (remainingValue >= testDenomination[i]) {
testDistribution[i] = testDistribution[i] + 1;
remainingValue = remainingValue - testDenomination[i];
distributed = true;
}
}
}
if (distributed == false) {
stop = true;
}
}
if (remainingValue !== 0) {
const redenominateIndex = failCount % numColors;
testDenomination = redenominate(testDenomination, redenominateIndex);
failCount += 1;
fail = true;
} else {
fail = false;
}
}
setDenominations(testDenomination);
setDistributions(testDistribution);
}, [totalChipsCount, maxDenomination, buyInAmount, playerCount]);
return (
<View style={styles.container}>
<Text style={styles.title}>Chip Distribution Summary:</Text>
{hasValidDistribution ? (
chipDistribution.map((count, index) => (
<Text key={index} style={[styles.chipText, { color: colors[index] }]}>
{`${colors[index]?.toString().toUpperCase()} Chips: ${count} per player`}
</Text>
))
) : (
<Text style={styles.noDataText}>
No valid distribution calculated yet.
</Text>
)}
<Text style={styles.title}>Distribution & Denomination</Text>
<Text style={styles.subTitle}>${potValue} Pot</Text>
<View style={styles.chipContainer}>
{totalChipsCount.map((_, index) => (
<View style={styles.chipRow} key={index}>
<Text
style={{
...styles.chipText,
color: colors[index],
...(colors[index] === "white" && styles.whiteShadow),
}}
>
{distributions[index]} chips:
</Text>
<Text
style={{
...styles.chipText,
color: colors[index],
...(colors[index] === "white" && styles.whiteShadow),
}}
>
${denominations[index]} each
</Text>
</View>
))}
</View>
<Text style={styles.chipText}>Total Value:{totalValue}</Text>
</View>
);
};
@ -68,17 +201,39 @@ const styles = StyleSheet.create({
container: {
marginTop: 20,
padding: 15,
backgroundColor: "#F8F9FA",
borderRadius: 10,
display: "flex",
alignItems: "center",
},
title: {
fontSize: 18,
fontWeight: "bold",
marginBottom: 10,
},
chipText: {
subTitle: {
fontSize: 16,
color: "gray",
fontWeight: "bold",
marginBottom: 10,
},
chipContainer: {
marginTop: 10,
},
chipRow: {
display: "flex",
flexDirection: "row",
justifyContent: "center",
gap: 10,
},
chipText: {
fontSize: 20,
marginVertical: 2,
fontWeight: "bold",
},
whiteShadow: {
textShadowColor: "black",
textShadowOffset: { width: 0, height: 0 },
textShadowRadius: 10,
},
noDataText: {
fontSize: 16,

View File

@ -54,7 +54,7 @@ describe("BuyInSelector Component", () => {
fireEvent.changeText(getByPlaceholderText("Enter custom buy-in"), "-10");
expect(setBuyInAmount).toHaveBeenCalledWith(null);
expect(setBuyInAmount).toHaveBeenCalledWith(25);
});
it("clears the custom amount when selecting a predefined option", () => {
@ -80,9 +80,9 @@ describe("BuyInSelector Component", () => {
expect(setBuyInAmount).toHaveBeenCalledWith(75);
fireEvent.changeText(getByPlaceholderText("Enter custom buy-in"), "-5");
expect(setBuyInAmount).toHaveBeenCalledWith(null);
expect(setBuyInAmount).toHaveBeenCalledWith(25);
fireEvent.changeText(getByPlaceholderText("Enter custom buy-in"), "abc");
expect(setBuyInAmount).toHaveBeenCalledWith(null);
expect(setBuyInAmount).toHaveBeenCalledWith(25);
});
});

View File

@ -5,35 +5,45 @@ import ChipDistributionSummary from "../ChipDistributionSummary";
describe("ChipDistributionSummary Component", () => {
test("renders correctly with valid data", () => {
const playerCount = 4;
const totalChipsCount = [100, 200, 300, 400, 500];
const totalChipsCount = [100, 80, 60, 40, 20];
const colors = ["WHITE", "RED", "GREEN", "BLUE", "BLACK"];
const buyInAmount = 20;
// Update this to match the actual component's chip distribution logic
const expectedDistribution = [8, 16, 25, 33, 41]; // Adjust based on actual component calculations
const expectedDistribution = [2, 2, 1, 2, 2];
const expectedDenominations = [0.5, 1, 2, 2.5, 5];
const { getByText } = render(
const { getByText, getAllByText } = render(
<ChipDistributionSummary
playerCount={playerCount}
buyInAmount={100}
buyInAmount={buyInAmount}
totalChipsCount={totalChipsCount}
/>
);
expect(getByText("Chip Distribution Summary:")).toBeTruthy();
expect(getByText("Distribution & Denomination")).toBeTruthy();
expectedDistribution.forEach((count, index) => {
expect(getByText(new RegExp(`${colors[index]} Chips: ${count} per player`, "i"))).toBeTruthy();
expect(getAllByText(new RegExp(`${count} chips:`, "i"))).toBeTruthy();
expect(
getByText(new RegExp(`\\$${expectedDenominations[index]} each`, "i"))
).toBeTruthy();
});
});
test("renders fallback message when no valid distribution", () => {
// Case not currently supported
test.skip("renders fallback message when no valid distribution", () => {
const { getByText } = render(
<ChipDistributionSummary playerCount={0} buyInAmount={null} totalChipsCount={[]} />
<ChipDistributionSummary
playerCount={0}
buyInAmount={20}
totalChipsCount={[]}
/>
);
expect(getByText("No valid distribution calculated yet.")).toBeTruthy();
});
test("scales down chips if exceeding MAX_CHIPS", () => {
// Case not currently supported
test.skip("scales down chips if exceeding MAX_CHIPS", () => {
const playerCount = 2;
let totalChipsCount = [300, 400, 500, 600, 700];
const MAX_CHIPS = 500;
@ -41,11 +51,12 @@ describe("ChipDistributionSummary Component", () => {
if (totalChips > MAX_CHIPS) {
const scaleFactor = MAX_CHIPS / totalChips;
totalChipsCount = totalChipsCount.map(count => Math.round(count * scaleFactor));
totalChipsCount = totalChipsCount.map((count) =>
Math.round(count * scaleFactor)
);
}
const expectedDistribution = [30, 40, 50, 60, 70]; // Adjust to match actual component calculations
const colors = ["WHITE", "RED", "GREEN", "BLUE", "BLACK"];
const { getByText } = render(
<ChipDistributionSummary
@ -55,11 +66,11 @@ describe("ChipDistributionSummary Component", () => {
/>
);
expect(getByText("Chip Distribution Summary:")).toBeTruthy();
expect(getByText("Distribution & Denomination")).toBeTruthy();
expectedDistribution.forEach((count, index) => {
expect(getByText(new RegExp(`${colors[index]} Chips: ${count} per player`, "i"))).toBeTruthy();
expect(getByText(new RegExp(`${count} chips:`, "i"))).toBeTruthy();
// expect(getByText(new RegExp(`$${count} each`, "i"))).toBeTruthy();
});
});
});