Compare commits

..

7 Commits

Author SHA1 Message Date
d78e07f513 option: "unique first names" and "relative/total" 2024-11-04 10:46:21 +01:00
fc073de1de fix json types 2024-11-02 14:48:27 +01:00
1d945977dc add git repo 2024-11-02 14:48:05 +01:00
1fda84bdef relative score is fairer to players with fewer prefs 2024-11-02 14:47:27 +01:00
6cd253d5f0 prepare for live updates 2024-11-02 09:49:49 +01:00
97e16e6be2 go through possible teams with brute force 2024-11-02 09:48:17 +01:00
a444c7efd3 add table with suggested teams 2024-11-02 09:47:25 +01:00
4 changed files with 290 additions and 4 deletions

119
brute_force.py Normal file
View File

@@ -0,0 +1,119 @@
import numpy as np
import json
import itertools
from pathlib import Path
rgen = np.random.default_rng(seed=42)
def load_stats():
db = Path("local_team.db")
players_list = "prefs_page/src/players.json"
with open(players_list, "r") as f:
players = json.load(f)
preferences = {}
for line in open(db, "r"):
date, person, prefs = line.split("\t")
if not person.strip() or not prefs.strip():
continue
preferences[person] = [p.strip() for p in prefs.split(",")]
for player in players:
if player not in preferences:
preferences[player] = []
return players, preferences
# synthetical data
# rgen = np.random.default_rng(seed=42)
# n_prefs = 8
# preferences = {
# player: rgen.choice(players, size=n_prefs, replace=False) for player in players
# }
def team_table_json():
players, preferences = load_stats()
best = apply_brute_force(players, preferences)
data = {k: {} for k in best}
for k, v in best.items():
mean, team0, team1 = v[0]
overall_matches = 0
overall_preference_statements = 0
for i, team in enumerate([team0, team1]):
tablename = f"Team {i+1}"
data[k][tablename] = []
for p in sorted(list(team)):
prefs = preferences[p]
matches = sum([pref in team for pref in preferences[p]])
data[k][tablename].append([p, matches, len(prefs)])
overall_matches += matches
overall_preference_statements += len(prefs)
# data[k]["overall_matches"] = overall_matches
# data[k]["overall_preference_statements"] = overall_preference_statements
with open("prefs_page/src/table.json", "w") as f:
json.dump(data, f)
def unique_names(team):
"""check if first names are unique"""
return len(set(team)) == len(set([p.split()[0] for p in team]))
def apply_brute_force(players, preferences):
def evaluate_teams(team0, team1):
scores = []
percentages = []
for team in [team0, team1]:
for p in team:
scores.append(sum([pref in team for pref in preferences[p]]))
if len(preferences[p]) > 0:
percentages.append(scores[-1] / len(preferences[p]))
return np.mean(scores), np.mean(percentages) * 100
best = {
f"{i},{j}": [(0, [], [])]
for i, j in itertools.product(["total", "relative"], ["unique", "non-unique"])
}
for team0 in itertools.combinations(players, 9):
team1 = {player for player in players if player not in team0}
score, percentage = evaluate_teams(team0, team1)
for k, v in best.items():
if k.startswith("total"):
meassure = score
elif k.startswith("relative"):
meassure = percentage
if meassure > best[k][0][0]:
if k.endswith(",unique") and not (
unique_names(team0) and unique_names(team1)
):
continue
best[k] = [(meassure, team0, team1)]
elif meassure == best[k][0][0] and set(team0) != set(best[k][0][1]):
if k.endswith(",unique") and not (
unique_names(team0) and unique_names(team1)
):
continue
best[k].append((meassure, team0, team1))
if __name__ == "__main__":
for k, v in best.items():
print("##", k)
for result in v:
print(result[0])
print(sorted(result[1]))
print(sorted(result[2]))
print()
# team_table(score, team0, team1)
return best
if __name__ == "__main__":
team_table_json()

View File

@@ -1,5 +1,5 @@
#root { #root {
max-width: 60vw; max-width: 70vw;
margin: 0 auto; margin: 0 auto;
padding: 2rem; padding: 2rem;
text-align: center; text-align: center;
@@ -11,4 +11,5 @@
.read-the-docs { .read-the-docs {
color: #888; color: #888;
padding: 0;
} }

View File

@@ -17,6 +17,7 @@ import {
FormHelperText, FormHelperText,
Stack, Stack,
} from "@mui/material"; } from "@mui/material";
import TeamTables from "./Table";
function getStyles(name: string, players: readonly string[], theme: Theme) { function getStyles(name: string, players: readonly string[], theme: Theme) {
return { return {
@@ -29,7 +30,8 @@ function getStyles(name: string, players: readonly string[], theme: Theme) {
async function submit( async function submit(
person: string, person: string,
players: string[], players: string[],
setResponseStatus: (value: number) => void setResponseStatus: (value: number) => void,
setDialog: (value: boolean) => void
) { ) {
// console.log(JSON.stringify({ person: person, players: players })); // console.log(JSON.stringify({ person: person, players: players }));
const response = await fetch("https://0124816.xyz/team/submit/", { const response = await fetch("https://0124816.xyz/team/submit/", {
@@ -40,6 +42,7 @@ async function submit(
body: JSON.stringify({ person: person, players: players }), body: JSON.stringify({ person: person, players: players }),
}); });
setResponseStatus(response.status); setResponseStatus(response.status);
setDialog(true);
} }
type SubmitButtonProps = { type SubmitButtonProps = {
@@ -49,17 +52,20 @@ type SubmitButtonProps = {
function SubmitButton(props: SubmitButtonProps) { function SubmitButton(props: SubmitButtonProps) {
const [responseStatus, setResponseStatus] = React.useState(0); const [responseStatus, setResponseStatus] = React.useState(0);
const [dialog, setDialog] = React.useState(false);
return ( return (
<div> <div>
<Button <Button
variant="contained" variant="contained"
color={responseStatus === 200 ? "success" : "primary"} color={responseStatus === 200 ? "success" : "primary"}
onClick={() => submit(props.person, props.players, setResponseStatus)} onClick={() =>
submit(props.person, props.players, setResponseStatus, setDialog)
}
> >
submit submit
</Button> </Button>
<Dialog open={responseStatus === 200}> <Dialog onClose={() => setDialog(false)} open={dialog}>
<DialogTitle>thank you. please leave now.</DialogTitle> <DialogTitle>thank you. please leave now.</DialogTitle>
</Dialog> </Dialog>
</div> </div>
@@ -194,8 +200,17 @@ function App() {
{SubmitButton({ person, players })} {SubmitButton({ person, players })}
<p>now: click submit.</p> <p>now: click submit.</p>
</div> </div>
<p className="read-the-docs">
suggested teams are not updated right away. don't try to hack the
algorithm ;D
</p>
{TeamTables()}
<p className="read-the-docs"> <p className="read-the-docs">
something not working? message <a href="https://t.me/x0124816">me</a>. something not working? message <a href="https://t.me/x0124816">me</a>.
sources:{" "}
<a href="https://git.0124816.xyz/julius/teambuilding" key="gitea">
<img src="gitea.svg" alt="gitea" height="20" />
</a>
</p> </p>
</> </>
); );

151
prefs_page/src/Table.tsx Normal file
View File

@@ -0,0 +1,151 @@
import { useState } from "react";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import Paper from "@mui/material/Paper";
import data from "./table.json";
import {
Box,
Checkbox,
FormControlLabel,
Stack,
Switch,
Typography,
} from "@mui/material";
interface Data {
[index: string]: Teams;
}
interface Teams {
teamname: Row;
}
type Row = [player: string, fulfilled: number, total: number];
function RenderTable(data: object) {
let nMatches: number = 0;
let nTotal: number = 0;
let nPlayers: number = 0;
let relativeScore: number = 0.0;
for (const rows of Object.values(data)) {
for (const row of rows) {
nMatches += row[1];
nTotal += row[2];
relativeScore += row[1] / row[2];
nPlayers++;
}
}
return (
<>
<Paper elevation={2} sx={{ p: 2, display: "inline-block" }}>
average wishes fulfilled:{" "}
<Typography sx={{ fontFamily: "monospace" }}>
{(nMatches / nPlayers).toPrecision(2)} (
{((relativeScore / nPlayers) * 100).toPrecision(3)}%)
</Typography>
</Paper>
{Object.entries(data).map(([teamname, rows]) => (
<>
<h1>{teamname}</h1>
<TableContainer component={Paper}>
<Table
sx={{ m: "8px auto" }}
size="small"
aria-label="simple table"
>
<TableHead>
<TableRow>
<TableCell>player</TableCell>
<TableCell>wishes fulfilled</TableCell>
</TableRow>
</TableHead>
<TableBody>
{(rows as Row[]).map((row: Row) => (
<TableRow
key={row[0]}
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
>
<TableCell key={row[0] + "-cell"}>{row[0]}</TableCell>
<TableCell key={row[0] + "-cell-2"}>
{"♥️".repeat(row[1]) + "♡".repeat(row[2] - row[1])}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</>
))}
</>
);
}
export default function TeamTables() {
const [total, setTotal] = useState("relative");
const [unique, setUnique] = useState("non-unique");
const handleTotalChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
setTotal("relative");
} else {
setTotal("total");
}
};
const handleUniqueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
setUnique("unique");
} else {
setUnique("non-unique");
}
};
let tabledata: Data = data as unknown as Data;
return (
<>
<Box
sx={{
m: "auto",
display: "flex",
flexWrap: "wrap",
justifyContent: "center",
}}
>
<Paper elevation={2} sx={{ m: 1, p: 1 }}>
<Typography className="read-the-docs">according to</Typography>
<Stack
direction="row"
spacing={1}
sx={{ p: "0 16px", m: "0 8px", alignItems: "center" }}
>
<Typography>total</Typography>
<Switch defaultChecked onChange={handleTotalChange} />
<Typography>relative</Typography>
</Stack>
<Typography className="read-the-docs">
number of wishes fulfilled
</Typography>
</Paper>
<Paper elevation={2} sx={{ m: 1, p: 1, justifyContent: "center" }}>
<FormControlLabel
sx={{ padding: 2, m: 1 }}
control={
<Checkbox
checked={unique === "unique"}
onChange={handleUniqueChange}
/>
}
label="unique first names"
/>
</Paper>
</Box>
{RenderTable(tabledata[[total, unique].join(",") as string])}
</>
);
}