diff --git a/hooks/8s/index.ts b/hooks/8s/index.ts deleted file mode 100644 index 0c8416297..000000000 --- a/hooks/8s/index.ts +++ /dev/null @@ -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 }; -}; diff --git a/hooks/8s/helpers.ts b/hooks/team-splitter/helpers.ts similarity index 61% rename from hooks/8s/helpers.ts rename to hooks/team-splitter/helpers.ts index 99ed1fd8a..eaed4fb07 100644 --- a/hooks/8s/helpers.ts +++ b/hooks/team-splitter/helpers.ts @@ -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 = (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 ""; +}; diff --git a/hooks/team-splitter/index.ts b/hooks/team-splitter/index.ts new file mode 100644 index 000000000..27c025452 --- /dev/null +++ b/hooks/team-splitter/index.ts @@ -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 }; +}; diff --git a/hooks/8s/types.ts b/hooks/team-splitter/types.ts similarity index 55% rename from hooks/8s/types.ts rename to hooks/team-splitter/types.ts index d4dc8e43f..7058a39c3 100644 --- a/hooks/8s/types.ts +++ b/hooks/team-splitter/types.ts @@ -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; - -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 }; diff --git a/pages/8s.tsx b/pages/team-splitter.tsx similarity index 50% rename from pages/8s.tsx rename to pages/team-splitter.tsx index 98bc40bb5..bf0515b97 100644 --- a/pages/8s.tsx +++ b/pages/team-splitter.tsx @@ -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 ? ( <> - {new Array(8).fill(null).map((_, i) => { + {new Array(10).fill(null).map((_, i) => { return ( { - + + + Choose 2 players that should never be placed in the same team + (optional) + + + dispatch({ + type: "SET_AMOUNT_OF_ROUNDS_WITH_SAME_TEAMS", + amountOfRoundsWithSameTeams: Number(value), + }) + } + value={String(state.amountOfRoundsWithSameTeams)} + > + + {state.players + .filter((p) => p.name !== "") + .map((p) => ( + + dispatch({ + type: "SET_NO_PLACING_TO_SAME_TEAM", + id: p.id, + checked: e.target.checked, + }) + } + > + {p.name} + + ))} + + + + + + + {state.errorWithPlayers} + + ) : ( <> { > Bravo + + Spectators + {state.matches[state.matches.length - 1].alpha.map((player, i) => { return ( @@ -119,12 +184,19 @@ const EightsPage = () => { {state.matches[state.matches.length - 1].bravo[i]} + + {state.matches[state.matches.length - 1].spectators[i] ?? + ""} + ); })} + {state.matches.length > 1 ? ( + + ) : null} + { ratio: winLossRatio(player)[0], }))} /> + )} ); }; -export default EightsPage; +export default TeamSplitterPage; diff --git a/public/layout/team-splitter.png b/public/layout/team-splitter.png new file mode 100644 index 000000000..6a7d98a2c Binary files /dev/null and b/public/layout/team-splitter.png differ diff --git a/utils/constants.ts b/utils/constants.ts index ce5c15322..4077ba14b 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -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",