Implement a history-based matching algorythm
The bot will attempt to keep producing groups with entirely unique matches based on the full history of matches until it can't. It'll then step forward and ignore a week of history and try again, ignoring more history until no history is left
This commit is contained in:
parent
ed2375386b
commit
874a24dd1d
8 changed files with 388 additions and 104 deletions
147
matching_test.py
147
matching_test.py
|
@ -4,18 +4,147 @@
|
|||
import discord
|
||||
import pytest
|
||||
import matching
|
||||
import history
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
def test_protocols():
|
||||
"""Verify the protocols we're using match the discord ones"""
|
||||
assert isinstance(discord.Member, matching.Member)
|
||||
assert isinstance(discord.Guild, matching.Guild)
|
||||
assert isinstance(discord.Role, matching.Role)
|
||||
|
||||
|
||||
class TestMember():
|
||||
def __init__(self, id: int):
|
||||
self._id = id
|
||||
|
||||
@property
|
||||
def mention(self) -> str:
|
||||
return f"<@{self._id}>"
|
||||
|
||||
@property
|
||||
def id(self) -> int:
|
||||
return self._id
|
||||
|
||||
@id.setter
|
||||
def id(self, value):
|
||||
self._id = value
|
||||
|
||||
|
||||
@pytest.mark.parametrize("matchees, per_group", [
|
||||
([discord.Member.__new__(discord.Member)] * 100, 3),
|
||||
([discord.Member.__new__(discord.Member)] * 12, 5),
|
||||
([discord.Member.__new__(discord.Member)] * 11, 2),
|
||||
([discord.Member.__new__(discord.Member)] * 356, 8),
|
||||
# Simplest test possible
|
||||
([TestMember(1)], 1),
|
||||
|
||||
# More requested than we have
|
||||
([TestMember(1)], 2),
|
||||
|
||||
# A selection of hyper-simple checks to validate core functionality
|
||||
([TestMember(1)] * 100, 3),
|
||||
([TestMember(1)] * 12, 5),
|
||||
([TestMember(1)] * 11, 2),
|
||||
([TestMember(1)] * 356, 8),
|
||||
])
|
||||
def test_matchees_to_groups(matchees, per_group):
|
||||
def test_matchees_to_groups_no_history(matchees, per_group):
|
||||
"""Test simple group matching works"""
|
||||
groups = matching.members_to_groups(matchees, per_group)
|
||||
hist = history.History()
|
||||
core_validate_members_to_groups(matchees, hist, per_group)
|
||||
|
||||
|
||||
def items_found_in_lists(list_of_lists, items):
|
||||
"""validates if any sets of items are found in individual lists"""
|
||||
for sublist in list_of_lists:
|
||||
if all(item in sublist for item in items):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("history_data, matchees, per_group, checks", [
|
||||
# Slightly more difficult test
|
||||
# Describe a history where we previously matched up some people and ensure they don't get rematched
|
||||
(
|
||||
[
|
||||
{
|
||||
"ts": datetime.now() - timedelta(days=1),
|
||||
"groups": [
|
||||
[TestMember(1), TestMember(2)],
|
||||
[TestMember(3), TestMember(4)],
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
TestMember(1),
|
||||
TestMember(2),
|
||||
TestMember(3),
|
||||
TestMember(4),
|
||||
],
|
||||
2,
|
||||
[
|
||||
lambda groups: not items_found_in_lists(
|
||||
groups, [TestMember(1), TestMember(2)]),
|
||||
lambda groups: not items_found_in_lists(
|
||||
groups, [TestMember(3), TestMember(4)])
|
||||
]
|
||||
),
|
||||
# Feed the system an "impossible" test
|
||||
# The function should fall back to ignoring history and still give us something
|
||||
(
|
||||
[
|
||||
{
|
||||
"ts": datetime.now() - timedelta(days=1),
|
||||
"groups": [
|
||||
[TestMember(1), TestMember(2), TestMember(3)],
|
||||
[TestMember(4), TestMember(5), TestMember(6)],
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
TestMember(1),
|
||||
TestMember(2),
|
||||
TestMember(3),
|
||||
TestMember(4),
|
||||
TestMember(5),
|
||||
TestMember(6),
|
||||
],
|
||||
3,
|
||||
[
|
||||
# Nothing specific to validate
|
||||
]
|
||||
),
|
||||
|
||||
])
|
||||
def test_matchees_to_groups_with_history(history_data, matchees, per_group, checks):
|
||||
"""Test simple group matching works"""
|
||||
hist = history.History()
|
||||
|
||||
# Replay the history
|
||||
for d in history_data:
|
||||
hist.log_groups_to_history(d["groups"], d["ts"])
|
||||
|
||||
groups = core_validate_members_to_groups(matchees, hist, per_group)
|
||||
|
||||
# Run the custom validate functions
|
||||
for check in checks:
|
||||
assert check(groups)
|
||||
|
||||
|
||||
def core_validate_members_to_groups(matchees: list[TestMember], hist: history.History, per_group: int):
|
||||
# Convert members to groups
|
||||
groups = matching.members_to_groups(matchees, hist, per_group)
|
||||
|
||||
# We should always have one group
|
||||
assert len(groups)
|
||||
|
||||
# Log the groups to history
|
||||
# This will validate the internals
|
||||
hist.log_groups_to_history(groups)
|
||||
|
||||
# Ensure each group contains within the bounds of expected members
|
||||
for group in groups:
|
||||
# Ensure the group contains the right number of members
|
||||
assert len(group) >= per_group
|
||||
assert len(group) < per_group*2
|
||||
if len(matchees) >= per_group:
|
||||
assert len(group) >= per_group
|
||||
else:
|
||||
assert len(group) == len(matchees)
|
||||
assert len(group) < per_group*2 # TODO: We could be more strict here
|
||||
|
||||
return groups
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue