Team Splitter first public version

This commit is contained in:
Kalle (Sendou)
2021-06-12 20:52:13 +03:00
parent 449012e3fb
commit fd24c22a28
7 changed files with 393 additions and 211 deletions

View File

@@ -1,162 +0,0 @@
import { useReducer } from "react";
import { shuffleArray } from "utils/arrays";
import { createNewMatch, teamsShouldChange } from "./helpers";
const MU_DEFAULT_VALUE = 0;
const SIGMA_DEFAULT_VALUE = 0;
const DEFAULT_AMOUNT_OF_ROUNDS_WITH_SAME_TEAMS = 2;
export const useEightsPage = () => {
const [state, dispatch] = useReducer(
(oldState: State, action: Action) => {
switch (action.type) {
case "SET_PLAYER":
const newPlayers = [...oldState.players];
newPlayers[action.number - 1] = {
...newPlayers[action.number - 1],
name: action.name,
};
return { ...oldState, players: newPlayers as State["players"] };
case "SET_AMOUNT_OF_ROUNDS_WITH_SAME_TEAMS":
return {
...oldState,
amountOfRoundsWithSameTeams: action.amountOfRoundsWithSameTeams,
};
case "CREATE_FIRST_MATCH":
const newMatch = createNewMatch({
players: oldState.players,
previousMatches: oldState.matches,
sittingOutCounts: new Map(),
});
window.scrollTo(0, 0);
return { ...oldState, matches: [...oldState.matches, newMatch] };
case "SET_WINNER":
const matches = [...oldState.matches];
matches[matches.length - 1].winner = action.winner;
const alphaPlayersSet = new Set(matches[matches.length - 1].alpha);
const bravoPlayersSet = new Set(matches[matches.length - 1].bravo);
const players = [...oldState.players].map((player) => {
let winCount = player.winCount;
if (alphaPlayersSet.has(player.name) && action.winner === "alpha")
winCount++;
else if (
bravoPlayersSet.has(player.name) &&
action.winner === "bravo"
)
winCount++;
let lossCount = player.lossCount;
if (alphaPlayersSet.has(player.name) && action.winner === "bravo")
lossCount++;
else if (
bravoPlayersSet.has(player.name) &&
action.winner === "alpha"
)
lossCount++;
return {
...player,
winCount,
lossCount,
};
});
const newMatchAfterSettingWinner = teamsShouldChange({
amountOfRoundsWithSameTeams: oldState.amountOfRoundsWithSameTeams,
matchesLength: oldState.matches.length,
})
? createNewMatch({
players,
previousMatches: matches,
sittingOutCounts: new Map(),
})
: {
...oldState.matches[oldState.matches.length - 1],
winner: undefined,
};
matches.push(newMatchAfterSettingWinner);
return { ...oldState, matches, players };
default:
return oldState;
}
},
{
players: [
{
id: 1,
name: "Sendou",
mu: MU_DEFAULT_VALUE,
sigma: SIGMA_DEFAULT_VALUE,
winCount: 0,
lossCount: 0,
},
{
id: 2,
name: "Kiver",
mu: MU_DEFAULT_VALUE,
sigma: SIGMA_DEFAULT_VALUE,
winCount: 0,
lossCount: 0,
},
{
id: 3,
name: "Brian",
mu: MU_DEFAULT_VALUE,
sigma: SIGMA_DEFAULT_VALUE,
winCount: 0,
lossCount: 0,
},
{
id: 4,
name: "Sorin",
mu: MU_DEFAULT_VALUE,
sigma: SIGMA_DEFAULT_VALUE,
winCount: 0,
lossCount: 0,
},
{
id: 5,
name: "DUDE",
mu: MU_DEFAULT_VALUE,
sigma: SIGMA_DEFAULT_VALUE,
winCount: 0,
lossCount: 0,
},
{
id: 6,
name: "Grey",
mu: MU_DEFAULT_VALUE,
sigma: SIGMA_DEFAULT_VALUE,
winCount: 0,
lossCount: 0,
},
{
id: 7,
name: "plontro",
mu: MU_DEFAULT_VALUE,
sigma: SIGMA_DEFAULT_VALUE,
winCount: 0,
lossCount: 0,
},
{
id: 8,
name: "Kaldemar",
mu: MU_DEFAULT_VALUE,
sigma: SIGMA_DEFAULT_VALUE,
winCount: 0,
lossCount: 0,
},
],
matches: [],
amountOfRoundsWithSameTeams: DEFAULT_AMOUNT_OF_ROUNDS_WITH_SAME_TEAMS,
}
);
return { state, dispatch };
};

View File

@@ -1,16 +1,29 @@
import { shuffleArray } from "utils/arrays";
const choosePlayersToSitOut = ({
players,
sittingOutCounts,
}: {
players: Players;
sittingOutCounts: SittingOutCounts;
}): PlayersTeam => {
if (players.length === 8) return players as PlayersTeam;
const choosePlayersToSitOut = (
players: Player[]
): [playingRoster: Player[], spectators: Player[]] => {
if (players.length === 8) return [players, []];
// TODO: cut down to 8
return players as PlayersTeam;
console.log("players", players);
const playersCopySorted = [...players].sort(
(a, b) => a.sittingOutCount - b.sittingOutCount
);
const playingRoster: Player[] = [];
const spectators: Player[] = [];
for (const [i, player] of playersCopySorted.entries()) {
if (i === 0) spectators.push(player);
else if (i === 1 && playersCopySorted.length === 10) {
spectators.push(player);
} else {
playingRoster.push(player);
}
}
return [playingRoster, spectators];
};
const getAllSubsetsOfSizeFour = <T>(theArray: T[]): T[][] =>
@@ -66,8 +79,6 @@ const removeTeamsThatAlreadyPlayed = ({
}): Player[][] => {
const result: Player[][] = [];
console.log("previousMatches", previousMatches);
for (const team of teams) {
let isNewTeam = true;
const teamSortedStringified = team
@@ -92,6 +103,39 @@ const removeTeamsThatAlreadyPlayed = ({
return result;
};
const removeTeamsWherePlayersShouldNotBeInTheSameTeam = ({
teams,
noPlacingToSameTeam,
players,
}: {
teams: Player[][];
noPlacingToSameTeam: [playerOneId?: number, playerTwoId?: number];
players: Player[];
}): Player[][] => {
if (noPlacingToSameTeam.length !== 2) return teams;
// Check if one or both of the unpreferred players are spectating
if (
players.reduce(
(count, player) =>
noPlacingToSameTeam.includes(player.id) ? count + 1 : count,
0
) !== 2
) {
return teams;
}
// If count is not one it means the unpreferred players are either both in this team (count 2)
// or in the other team (count 2)
return teams.filter(
(t) =>
t.reduce(
(count, player) =>
noPlacingToSameTeam.includes(player.id) ? count + 1 : count,
0
) === 1
);
};
const selectAlphaTeamOfBestProjectedMatchUp = ({
teams,
players,
@@ -135,19 +179,24 @@ const selectAlphaTeamOfBestProjectedMatchUp = ({
export const createNewMatch = ({
players,
previousMatches,
sittingOutCounts,
noPlacingToSameTeam,
}: {
players: Players;
players: Player[];
previousMatches: Match[];
sittingOutCounts: SittingOutCounts;
noPlacingToSameTeam: [playerOneId?: number, playerTwoId?: number];
}): Match => {
// TODO: who sits out has to also be recorded
const playersForMatch = shuffleArray(
choosePlayersToSitOut({ players, sittingOutCounts })
const [playersForMatch, spectators] = choosePlayersToSitOut(
shuffleArray(players)
);
let teams = getAllSubsetsOfSizeFour(playersForMatch);
teams = removeDuplicates({ teams, players: playersForMatch });
teams = removeTeamsThatAlreadyPlayed({ teams, previousMatches });
teams = removeTeamsWherePlayersShouldNotBeInTheSameTeam({
teams,
noPlacingToSameTeam,
players: playersForMatch,
});
const alpha = selectAlphaTeamOfBestProjectedMatchUp({
teams,
@@ -156,7 +205,7 @@ export const createNewMatch = ({
const bravo = playersForMatch
.filter((player) => !alpha.includes(player.name))
.map((p) => p.name);
return { alpha, bravo };
return { alpha, bravo, spectators: spectators.map((s) => s.name) };
};
export const teamsShouldChange = ({
@@ -168,3 +217,16 @@ export const teamsShouldChange = ({
}) => {
return matchesLength % amountOfRoundsWithSameTeams === 0;
};
export const errorWithPlayers = (players: Player[]) => {
const playerNames = players
.map((p) => p.name.trim())
.filter((name) => name !== "");
if (playerNames.length < 8) return "Please enter at least 8 player names";
const uniqueNames = new Set(playerNames.map((name) => name.toUpperCase()));
if (uniqueNames.size !== playerNames.length)
return "Every name has to be unique";
return "";
};

View File

@@ -0,0 +1,171 @@
import { useEffect, useReducer } from "react";
import { createNewMatch, teamsShouldChange, errorWithPlayers } from "./helpers";
const DEFAULT_AMOUNT_OF_ROUNDS_WITH_SAME_TEAMS = 2;
const getInitialState = (ignoreLocalStorage: boolean = false): State => {
const stateFromLocalStorage = JSON.parse(
window.localStorage.getItem("team-splitter-state") ?? "{}"
);
if (
!ignoreLocalStorage &&
stateFromLocalStorage &&
Object.keys(stateFromLocalStorage).length > 0
) {
return stateFromLocalStorage;
}
return {
players: new Array(10).fill(null).map((_, i) => ({
id: i + 1,
name: "",
winCount: 0,
lossCount: 0,
sittingOutCount: 0,
})),
matches: [],
amountOfRoundsWithSameTeams: DEFAULT_AMOUNT_OF_ROUNDS_WITH_SAME_TEAMS,
errorWithPlayers: "",
noPlacingToSameTeam: [],
};
};
export const useTeamSplitterPage = () => {
const [state, dispatch] = useReducer((oldState: State, action: Action) => {
switch (action.type) {
case "SET_PLAYER":
const newPlayers = [...oldState.players];
newPlayers[action.number - 1] = {
...newPlayers[action.number - 1],
name: action.name,
};
return {
...oldState,
players: newPlayers as State["players"],
errorWithPlayers: errorWithPlayers(newPlayers),
};
case "SET_NO_PLACING_TO_SAME_TEAM":
return {
...oldState,
noPlacingToSameTeam: action.checked
? oldState.noPlacingToSameTeam.concat(action.id)
: (oldState.noPlacingToSameTeam.filter(
(id) => id !== action.id
) as any),
};
case "SET_AMOUNT_OF_ROUNDS_WITH_SAME_TEAMS":
return {
...oldState,
amountOfRoundsWithSameTeams: action.amountOfRoundsWithSameTeams,
};
case "CREATE_FIRST_MATCH":
const playersCleanedUp = oldState.players
.filter((p) => p.name !== "")
.map((p) => ({ ...p, name: p.name.trim() }));
const newMatch = createNewMatch({
players: playersCleanedUp,
previousMatches: oldState.matches,
noPlacingToSameTeam: oldState.noPlacingToSameTeam,
});
window.scrollTo(0, 0);
return {
...oldState,
matches: [...oldState.matches, newMatch],
players: playersCleanedUp,
};
case "SET_WINNER":
const matches = [...oldState.matches];
matches[matches.length - 1].winner = action.winner;
const alphaPlayersSet = new Set(matches[matches.length - 1].alpha);
const bravoPlayersSet = new Set(matches[matches.length - 1].bravo);
const players = [...oldState.players].map((player) => {
let winCount = player.winCount;
if (
!alphaPlayersSet.has(player.name) &&
!bravoPlayersSet.has(player.name)
) {
return { ...player, sittingOutCount: player.sittingOutCount + 1 };
}
if (alphaPlayersSet.has(player.name) && action.winner === "alpha")
winCount++;
else if (
bravoPlayersSet.has(player.name) &&
action.winner === "bravo"
)
winCount++;
let lossCount = player.lossCount;
if (alphaPlayersSet.has(player.name) && action.winner === "bravo")
lossCount++;
else if (
bravoPlayersSet.has(player.name) &&
action.winner === "alpha"
)
lossCount++;
return {
...player,
winCount,
lossCount,
};
});
const newMatchAfterSettingWinner = teamsShouldChange({
amountOfRoundsWithSameTeams: oldState.amountOfRoundsWithSameTeams,
matchesLength: oldState.matches.length,
})
? createNewMatch({
players,
previousMatches: matches,
noPlacingToSameTeam: oldState.noPlacingToSameTeam,
})
: {
...oldState.matches[oldState.matches.length - 1],
winner: undefined,
};
matches.push(newMatchAfterSettingWinner);
return { ...oldState, matches, players };
case "UNDO_LATEST_MATCH":
// oldState.matches[oldState.matches.length-1] is the match that is currently in progress
const lastFinishedMatch = oldState.matches[oldState.matches.length - 2];
const adjustedPlayers = oldState.players.map((p) => {
if (lastFinishedMatch.spectators.includes(p.name)) {
return { ...p, sittingOutCount: p.sittingOutCount - 1 };
}
if (lastFinishedMatch[lastFinishedMatch.winner!].includes(p.name)) {
return { ...p, winCount: p.winCount - 1 };
}
return { ...p, lossCount: p.lossCount - 1 };
});
const adjustedMatches = oldState.matches
.slice(0, oldState.matches.length - 1)
.map((match, i, arr) =>
i === arr.length - 1 ? { ...match, winner: undefined } : match
);
return {
...oldState,
players: adjustedPlayers,
matches: adjustedMatches,
};
case "RESET":
window.scroll(0, 0);
return getInitialState(true);
default:
return oldState;
}
}, getInitialState());
useEffect(() => {
window.localStorage.setItem("team-splitter-state", JSON.stringify(state));
}, [state]);
return { state, dispatch };
};

View File

@@ -1,33 +1,24 @@
interface Player {
id: number;
name: string;
mu: number;
sigma: number;
winCount: number;
lossCount: number;
sittingOutCount: number;
}
type Players = Player[];
type PlayersTeam = [
Player,
Player,
Player,
Player,
Player,
Player,
Player,
Player
];
type SittingOutCounts = Map<string, number>;
type Match = { alpha: string[]; bravo: string[]; winner?: "alpha" | "bravo" };
type Match = {
alpha: string[];
bravo: string[];
spectators: string[];
winner?: "alpha" | "bravo";
};
interface State {
players: Players;
players: Player[];
matches: Match[];
amountOfRoundsWithSameTeams: number;
errorWithPlayers: string;
noPlacingToSameTeam: [playerOneId?: number, playerTwoId?: number];
}
// TODO: names must be unique
@@ -43,4 +34,7 @@ type Action =
| {
type: "SET_AMOUNT_OF_ROUNDS_WITH_SAME_TEAMS";
amountOfRoundsWithSameTeams: number;
};
}
| { type: "UNDO_LATEST_MATCH" }
| { type: "RESET" }
| { type: "SET_NO_PLACING_TO_SAME_TEAM"; id: number; checked: boolean };

View File

@@ -1,9 +1,10 @@
import { Button } from "@chakra-ui/button";
import { Checkbox } from "@chakra-ui/checkbox";
import { Input } from "@chakra-ui/input";
import { Box, Grid, Stack, Text } from "@chakra-ui/layout";
import { Box, Flex, Grid, Stack, Text } from "@chakra-ui/layout";
import { Radio, RadioGroup } from "@chakra-ui/radio";
import NewTable from "components/common/NewTable";
import { useEightsPage } from "hooks/8s";
import { useTeamSplitterPage } from "hooks/team-splitter";
import { Fragment } from "react";
const winLossRatio = ({
@@ -21,15 +22,15 @@ const winLossRatio = ({
return [`${prefix}${ratio}`, ratio];
};
const EightsPage = () => {
const { state, dispatch } = useEightsPage();
const TeamSplitterPage = () => {
const { state, dispatch } = useTeamSplitterPage();
return (
<>
{state.matches.length === 0 ? (
<>
<Box maxW={80}>
{new Array(8).fill(null).map((_, i) => {
{new Array(10).fill(null).map((_, i) => {
return (
<Box key={i} my={4}>
<Text
@@ -83,17 +84,73 @@ const EightsPage = () => {
</Stack>
</RadioGroup>
</Box>
<Button onClick={() => dispatch({ type: "CREATE_FIRST_MATCH" })}>
Start
</Button>
<Box my={6}>
<Text
as="label"
display="block"
mb="8px"
fontSize="sm"
fontWeight="bold"
>
Choose 2 players that should never be placed in the same team
(optional)
</Text>
<RadioGroup
onChange={(value) =>
dispatch({
type: "SET_AMOUNT_OF_ROUNDS_WITH_SAME_TEAMS",
amountOfRoundsWithSameTeams: Number(value),
})
}
value={String(state.amountOfRoundsWithSameTeams)}
>
<Flex flexWrap="wrap">
{state.players
.filter((p) => p.name !== "")
.map((p) => (
<Checkbox
mr={4}
key={p.id}
isChecked={state.noPlacingToSameTeam.includes(p.id)}
isDisabled={
!state.noPlacingToSameTeam.includes(p.id) &&
state.noPlacingToSameTeam.length === 2
}
onChange={(e) =>
dispatch({
type: "SET_NO_PLACING_TO_SAME_TEAM",
id: p.id,
checked: e.target.checked,
})
}
>
{p.name}
</Checkbox>
))}
</Flex>
</RadioGroup>
</Box>
<Flex alignItems="center">
<Button
onClick={() => dispatch({ type: "CREATE_FIRST_MATCH" })}
disabled={Boolean(state.errorWithPlayers)}
mr={4}
>
Start
</Button>
<Text fontSize="sm" color="red.500">
{state.errorWithPlayers}
</Text>
</Flex>
</>
) : (
<>
<Grid
gridTemplateColumns="1fr 1fr"
maxW={80}
gridTemplateColumns="1fr 1fr 1fr"
maxW="32rem"
gridColumnGap="2rem"
gridRowGap="0.5rem"
mx="auto"
mb={8}
>
<Text
@@ -112,6 +169,14 @@ const EightsPage = () => {
>
Bravo
</Text>
<Text
fontSize="sm"
fontWeight="bold"
borderBottom="2px solid"
borderColor="gray.500"
>
Spectators
</Text>
{state.matches[state.matches.length - 1].alpha.map((player, i) => {
return (
<Fragment key={player}>
@@ -119,12 +184,19 @@ const EightsPage = () => {
<Text>
{state.matches[state.matches.length - 1].bravo[i]}
</Text>
<Text>
{state.matches[state.matches.length - 1].spectators[i] ??
""}
</Text>
</Fragment>
);
})}
<Button
size="sm"
backgroundColor="#F63778"
fontSize="xs"
_hover={{ backgroundColor: "#F63778" }}
_active={{ backgroundColor: "#F63778" }}
onClick={() => dispatch({ type: "SET_WINNER", winner: "alpha" })}
>
Alpha won
@@ -132,11 +204,44 @@ const EightsPage = () => {
<Button
size="sm"
backgroundColor="#01E262"
fontSize="xs"
_hover={{ backgroundColor: "#01E262" }}
_active={{ backgroundColor: "#01E262" }}
onClick={() => dispatch({ type: "SET_WINNER", winner: "bravo" })}
>
Bravo won
</Button>
{state.matches.length > 1 ? (
<Button
size="sm"
fontSize="xs"
onClick={() => dispatch({ type: "UNDO_LATEST_MATCH" })}
colorScheme="gray"
>
Undo last
</Button>
) : null}
</Grid>
<Button
my={4}
size="sm"
variant="ghost"
onClick={() => {
navigator.clipboard.writeText(
state.players
.sort((a, b) => winLossRatio(b)[1] - winLossRatio(a)[1])
.map(
(player) =>
`${player.name} - ${winLossRatio(player)[0]} (${
player.winCount
}W/${player.lossCount}L)`
)
.join("\n")
);
}}
>
Copy leaderboards to clipboard
</Button>
<NewTable
caption="Leaderboards"
headers={[
@@ -155,10 +260,21 @@ const EightsPage = () => {
ratio: winLossRatio(player)[0],
}))}
/>
<Button
display="block"
size="sm"
mx="auto"
mt={6}
variant="outline"
colorScheme="red"
onClick={() => dispatch({ type: "RESET" })}
>
Reset
</Button>
</>
)}
</>
);
};
export default EightsPage;
export default TeamSplitterPage;

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -47,6 +47,7 @@ export const navItems: {
{ code: "t", name: "Teams" },
{ code: "plans", name: "Plans" },
{ code: "maps", name: "Map Lists" },
{ code: "team-splitter", name: "Team Splitter" },
{
code: "plus",
name: "Plus Server",