From c6a279a703ddffc6241d7b9d4945c85ec232fd3c Mon Sep 17 00:00:00 2001 From: Jack Jackson Date: Mon, 18 Nov 2024 19:47:40 -0800 Subject: [PATCH] Support ties --- DEVELOPMENT.md | 7 + app/elo/elo.py | 7 +- app/routers/games.py | 12 +- app/routers/seed.py | 121 +++++++++++----- app/sql/models.py | 5 + app/sql/schemas.py | 1 + seed-data/all-in-one-with-tied-games.csv | 169 +++++++++++++++++++++++ tests/elo/test_elo.py | 9 +- tests/test_fresh_db_tests.py | 61 +++++++- 9 files changed, 345 insertions(+), 47 deletions(-) create mode 100644 seed-data/all-in-one-with-tied-games.csv diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 87d7c76..36ff6d3 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -29,3 +29,10 @@ $ kubectl -n edh-elo exec -it edh-elo-postgresql-0 -- psql -U db_user -d postgr ``` $ docker cp edh-elo-server-1:/app/database/ ``` + +# To add a new column to a table + +* Edit the appropriate classes in `app/sql/models.py` and `app/sql/schemas.py` +* (If necessary) Update `app/routers/seed.py:all_in_one` + +(TODO - investigate use of [Alembic](https://alembic.sqlalchemy.org/) to model migrations) diff --git a/app/elo/elo.py b/app/elo/elo.py index 5f5b357..424157a 100644 --- a/app/elo/elo.py +++ b/app/elo/elo.py @@ -4,11 +4,14 @@ K_FACTOR = 10.0 BETA = 200 -def rerank(ratings: List[float], winning_player_idx: int) -> Iterable[float]: +def rerank(ratings: List[float], winning_player_idxs: Iterable[int]) -> Iterable[float]: expectations = _expectations(ratings) return [ float(rating) - + (K_FACTOR * ((1.0 if winning_player_idx == idx else 0.0) - expectations[idx])) + + ( + K_FACTOR + * ((1.0 if idx in winning_player_idxs else 0.0) - expectations[idx]) + ) for idx, rating in enumerate(ratings) ] diff --git a/app/routers/games.py b/app/routers/games.py index 57595e6..12f3560 100644 --- a/app/routers/games.py +++ b/app/routers/games.py @@ -50,9 +50,15 @@ def create_game(game: schemas.GameCreate, db: Session = Depends(get_db)): deck_scores_before_this_game = [ crud.get_latest_score_for_deck(db, deck_id) for deck_id in deck_ids ] - new_scores = rerank( - deck_scores_before_this_game, deck_ids.index(game.winning_deck_id) - ) + winning_deck_ids = [deck_ids.index(game.winning_deck_id)] + if game.other_winning_deck_ids: + winning_deck_ids.extend( + [ + deck_ids.index(other_winning_id) + for other_winning_id in game.other_winning_deck_ids.split(",") + ] + ) + new_scores = rerank(deck_scores_before_this_game, winning_deck_ids) for score, deck_id in zip(new_scores, deck_ids): db.add( models.EloScore(after_game_id=created_game.id, deck_id=deck_id, score=score) diff --git a/app/routers/seed.py b/app/routers/seed.py index 3494607..dcfcc8b 100644 --- a/app/routers/seed.py +++ b/app/routers/seed.py @@ -203,40 +203,93 @@ def all_in_one(file: UploadFile, db: Session = Depends(get_db)): # Note that we intentionally create via the API, not via direct `crud.create_game`, to trigger ELO calculation. - index_of_winning_deck = [ - row[f"Deck {i+1}"] == row["Winning Deck"] for i in range(6) - ].index(True) - print(f"DEBUG - checking row {row}") - created_game = create_game( - schemas.GameCreate( - date=date_of_current_row, - **{ - f"deck_id_{i+1}": deck_id_lookup[ - row[f"Player {i+1}"] + ":" + row[f"Deck {i+1}"] - ] - for i in range(6) - if row[f"Deck {i+1}"] - }, - winning_deck_id=deck_id_lookup[ - row[f"Player {index_of_winning_deck+1}"] - + ":" - + row[f"Deck {index_of_winning_deck+1}"] - ], - number_of_turns=int(row["# turns"]), - first_player_out_turn=row["turn 1st player out"], - win_type_id=[ - win_type.id - for win_type in win_types - if win_type.name == row["Type of win"] - ][0], - format_id=[ - format.id for format in formats if format.name == row["Format"] - ][0], - description=row["Notes"], - ), - db, - ) - LOGGER.info(f"Seeded {created_game=}") + if not row["Winning Deck"].startswith("Tie"): + print(f"DEBUG - checking row {row}") + index_of_winning_deck = [ + row[f"Deck {i+1}"] == row["Winning Deck"] for i in range(6) + ].index(True) + created_game = create_game( + schemas.GameCreate( + date=date_of_current_row, + **{ + f"deck_id_{i+1}": deck_id_lookup[ + row[f"Player {i+1}"] + ":" + row[f"Deck {i+1}"] + ] + for i in range(6) + if row[f"Deck {i+1}"] + }, + winning_deck_id=deck_id_lookup[ + row[f"Player {index_of_winning_deck+1}"] + + ":" + + row[f"Deck {index_of_winning_deck+1}"] + ], + number_of_turns=int(row["# turns"]), + first_player_out_turn=row["turn 1st player out"], + win_type_id=[ + win_type.id + for win_type in win_types + if win_type.name == row["Type of win"] + ][0], + format_id=[ + format.id for format in formats if format.name == row["Format"] + ][0], + description=row["Notes"], + ), + db, + ) + LOGGER.info(f"Seeded {created_game=}") + else: + # "Winning Deck" starts with the string `Tie` => the game was a tie + print(f"DEBUG - checking row {row}") + LOGGER.info("Checking a game with a tie!") + + winning_deck_names = row["Winning Deck"][5:-1].split("; ") + print(f"DEBUG - {winning_deck_names=}") + indices_of_winning_decks = [ + [row[f"Deck {i+1}"] == name for i in range(6)].index(True) + for name in winning_deck_names + ] + + created_game = create_game( + schemas.GameCreate( + date=date_of_current_row, + **{ + f"deck_id_{i+1}": deck_id_lookup[ + row[f"Player {i+1}"] + ":" + row[f"Deck {i+1}"] + ] + for i in range(6) + if row[f"Deck {i+1}"] + }, + winning_deck_id=deck_id_lookup[ + row[f"Player {indices_of_winning_decks[0]+1}"] + + ":" + + row[f"Deck {indices_of_winning_decks[0]+1}"] + ], + other_winning_deck_ids=",".join( + [ + str( + deck_id_lookup[ + row[f"Player {i+1}"] + ":" + row[f"Deck {i+1}"] + ] + ) + for i in indices_of_winning_decks[1:] + ] + ), + number_of_turns=int(row["# turns"]), + first_player_out_turn=row["turn 1st player out"], + win_type_id=[ + win_type.id + for win_type in win_types + if win_type.name == row["Type of win"] + ][0], + format_id=[ + format.id for format in formats if format.name == row["Format"] + ][0], + description=row["Notes"], + ), + db, + ) + LOGGER.info(f"Seeded {created_game=}") return "Ok!" diff --git a/app/sql/models.py b/app/sql/models.py index 7de2bc6..a0ba15f 100644 --- a/app/sql/models.py +++ b/app/sql/models.py @@ -58,6 +58,11 @@ class Game(Base): format_id = Column(Integer, ForeignKey("formats.id"), nullable=False) description = Column(String) elo_scores: Mapped[List["EloScore"]] = relationship() + # In support of ties, we allow a Game to have a (comma-separated) set of (other) winning Deck Ids, all of which will + # be used for ELO calculation. + # Is this the best approach? Probably not! + # Does it work (without having to migrate existing data to a new format)? Yes! + other_winning_deck_ids = Column(String) class EloScore(Base): diff --git a/app/sql/schemas.py b/app/sql/schemas.py index 9344b3d..b56aba2 100644 --- a/app/sql/schemas.py +++ b/app/sql/schemas.py @@ -56,6 +56,7 @@ class GameBase(BaseModel): deck_id_5: Optional[int] = None deck_id_6: Optional[int] = None winning_deck_id: int + other_winning_deck_ids: Optional[str] = None number_of_turns: int first_player_out_turn: int win_type_id: int diff --git a/seed-data/all-in-one-with-tied-games.csv b/seed-data/all-in-one-with-tied-games.csv new file mode 100644 index 0000000..30e1ef8 --- /dev/null +++ b/seed-data/all-in-one-with-tied-games.csv @@ -0,0 +1,169 @@ +Date,Player 1,Deck 1,Player 2,Deck 2,Player 3,Deck 3,Player 4,Deck 4,Player 5,Deck 5,Player 6,Deck 6,Winning Player,Winning Deck,# turns,turn 1st player out,Type of win,Format,Notes +1/13/24,Evan,Kelsien the Plague,Terence,Ravos/Rebbec,Patrick,Duke Ulder Ravengard,Jeff,Mondrak,,,,,Terence,Ravos/Rebbec,15,12,combat damage,FFA,3-4 board wipes; Biotransference +1/13/24,Terence,Wernog/Cecily,Patrick,Duke Ulder Ravengard,Ryan,Don Andres,Jeff,Mondrak,Evan,Jasmine Boreal of the Seven,,,Patrick,Duke Ulder Ravengard,12,10,combat damage,FFA,Possessed Portal scary! +1/13/24,Jeff,Go-Shintai of Life's Origin,Evan,Kethis the Hidden Hand,Terence,Rograkh/Silas ninjas,Patrick,Tekuthal,Ryan,Gitrog,,,Patrick,Tekuthal,8,8,poison,FFA,Flux Channeler + Tekuthal +1/13/24,Patrick,Tekuthal,Jeff,Illuna,Evan,"Atraxa, Praetor's Voice",Terence,Muldrotha,,,,,Patrick,Tekuthal,20,5,combat damage,FFA,3 Hullbreaker Horrors simultaneously (Evan forfeited ~t5; game took ~2hrs) +1/17/24,Patrick,Duke Ulder Ravengard,Ryan,Grist,Terence,Ravos/Rebbec,,,,,,,Patrick,Duke Ulder Ravengard,12,12,combat damage,FFA,"Eternal Wanderer wipe, Angel Serenity + Sun Titan followup" +1/17/24,Terence,Ravos/Rebbec,Patrick,Duke Ulder Ravengard,Ryan,Grist,,,,,,,Terence,Ravos/Rebbec,13,11,combat damage,FFA,Verge Rangers + Conjurer's Mantle +1/23/24,Ryan,Goose Mother,Patrick,Maarika,Terence,Wilson/Cultist,,,,,,,Patrick,Maarika,8,7,21+ commander,FFA,Maarika + Runes of the Deus +1/23/24,Ryan,Goose Mother,Patrick,Maarika,Terence,Wilson/Cultist,,,,,,,Terence,Wilson/Cultist,10,8,combat damage,FFA,Scepter of Celebration brings an army +1/23/24,Terence,Abdel Adrian/Far Traveler,Ryan,Obeka,Patrick,Laelia,,,,,,,Patrick,Laelia,9,8,21+ commander,FFA,Nalfeshnee copying Storm's Wrath cleared the way; Ryan mana screwed +1/23/24,Terence,Jan Jansen,Ryan,Don Andres,Patrick,Laelia,,,,,,,Terence,Jan Jansen,10,10,aristocrats/burn,FFA,Mirkwood Bats + Thornbite Staff ftw; Ryan mana screwed +1/30/24,Patrick,Urza,Terence,"Me, the Immortal",Ryan,Gitrog,,,,,,,Ryan,Gitrog,8,7,combat damage,FFA,Mending of Dominaria + Lotus Cobra (10 lands) +1/30/24,Patrick,Kiora,Terence,Raffine,Ryan,Kozilek,,,,,,,Patrick,Kiora,11,9,combat damage,FFA,"Raffine does all the work, then Kederekt Leviathan cleans up" +2/4/24,Ajit,Jhoira of the Ghitu,Brandon,Anikthea,Patrick,Oops all Kayas,,,,,,,Brandon,Anikthea,12,11,combat damage,FFA,Unopposed enchantress with card draw and Nature's Will +2/4/24,Brandon,Silvar/Trynn,Patrick,Oops all Kayas,Ryan,"Purphoros, God of the Forge",Ajit,Jhoira of the Ghitu,,,,,Brandon,Silvar/Trynn,12,10,aristocrats/burn,FFA,Bastion of Remembrance overcame Emrakul + It That Betrays +2/4/24,Ajit,Brago,Brandon,Marneus Calgar,Patrick,Laelia,Ryan,Gitrog,,,,,Ryan,Gitrog,15,12,combat damage,FFA,Gitrog draws a zillion cards and closes w/ Multani +2/4/24,Terence,Slimefoot and Squee,Patrick,Ayara,Ryan,Omnath,,,,,,,Ryan,Omnath,8,8,combat damage,FFA,"Vorinclex, Voice of Hunger enables Crackle with Power (x=4) to overcome Army of the Damned" +2/20/24,Terence,Wilson/Cultist,Ryan,Gitrog,Patrick,Tekuthal,,,,,,,Terence,Wilson/Cultist,15,12,combat damage,FFA,Weatherlight and Ormendahl collect many tithes +2/20/24,Ryan,Myrel,Patrick,Tekuthal,Terence,Ravos/Rebbec,,,,,,,Patrick,Tekuthal,10,9,combat damage,FFA,Arcbound Crusher got chonky (w/ Sword of Truth & Justice) +2/20/24,Terence,Ravos/Rebbec,Ryan,Myrel,Patrick,Tekuthal,,,,,,,Terence,Ravos/Rebbec,10,9,combat damage,FFA,Ryan's Coat of Arms + Terence's Door of Destinies and Haunted One = math! +2/25/24,Patrick,Elmar storm,Jack J,Gale/Scion of Halaster,Terence,Kefnet the Mindful,Jeff,Mondrak,Ryan,Don Andres,,,Jeff,Mondrak,12,9,combat damage,FFA,Elesh Norn is a good finisher; Ryan achievement unlocked: control all other commanders at once +2/25/24,Terence,Rograkh/Silas ninjas,Jeff,Illuna,Ryan,Emiel,Patrick,Myrkul planeswalkers,Jack J,Syr Ginger,,,Terence,Rograkh/Silas ninjas,16,15,combat damage,FFA,Yuriko + ninja informant = overwhelming card advantage +2/25/24,Ryan,Omnath,Patrick,Rafiq,Terence,Yoshimaru/Reyhan,Jeff,Go-Shintai of Life's Origin,,,,,Jeff,Go-Shintai of Life's Origin,8,5,combat damage,FFA,Shrine of tapping stuff down bought just enough time +2/28/24,Terence,Dargo/Nadier,Ryan,Pantlaza,Patrick,Niv-Mizzet,,,,,,,Ryan,Pantlaza,10,9,combat damage,FFA,Hasty dinosaurs post-wrath with a Savage Order for Gishath chonks life totals +2/28/24,Ryan,Gitrog,Patrick,Niv-Mizzet,Terence,Dargo/Nadier,,,,,,,Patrick,Niv-Mizzet,9,9,combat damage,FFA,Faeburrow Elder enables quick acceleration; Case of the Shattered Pact is quick damage +2/28/24,Ryan,Gitrog,Patrick,Niv-Mizzet,Terence,Dargo/Nadier,,,,,,,Terence,Dargo/Nadier,8,5,combat damage,FFA,Dargo with Jeska 1-shot Ryan (and earned 27 impulse draws via Fire Giant's Fury - into Burnt Offering); Skull Storm precisely lethal +2/28/24,Ryan,"Liesa, Shroud of Dusk",Patrick,Duke Ulder Ravengard,Terence,Malcolm/Ich-Tekik,,,,,,,Patrick,Duke Ulder Ravengard,13,11,combat damage,FFA,Stealing Angel of Destiny and giving myriad 1-shot Ryan (while earning 60 life) +3/10/24,stranger,"Krenko, Tin Street Kingpin",Patrick,Tekuthal,stranger,Gishath,stranger,Ur-Dragon,,,,,stranger,Ur-Dragon,7,7,combat damage,FFA,"Master Warcraft with Atarka, Miirym, and 2 Ur-Dragons; poor threat analysis (2 friends didn't target each other)" +3/10/24,stranger,Gishath,stranger,Ur-Dragon,stranger,Korvold,Patrick,Dihada,,,,,Patrick,Dihada,10,8,combat damage,FFA,Heroes' Podium + Day of Destiny stacks quickly; Malik gets around Lightning Greaves (same 2 friends) +3/10/24,stranger,"Lazav, the Multifarious",stranger,"Mirri, Weatherlight Duelist",Patrick,Duke Ulder Ravengard,stranger,Anzrag,,,,,Patrick,Duke Ulder Ravengard,11,9,combat damage,FFA,Lazav cast an honest Eater of Days to block Anzrag buffed by Xenagos; Knight-Captain of Eos perma-fogged Anzrag to victory +3/10/24,stranger,"Brimaz, Blight of Oreskos",stranger,"The Master, Multiplied",stranger,"Lonis, Cryptozoologist",stranger,"Mirri, Weatherlight Duelist",Patrick,Tekuthal,,,stranger,"The Master, Multiplied",7,7,combat damage,FFA,unchecked Master Multiplied with ramp = 13 Masters attacking everyone; Brimaz + Tekuthal both mana screwed +3/10/24,stranger,"Brimaz, Blight of Oreskos",stranger,"The Master, Multiplied",stranger,"Lonis, Cryptozoologist",stranger,"Mirri, Weatherlight Duelist",Patrick,Tekuthal,,,stranger,"Brimaz, Blight of Oreskos",12,12,quality of life concede,FFA,Several board wipes + removal with life totals largely intact; dealt with Koma; Myojin of Seeing Winds drew 13 and could keep going but conceded +3/15/24,Terence,Atraxa win-cons,Ryan,Emiel,Patrick,Duke Ulder Ravengard,,,,,,,Patrick,Duke Ulder Ravengard,16,13,combat damage,FFA,Ghostway + wrath to slow down Emiel+Seedborn +3/15/24,Patrick,Niv-Mizzet,Terence,Yidris dredge,Ryan,Pantlaza,,,,,,,Terence,Yidris dredge,10,9,combat damage,FFA,"Living End claims another victim, even when scripted a turn ahead" +3/15/24,Ryan,Pantlaza,Patrick,Niv-Mizzet,Terence,Yidris dredge,,,,,,,Ryan,Pantlaza,12,12,combat damage,FFA,Ghalta discover trigger into Portal to Phyrexia (with Skullspore Nexus) +3/15/24,Ryan,Reyav,Patrick,Laelia,Terence,Yidris dredge,,,,,,,Terence,Yidris dredge,9,7,combat damage,FFA,Echoing Equation on Hogaak takes out Patrick; Narcomoeba attacking was the final damage to Ryan +3/30/24,Jack F,"Brenard, Ginger Sculptor",Terence,Ravos/Rebbec,Patrick,Duke Ulder Ravengard,Jeff,Mondrak,Brandon,The Wise Mothman,,,Jeff,Mondrak,11,8,combat damage,FFA,Anointed Procession + (destroyed: Cathar's Crusade + Starlight Spectacular) with multiple wraths overcame a LARGE Mothman and Brenard's army +3/30/24,Ajit,Brago,Patrick,Laelia,Jeff,Go-Shintai of Life's Origin,Brandon,"Liberty Prime, Recharged",Jack F,"Astor, Bearer of Blades",,,Ajit,Brago,14,9,combat damage,FFA,"Brago + Medomai turns ft. 21/21 Astor (RIP Patrick), 22/22 Danitha; 31/31 Laelia; Goblin Welder loops of Synth-Reflector Mages; 7+ shrines threatening w/ Zur" +4/10/24,Terence,Ishai/Tana,Ryan,Don Andres,Patrick,Tekuthal,,,,,,,Terence,Ishai/Tana,9,9,combat damage,FFA,Assemble the Legion + Felidar Retreat +4/10/24,Patrick,Tekuthal,Terence,Ishai/Tana,Ryan,Don Andres,,,,,,,Patrick,Tekuthal,10,10,poison,FFA,Don Andres cast Eternal Dominion +4/16/24,Ryan,Goose Mother,Terence,"Vorinclex, Monstrous Raider",Patrick,Maarika,,,,,,,Terence,"Vorinclex, Monstrous Raider",10,9,21+ commander,FFA,Vorinclex + Bone Saws backed up by hexproof +4/16/24,Terence,Kefnet the Mindful,Patrick,Maarika,Ryan,Goose Mother,,,,,,,Terence,Kefnet the Mindful,10,9,21+ commander,FFA,Kefnet wears a Robe of the Archmagi and survives Phasing of Zhalfir; Ryan Mana Drained a t4 Harmonize into a large Goose +4/16/24,Terence,Kefnet the Mindful,Patrick,Dihada,Ryan,Emiel,,,,,,,Terence,Kefnet the Mindful,13,12,21+ commander,FFA,Kefnet w/ Empyrial Plate +4/20/24,Jack J,Syr Ginger,Patrick,Kamahl/Prava,Terence,Atraxa win-cons,,,,,,,Patrick,Kamahl/Prava,9,8,combat damage,FFA,Inspiring Leader + both commanders +4/20/24,Terence,Atraxa win-cons,Jack J,Syr Ginger,Ryan,Emiel,Patrick,Kamahl/Prava,,,,,Jack J,Syr Ginger,15,12,combat damage,FFA,Ghalta/Mavren made 20 vampires (w/ Doubling Season); Millenium Calendar hit 50; Spine of Ish Sah recurred each turn +4/20/24,Ryan,Gitrog,Patrick,Laelia,Terence,Amber/Veteran Soldier,Jack J,Ghyrson Starn,,,,,Patrick,Laelia,10,9,21+ commander,FFA,"Throne of Eldraine into Etali snowballed, then Laelia cascaded for +50/+50. Slicer *nearly* took Laelia out. Terence Generous Gifted Ryan's only land" +4/20/24,Terence,Vial Smasher/Sidar Kondo,Ryan,"Liesa, Shroud of Dusk",Patrick,Dihada,,,,,,,Ryan,"Liesa, Shroud of Dusk",11,10,combat damage,FFA,Austere Command +4/20/24,Patrick,Dihada,Terence,Vial Smasher/Sidar Kondo,Ryan,"Liesa, Shroud of Dusk",,,,,,,Terence,Vial Smasher/Sidar Kondo,10,8,combat damage,FFA,Authority of the Consul +4/23/24,Patrick,Ashad,Terence,Phabine,Ryan,Zaxara,,,,,,,Terence,Phabine,8,7,combat damage,FFA,"Adeline, Port Razer, Halana and Alena into Phabine" +4/23/24,Ryan,Zaxara,Patrick,Ashad,Terence,(Borrowed) Ryan's Jon Irenicus,,,,,,,Ryan,Zaxara,9,7,combat damage,FFA,Genesis Wave for 7; Exponential Growth for 3; Villainous Wealth for 19; Biomass Mutation for 32 (477 trample damage) +4/23/24,Ryan,Reyav,Patrick,Gwenna,Terence,Yidris dredge,,,,,,,Patrick,Gwenna,11,7,combat damage,FFA,Living Death wiped out Reyav early; Ram Through post Zopandrel for lethal +4/23/24,Ryan,Gitrog,Patrick,Rafiq,Terence,Jan Jansen,,,,,,,Terence,Jan Jansen,9,8,aristocrats/burn,FFA,Mirkwood Bats + Thornbite Staff + Mayhem Devil + Jan Jansen ftw; Rafiq flooded after a terrifying start +5/1/24,Brandon,"Gavi, Nest Warden",Ryan,Ziatora,Patrick,Ashad,Terence,"Izzet (Jori En, Ruin Diver)",,,,,Terence,"Izzet (Jori En, Ruin Diver)",9,8,combat damage,FFA,"Arcane Bombardment, Stormkiln Artist, Haughty Djinn should've been stopped sooner. Djinn Illuminatus replicated Lightning Bolt x8 to take out Ashad" +5/1/24,Patrick,Ashad,Terence,"Izzet (Bilbo, Retired Burglar)",Brandon,"Dogmeat, Ever Loyal",Ryan,Ziatora,,,,,Patrick,Ashad,11,6,combat damage,FFA,Ziatora+GE-Rhonas+Zopandrel KO'ed Brandon on t6; Ashad and 8 Mishra's Self-Replicators went wide enough +5/7/24,Terence,Tevesh Szat/Kraum,Ryan,Ur-Dragon,Patrick,Kamahl/Prava,,,,,,,Ryan,Ur-Dragon,11,10,combat damage,FFA,"Elminster's Simulacrum (copied) created 2 dragons and a Kamahl, but didn't live to untap" +5/7/24,Terence,Wernog/Cecily,Ryan,Ur-Dragon,Patrick,Kamahl/Prava,,,,,,,Terence,Wernog/Cecily,11,11,alt win-con,FFA,Hellkite Tyrant win-con enabled by Storm the Vault and Wernog flickers (equalled 32 life!) +5/7/24,Terence,Ravos/Rebbec,Ryan,Zaxara,Patrick,Kamahl/Prava,,,,,,,Terence,Ravos/Rebbec,11,10,combat damage,FFA,"Rammas Echor, Door of Destinies, Metallic Mimic, Hero of Bladehold took out Ryan 2 turns before Simic Ascendancy, then Anduril, Narsil Reforged + Biotransference closed it out after a mana-screwed Patrick dropped 8 lands (Harvest Season) w/ Felidar Retreat and Doubling Season" +5/15/24,Ryan,"Tinybones, the Pickpocket",Patrick,"Smeagol, Helpful Guide",Terence,Atraxa win-cons,,,,,,,Terence,Atraxa win-cons,10,10,alt win-con,FFA,Accidental Millennium Calendar win; Smeagol milled both opponents out but forgot Calendar would win on upkeep (and had artifact removal in hand) +5/15/24,Patrick,"Smeagol, Helpful Guide",Terence,Atraxa win-cons,Ryan,"Tinybones, the Pickpocket",,,,,,,Patrick,"Smeagol, Helpful Guide",11,10,combat damage,FFA,Awakening Zone triggered Smeagol every turn; Rampaging Baloths + Invasion of Lorwyn closed out +5/15/24,Terence,Emry,Ryan,Omnath,Patrick,Niv-Mizzet,,,,,,,Terence,Emry,9,9,combat damage,FFA,"T3 Kappa Cannoneer survived 2 board wipes, then Echo Storm created 4 more" +5/15/24,Patrick,Niv-Mizzet,Terence,Emry,Ryan,Omnath,,,,,,,Ryan,Omnath,8,8,aristocrats/burn,FFA,Nissa + Ancient Greenwarden into Crackle With Power +5/18/24,Jack J,Syr Ginger,Patrick,Maarika,Jack F,"Brenard, Ginger Sculptor",Brandon,"Dogmeat, Ever Loyal",,,,,Brandon,"Dogmeat, Ever Loyal",9,6,combat damage,FFA,31-power Dogmeat (Strong Back+Mantle of the Ancients) archenemy'ed the table +5/18/24,Patrick,Maarika,Jack F,Jeskai (The War Doctor/Clara Oswald (blue)),Brandon,"Caesar, Legion's Emperor",Jack J,Ghyrson Starn,,,,,Patrick,Maarika,12,7,combat damage,FFA,Maarika/Tergrid stole Fervent Charge and Iroas to lethal Brandon +5/18/24,Jack F,Jeskai (Kate Stewart),Brandon,"Morska, Undersea Sleuth",Jack J,Ghyrson Starn,Patrick,Kamahl/Prava,,,,,Jack J,Ghyrson Starn,7,7,aristocrats/burn,FFA,"Niv-Mizzet, Ghyrson Starn, Ophidian Eye" +5/18/24,Jack F,"Astor, Bearer of Blades",Brandon,Commodore Guff,Ryan,Don Andres,Jack J,Jon Irenicus,Patrick,Kamahl/Prava,,,Jack F,"Astor, Bearer of Blades",15,9,combat damage,FFA,Lae'Zel and 5 planeswalkers (unanswered for 5 turns) played archenemy until Holy Day and 2 Bruenor swings to the face +5/21/24,Terence,Wilson/Cultist,Patrick,Tekuthal,Ryan,Emiel,,,,,,,Terence,Wilson/Cultist,16,14,combat damage,FFA,Wilson's card-advantage engines never stopped; Jace TMS ult'ed (after a 5*2 proliferate turn) on Emiel; Emiel's White Dragon tapped down Tekuthal's blockers +5/21/24,Ryan,Toxrill,Terence,Rigo,Patrick,Jared [Jegantha],,,,,,,Terence,Rigo,10,8,poison,FFA,"Norn's Decree: incentivized Jared to only attack Toxrill (which halted the 1/1 swarm), then was lethal to a first strike + normal strike would-be attack" +5/21/24,Terence,Vial Smasher/Sidar Kondo,Patrick,Ayara,Ryan,Gitrog,,,,,,,Terence,Vial Smasher/Sidar Kondo,9,8,combat damage,FFA,"Vial Smasher literally only hit Ayara (~20 damage); Ayara jumped from 2 to 20 (taking out mana-screwed Gitrog) w/ Gary, but unblockable + Mercadia's Downfall was exactly lethal" +5/25/24,Jeff,Mondrak,Terence,Viconia/Cultist,Patrick,Kiora,,,,,,,Terence,Viconia/Cultist,9,9,aristocrats/burn,FFA,Living Death: Ayara + Abhorrent Overlord devotion 32 insta lethal +5/25/24,Patrick,Kiora,Jeff,Mondrak,Terence,Viconia/Cultist,,,,,,,Terence,Viconia/Cultist,16,15,combat damage,FFA,Slow one: multiple board bounces and one wipe; bestowed Nighthowler for 19 closed it out +5/26/24,stranger,Jeska/Vial Smasher,stranger,Riku of Many Paths,stranger,Sovereign Okinec Ahau,Patrick,Rafiq,,,,,Patrick,Rafiq,10,5,21+ commander,FFA,"T3 Rafiq w/ mom protection took out Riku, then rebuilt post board wipe (mom survived) and 3/3 commander lethal" +5/26/24,Patrick,Ayara,stranger,"Kellan, the Fae-Blooded",stranger,Sovereign Okinec Ahau,,,,,,,stranger,"Kellan, the Fae-Blooded",13,8,combat damage,FFA,(opted not to play Bolas' Citadel b/c had just won previous game); Kellan won due to fog effect stopping Sovereign from hitting for 100+ +5/26/24,stranger,The Swarmlord,stranger,Tergrid,Patrick,Ashad,stranger,"Rocco, Street Chef",,,,,stranger,"Rocco, Street Chef",10,9,combat damage,FFA,Rocco and Tergrid emerged as heavyweights; Rocco Final Showdown'ed in response to Tergrid's Myojin of Night's Reach; Tergrid had to leave and Rocco cleaned up fast +6/2/24,Brandon,Sliver Gravemother,Ajit,Brago,Terence,Umori,Jeff,Go-Shintai of Life's Origin,Patrick,"Smeagol, Helpful Guide",,,Terence,Umori,12,8,combat damage,Star,"Smeagol was a punching bag; Aetherspouts stopped a lethal Rumbleweed, but Sanctum of Stone Fangs inadvertently took out Brandon before Ajit could be taken out" +6/2/24,Patrick,"Smeagol, Helpful Guide",Brandon,Abaddon,Ajit,Jhoira of the Ghitu,Terence,Borborygmos Enraged,Jeff,Mondrak,,,Patrick,"Smeagol, Helpful Guide",6,6,mill,Star,Naturally drew the mill combo +6/2/24,Terence,Borborygmos Enraged,Jeff,Mondrak,Patrick,Duke Ulder Ravengard,Brandon,"Morska, Undersea Sleuth",Ajit,Jhoira of the Ghitu,,,Terence,Borborygmos Enraged,10,8,combat damage,Star,"Windshaper Planetar rerouted 21 Mondrak damage from Brandon to Terence; Mondrak's Starlight Spectacular (would've won if we'd done math precombat to play 1 more creature) was short of killing Brandon so took out Patrick; Morska's Kappa Cannoneer ended Jeff, leaving Terence last one standing" +6/2/24,Brandon,"Atraxa, Grand Unifier",Patrick,Talion,Jeff,Illuna,Ajit,Kadena,Terence,Rograkh/Silas ninjas,,,Patrick,Talion,11,9,combat damage,Star,"Kadena early Tempt for Discovery and stole Talion; Atraxa early Breach the Multiverse (Mommy Norn, Oko, Apex Altisaur, Elspeth Knight Errant) became archenemy; loads of interaction; eventually Talion+Sakashima + Sheoldred closed the door" +6/11/24,Ryan,Reyav,Patrick,Talion,Terence,Umori,,,,,,,Patrick,Talion,13,11,21+ commander,FFA,T2 Hunted Horror centaurs nearly solo'ed Patrick; Sword of W&P stabilized from 5 life +6/11/24,Patrick,Duke Ulder Ravengard,Terence,Ardenn/Esior,Ryan,Emiel,,,,,,,Ryan,Emiel,11,9,combat damage,FFA,Esior w/ Sword of F&F and double strike was dominant until double Manglehorn +6/11/24,Terence,Slimefoot and Squee,Ryan,Chatterfang,Patrick,Ayara,,,,,,,Patrick,Ayara,9,8,aristocrats/burn,FFA,Bolas' Citadel activation lethaled Terence; Muranda Petroglyphs buffed more zombies than squirrels (thus keeping big Liliana alive) +7/5/24,Jeff,Illuna,Natalie,Anikthea,Jack J,Gale/Scion of Halaster,,,,,,,Jeff,Illuna,15,15,aristocrats/burn,FFA,A large Setessan Champion provided ample fuel for a triple Brash Taunter activation +7/5/24,Jeff,Ulalek,Natalie,(Borrowed) Jeff's Go-Shintai,Jack J,Sliver Overlord,,,,,,,Jack J,Sliver Overlord,8,8,combat damage,FFA,Jack snookered himself as Crystalline Sliver blocked Magma Sliver from going off. Magma Sliver + Sliver Queen + Heart Sliver + Ashnod's Altar is nuts (as is Mana Echoes) +7/5/24,Natalie,(Borrowed) Jack's Ghyrson Starn,Jack J,Evra,Jeff,Ulalek,,,,,,,Natalie,(Borrowed) Jack's Ghyrson Starn,9,9,aristocrats/burn,FFA,"Natalie won by playing a pointless Mana Geyser which triggered a bunch of ""on instant/sorcery"" effects" +7/5/24,Jeff,Mondrak,Natalie,(Borrowed) Jack's Melek,Jack J,Evra,,,,,,,Jack J,Evra,6,6,alt win-con,FFA,Jack wins with Felidar Sovereign after a slow start and with Aetherflux Reservoir +7/5/24,Jack J,Evra,Jeff,Mondrak,Natalie,(Borrowed) Jack's Melek,,,,,,,Jeff,Mondrak,11,11,combat damage,FFA,Guardian of Faith whiffed a huge Evra swing +7/5/24,Natalie,Anikthea,Jack J,Syr Ginger,Jeff,"Liesa, Forgotten Archangel",,,,,,,Natalie,Anikthea,15,15,combat damage,FFA,Enchantments are oppressive! +7/13/24,Ryan,Ulalek,Patrick,Oops all Kayas,Jeff,Ulalek,,,,,,,Jeff,Ulalek,9,7,combat damage,FFA,MH3 Ulamog as a 19/19 w/ Annihilator 12 > two It That Betrays; Patrick mana screwed +7/13/24,Patrick,Oops all Kayas,Jeff,Ulalek,Ryan,Ulalek,,,,,,,Patrick,Oops all Kayas,11,10,combat damage,FFA,13 spirit and bird tokens ftw! Buffed by Intangible Virtue +7/13/24,Patrick,Ashad,Jeff,"Liesa, Forgotten Archangel",Ryan,Rin and Seri,,,,,,,Ryan,Rin and Seri,11,10,combat damage,FFA,"Liesa + Luminous Broodmoth + Vito = redundancy and fast drains! But, double Combustible Gearhulk dropped Liesa from 39 to 4 before dying; Rin and Seri had exactly enough creatures for lethal (upon noting Dauthi Voidwalker has shadow and couldn't block!)" +7/13/24,Patrick,Tekuthal,Jeff,Mondrak,Ryan,Gitrog,,,,,,,Jeff,Mondrak,8,7,combat damage,FFA,"Sol Ring -> Throne of Eldraine -> Cathar's Crusade -> Elesh Norn, GC took out Tekuthal + Flux Channeler + Danny Pink w/ Sword of Truth and Justice one turn before lethal; Gitrog mana screwed" +7/13/24,Jeff,Go-Shintai of Life's Origin,Ryan,Breya,Patrick,Gwenna,,,,,,,Patrick,Gwenna,12,12,combat damage,FFA,Heavyweight battle! KO turn: topdecked Return of the Wildspeaker for 10 cards -> Old Gnawbone + Pest Infestation to clear out leftovers after exiling Darksteel Forge. Double Portal to Phyrexia ate enough Shrines; red Shrine took Daretti and 1 Garruk +7/13/24,Ryan,Reyav,Patrick,Dihada,Jeff,Mondrak,,,,,,,Ryan,Reyav,5,4,21+ commander,FFA,Sol Ring -> Reyav + Shadowspear + 3 more equipment +7/13/24,Patrick,Dihada,Jeff,Mondrak,Ryan,Reyav,,,,,,,Patrick,Dihada,8,7,combat damage,FFA,Reaver Cleaver on Mogis into Gallifrey Falls+No More to (mostly) 1-sided wipe +7/25/24,Ryan,Ulalek,Terence,Ardenn/Esior,Patrick,Ashad,,,,,,,Terence,Ardenn/Esior,12,7,21+ commander,FFA,Ardenn w/ Colossus Hammer for 22; Ugin's Binding bought Ryan a couple turns before the inevitable +7/25/24,Terence,Ardenn/Esior,Patrick,Ashad,Ryan,Goose Mother,,,,,,,Ryan,Goose Mother,12,12,combat damage,FFA,Esior stalled big time; 2x Blinkmoth Urn accelerated large Goose Mothers; critical Silence stopped Ashad; Momentous Fall for 12 into 2x counterspell + Questing Beast lethal +7/25/24,Terence,Vial Smasher/Sidar Kondo,Patrick,Niv-Mizzet,Ryan,Emiel,,,,,,,Terence,Vial Smasher/Sidar Kondo,11,7,combat damage,FFA,Mercadia's Downfall adds 20+ damage to oust Niv-Mizzet +7/31/24,Terence,Atraxa win-cons,Patrick,Kamahl/Prava,Ryan,Chatterfang,,,,,,,Terence,Atraxa win-cons,14,13,combat damage,FFA,"Gideon, Champion of Justice vs tokens? Got to 24 loyalty (aided by The Eternal Wanderer and Maze of Ith), then ulted" +7/31/24,Patrick,Kamahl/Prava,Ryan,Chatterfang,Terence,Atraxa win-cons,,,,,,,Ryan,Chatterfang,7,6,combat damage,FFA,Beastmaster Ascension after Deepwood Hermit +7/31/24,Patrick,Ayara,Terence,Dargo/Nadier,Ryan,Breya,,,,,,,Ryan,Breya,7,6,combat damage,FFA,Dargo + Jeska 1-shot Ayara (at 47); Breya ETB made Kappa Cannoneer exactly lethal vs Dargo +7/31/24,Patrick,Ayara,Terence,Dargo/Nadier,Ryan,Breya,,,,,,,Ryan,Breya,8,8,aristocrats/burn,FFA,Descent into Avernus fueled Storm the Vault for Breya to burn for 24 face damage +8/2/24,Ajit,Brago,Brandon,Dr. Madison Li,Terence,Borborygmos Enraged,Ryan,"Alania, Divergent Storm",,,,,Ryan,"Alania, Divergent Storm",16,16,combat damage,FFA,Jin Gitaxias is mean and Terence dealt 30 to Brandon with land discard at 1 life +8/2/24,Jack J,Evra,Kyle,"Tatyova, Benthic Druid",Patrick,"Narset, Enlightened Exile",Jeff,Ulalek,,,,,Kyle,"Tatyova, Benthic Druid",14,12,combat damage,FFA,CycRift with 90+ power on board; Evra consistently at 70+; new Ulamog for annihilator 7 and a 30/27 +8/2/24,Patrick,"Narset, Enlightened Exile",Jack J,Ghyrson Starn,Terence,Phabine,Ajit,Brago,,,,,Jack J,Ghyrson Starn,6,5,aristocrats/burn,FFA,Sol Ring -> t3 pinger with Curiosity and Ghyrson Starn. Repercussion closed extra quickly! Terence missed lethal w/ Savage Beating under Windbrisk Heights +8/2/24,Kyle,Jodah the Unifier,Brandon,The Wise Mothman,Jeff,Mondrak,Ryan,Breya,,,,,Ryan,Breya,15,11,combat damage,FFA,Everyone forgot about scourglass +8/2/24,Patrick,Laelia,Jeff,Go-Shintai of Life's Origin,Kyle,Voja,,,,,,,Kyle,Voja,8,8,combat damage,FFA,Voja and literally any creatures are immediately terrifying +8/2/24,Brandon,Okaun/Zndrsplt,Jack J,"Herigast, Erupting Nullkite",Ryan,Chatterfang,Terence,Yidris dredge,,,,,Brandon,Okaun/Zndrsplt,8,8,quality of life concede,FFA,Zndrsplt flipped 9 coins and had lifelink. Others conceded w/ Brandon at 2500+ life +8/4/24,Patrick,"Narset, Enlightened Exile",stranger,"Arcades, the Strategist",stranger,"Olivia, Opulent Outlaw",stranger,Davros,,,,,Patrick,"Narset, Enlightened Exile",14,14,combat damage,FFA,Narset go-wide (Promise of Bunrei + Saheeli) and multiple prowess triggers add up quickly; negligible threat assessment vs me +8/4/24,stranger,Animar,Patrick,Tekuthal,stranger,Chatterfang,,,,,,,stranger,Animar,11,10,combat damage,FFA,Indestructible Tekuthal with Sword of T&J exiled by Ulamog; Eldrazi annihilation ensued +8/11/24,stranger,"Coram, the Undertaker",Patrick,"Narset, Enlightened Exile",Terence,Rograkh/Silas ninjas,stranger,Tergrid,,,,,stranger,"Coram, the Undertaker",9,9,aristocrats/burn,FFA,"Season of Loss, Mesmeric Orb" +8/11/24,stranger,"Otharri, Sun's Glory",stranger,Ghyrson Starn,Patrick,Niv-Mizzet,Terence,Slimefoot and Squee,,,,,stranger,"Otharri, Sun's Glory",9,9,combat damage,FFA,Unanswered Otharri with Relentless Assault to close out +8/11/24,stranger,"Miirym, Sentinel Wyrm",stranger,Ghyrson Starn,Patrick,"Smeagol, Helpful Guide",Terence,Slimefoot and Squee,,,,,Patrick,"Smeagol, Helpful Guide",11,8,combat damage,FFA,Ghyrson eliminated 2 players at once via pings and left Smeagol; Smeagol topdecked removal for Ghyrson and beat down with Dreadfeast Demons +8/14/24,Terence,Borborygmos and Fblthp,Patrick,Tekuthal,Ryan,Gitrog,,,,,,,Terence,Borborygmos and Fblthp,10,9,combat damage,FFA,Avenger of Zendikar with fetches and Ancient Greenwarden quickly made 18/19 plants +8/14/24,Terence,Borborygmos and Fblthp,Patrick,Dihada,Ryan,Gitrog,,,,,,,Patrick,Dihada,14,14,combat damage,FFA,Iroas + Gallifrey Falls as one-sided board wipe; Gitrog started mana screwed (but later Strip Mine'd Maze of Ith) +8/17/24,Terence,Borborygmos and Fblthp,Ryan,Breya,Patrick,Maarika,Jack J,Sliver Overlord,Jeff,Mondrak,,,Ryan,Breya,15,11,aristocrats/burn,FFA,"Breya turbo'ed out Darksteel Forge, eventually lost it to Maarika, but Dockside into Portal to Phyrexia and Breya pings closed it" +8/17/24,Terence,Wernog/Cecily,Ryan,"Alania, Divergent Storm",Patrick,Jared [Jegantha],Jack J,Syr Ginger,Jeff,Ulalek,,,Ryan,"Alania, Divergent Storm",15,11,aristocrats/burn,FFA,Wernog/Cecily archenemy with Coveted Jewel and Displacer Kitten but ran out of steam; Alania won with Guttersnipe pings with 3 cards left in library +8/20/24,Patrick,Urza,Terence,Rionya,Ryan,Reyav,,,,,,,Patrick,Urza,9,8,combat damage,FFA,Unstable Wanderglyph and 4 crafted copies with Suspend and Teferi's Protection backup +8/20/24,Terence,Rionya,Ryan,Reyav,Patrick,Urza,,,,,,,Ryan,Reyav,7,6,21+ commander,FFA,Giver of Runes and Menace on Reyav quickly pushed through lethal +8/20/24,Patrick,Talion,Terence,Jan Jansen,Ryan,Pantlaza,,,,,,,Patrick,Talion,10,9,combat damage,FFA,T3 Talion drew plenty of interaction to bounce Ghalta + friends and outrace Steel Overseer with Sting +9/3/24,Ryan,Chatterfang,Patrick,Laelia,Terence,Abdel Adrian/Far Traveler,,,,,,,Ryan,Chatterfang,7,7,combat damage,FFA,Craterhoof Behemoth after a Second Harvest - even beat two opposing Sol Rings +9/3/24,Patrick,Laelia,Terence,Abdel Adrian/Far Traveler,Ryan,Chatterfang,,,,,,,Patrick,Laelia,9,8,21+ commander,FFA,Laelia w/ Tenza +9/3/24,Patrick,Rafiq,Terence,Jan Jansen,Ryan,Breya,,,,,,,Patrick,Rafiq,6,5,21+ commander,FFA,"T3 Rafiq -> T4 Kor Spiritdancer -> Ossification + Aqueous Form to preempt Scourglass, then one-shot Jan Jansen (aka 4 consecutive topdecks FTW)" +9/3/24,Ryan,Breya,Patrick,Rafiq,Terence,Jan Jansen,,,,,,,Terence,Jan Jansen,7,7,combat damage,FFA,Double Thornbite Staff = infinite tokens +9/3/24,Ryan,Reyav,Patrick,Talion,Terence,Jan Jansen,,,,,,,Terence,Jan Jansen,11,10,aristocrats/burn,FFA,Thornbite Staff + Jan Jansen + Mayhem Devil +9/6/24,Ryan,Omnath,Patrick,Dihada,Terence,Miara/Thrasios,,,,,,,Terence,Miara/Thrasios,8,8,combat damage,FFA,Infinite elves. 488k Scute Swarm. Essence Warden. Kardur. Galadhrin Ambush. +9/6/24,Patrick,Tameshi,Terence,Vial Smasher/Sidar Kondo,Ryan,Reyav,,,,,,,Terence,Vial Smasher/Sidar Kondo,11,9,combat damage,FFA,Inkshield +9/6/24,Patrick,Tameshi,Terence,Rograkh/Silas ninjas,Ryan,Breya,,,,,,,Terence,Rograkh/Silas ninjas,10,9,combat damage,FFA,Yuriko + Grazilaxx = enough card advantage through Jin-Gitaxias + Ethersworn Canonist +9/8/24,Patrick,Ashad,Jack J,Nadu,Terence,Borborygmos and Fblthp,,,,,,,Terence,Borborygmos and Fblthp,12,10,combat damage,FFA,Energy Flux shut down Ashad. Nadu+Rampaging Baloths + Retreat to Coralhelm landed 14 beasts in one go. Avenger of Zendikar for 20 4/5s. Melded Titania made the plants very large. +9/8/24,Terence,Viconia/Cultist,Patrick,Oops all Kayas,Jack J,Evra,,,,,,,Jack J,Evra,12,10,aristocrats/burn,FFA,"Surestrike Trident ousted Kayas. Aetherflux Reservoir (while at 218) downed Viconia. Multiple board wipes and edicts delayed Evra, but in vain." +9/8/24,Jack J,"Herigast, Erupting Nullkite",Terence,Rigo,Patrick,Duke Ulder Ravengard,,,,,,,Terence,Rigo,8,8,poison,FFA,Flurry of Wings and proliferate 10 ended the foregone conclusion +9/8/24,Patrick,Duke Ulder Ravengard,Jack J,"Herigast, Erupting Nullkite",Terence,Rigo,,,,,,,Jack J,"Herigast, Erupting Nullkite",12,11,combat damage,FFA,Herigast survived 9 poison counters and Lumbering Battlement blink shenanigans and went off with Echoes of Eternity and Darksteel Monolith +9/17/24,Patrick,Urza,Terence,Satya,Ryan,"Liesa, Shroud of Dusk",,,,,,,Terence,Satya,13,9,21+ commander,FFA,Everyone had a Liesa! Unchecked Sword of H&H with Whirler Rogue. Sorin with Exquisite Blood for a 50pt drain wasn't enough +9/17/24,Terence,Satya,Ryan,Emiel,Patrick,Urza,,,,,,,Patrick,Urza,13,9,combat damage,FFA,26 Powerstone Shards +9/17/24,Patrick,Gwenna,Terence,Muldrotha,Ryan,Breya,,,,,,,Patrick,Gwenna,8,8,combat damage,FFA,Stonehoof Chieftain into Old Gnawbone +9/17/24,Terence,Muldrotha,Ryan,Breya,Patrick,Gwenna,,,,,,,Terence,Muldrotha,12,8,combat damage,FFA,Gwenna put in kingmaking position with Nissa ult for +9/+9 against impending Phyrexian Scriptures and Scourglass + Darksteel Forge +9/20/24,Ryan,Toxrill,Patrick,Laelia,Jack J,"Aminatou, Veil Piercer",,,,,,,Jack J,"Aminatou, Veil Piercer",13,11,combat damage,FFA,One with the Multiverse value-town'ed with Boon of the Hidden Realm (+ Estrid's Invocation) + All That Glitters was a lot of math with Toxrill. Toxrill faded after Blasphemous Act; Laelia faded after Cyclonic Act +9/20/24,Brandon,Magus Lucea Kane,Jeff,Mondrak,Ryan,Reyav,,,,,,,Brandon,Magus Lucea Kane,8,7,combat damage,FFA,"Sol Ring into Branching Evolution, Hardened Scales, and Walking Ballista walk into a bar..." +9/20/24,Jack J,Meren of Clan Nel Toth,Stephanie,"(Borrowed) Jack's Szarekh, the Silent King",Patrick,"(Borrowed) Jack's Nahiri, the Lithomancer",,,,,,,Stephanie,"(Borrowed) Jack's Szarekh, the Silent King",11,10,combat damage,FFA,Cranial Plating +9/20/24,Ryan,Reyav,Brandon,Magus Lucea Kane,Jeff,Mondrak,,,,,,,Ryan,Reyav,9,6,combat damage,FFA,Gingerbrute suited to the nines. +9/20/24,Brandon,"Atraxa, Grand Unifier",Jeff,Mondrak,Ryan,Pantlaza,,,,,,,Brandon,"Atraxa, Grand Unifier",10,7,combat damage,FFA,T5 Atraxa picked up Sword of Feast and Famine; T6 og Vorinclex +9/21/24,Jack J,Evra,Patrick,Tekuthal,Brandon,Silvar/Trynn,Jeff,Ulalek,,,,,Jack J,Evra,10,8,21+ commander,FFA,Ulalek t1 Sol Ring+Mana Crypt+Talisman into t3 Ugin and t4 Ulamog (then Oblivion Sower got 23 lands from Tekuthal). Evra w/ double strike and unblockable lethal'ed everyone +9/21/24,Brandon,Anikthea,Jeff,Ulalek,Jack J,Gitrog,Patrick,Kamahl/Prava,,,,,Jack J,Gitrog,11,9,combat damage,FFA,"Patrick's Beastmaster Ascension took out Brandon; Jack's Avenger of Zendikar for 8 counters took out Patrick, then Jeff" +9/21/24,Brandon,Marneus Calgar,Patrick,"Narset, Enlightened Exile",Jack J,"Aminatou, Veil Piercer",Ryan,Sythis,Jeff,Go-Shintai of Life's Origin,,,Tie (Patrick; Jack J),"Tie (Narset, Enlightened Exile; Aminatou, Veil Piercer)",11,10,combat damage,Star,Patrick and Jack tied; og Zur w/ 2x All That Glitters took out Brandon; Ryan nearly milled out (died to Narset commander damage) +9/21/24,Jeff,Mondrak,Ryan,Omnath,Jack J,Syr Ginger,Brandon,"Dogmeat, Ever Loyal",Patrick,Rafiq,,,Ryan,Omnath,10,10,combat damage,Star,Omnath's board wipe stopped Adeline + Cathar's Crusade; Avenger of Zendikar w/ 6 land drops ended the game +9/21/24,Ryan,Akul,Brandon,Zhulodok,Patrick,Duke Ulder Ravengard,Jeff,Mondrak,,,,,Brandon,Zhulodok,10,9,combat damage,FFA,Unchecked Eldrazi ramp (t3 Zhulodok cascaded out a zillion threats) +9/22/24,Jack J,Chishiro,Patrick,Kiora,Brandon,Dr. Madison Li,Jeff,Hylda,Ryan,Obeka,,,Jeff,Hylda,13,8,combat damage,Star,Kiora Bests the Sea God backstabbing Kiora +9/25/24,Patrick,(Borrowed) Ryan's Rin and Seri,Terence,Ishai/Tana,Ryan,"Vito, Fanatic of Aclazotz",,,,,,,Terence,Ishai/Tana,11,10,combat damage,FFA,Vesuvan Duplimancy made 4 Setessan Champions; Boon of the Spirit Realm +9/25/24,Patrick,(Borrowed) Ryan's Rin and Seri,Terence,Ishai/Tana,Ryan,Nekusar,,,,,,,Ryan,Nekusar,11,10,aristocrats/burn,FFA,Peer Into the Abyss ended Rin and Seri from 64 +9/25/24,Patrick,Ayara,Terence,Muldrotha,Ryan,Sythis,,,,,,,Ryan,Sythis,6,5,21+ commander,FFA,All That Glitters + Armadillo Cloak (trample); Land Tax + Burgeoning outpaced Dark Ritual + Sol Ring + Arcane Signet +9/25/24,Patrick,Ayara,Terence,Muldrotha,Ryan,Sythis,,,,,,,Ryan,Sythis,7,7,21+ commander,FFA,All That Glitters + Hallowed Haunting (flying) + Ancestral Mask +10/01/24,Terence,Satya,Ryan,Myrel,Patrick,Ashad,,,,,,,Patrick,Ashad,10,10,combat damage,FFA,Myrel tokens; Satya board wiped; Ashad Mishra's Self Replicator Inkwell Leviathan and Brudiclad +10/01/24,Ryan,Myrel,Patrick,Ashad,Terence,Satya,,,,,,,Terence,Satya,11,11,21+ commander,FFA,Many copies of Pyrogoyf +10/01/24,Patrick,Dihada,Terence,Yoshimaru/Reyhan,Ryan,Gitrog,,,,,,,Patrick,Dihada,12,11,combat damage,FFA,Cadric and Ratadrabik making 2 extra copies per legend outpaced slower starts from the others +10/08/24,Brandon,Yawgmoth,Patrick,Oops all Kayas,Terence,Fourth Doctor/Susan Foreman,Ryan,Nekusar,,,,,Ryan,Nekusar,12,12,aristocrats/burn,FFA,"2 Sheoldreds, Gary, Font of Mythos, Body of Knowledge" +10/08/24,Ryan,"Alania, Divergent Storm",Patrick,Jared [Jegantha],Terence,The Rani,,,,,,,Patrick,Jared [Jegantha],10,9,combat damage,FFA,Large kavus +10/20/24,Kyle,Meren,Patrick,Ashad,Jack F,Baral and Kari Zev,,,,,,,Kyle,Meren,13,12,combat damage,FFA,Meren value with Mazirek Death Priest outraced 2x Kappa Cannoneer; Jack kingmade removing Patrick's blocker +10/20/24,Patrick,Niv-Mizzet,Anton,Breya [Zirda],Brandon,"Atraxa, Grand Unifier",,,,,,,Patrick,Niv-Mizzet,10,10,21+ commander,FFA,Faeburrow Elder w/ Saffi and Linvala protection +10/20/24,Jeff,King Bumi,Anton,Azlask,Brandon,"Gavi, Nest Warden",,,,,,,Brandon,"Gavi, Nest Warden",13,12,combat damage,FFA,Dockside flickered repeatedly with Astral Slide +10/20/24,Anton,Sisay [Jegantha],Brandon,The Ur-Dragon,Patrick,Dihada,,,,,,,Anton,Sisay [Jegantha],6,6,aristocrats/burn,FFA,Unchecked Sisay combo'ed off w/ Derevi and Chromatic Orrery +10/20/24,Kyle,Zur the Enchanter,Jeff,Hylda,Jack F,"Astor, Bearer of Blades",,,,,,,Jeff,Hylda,15,15,combat damage,FFA, +10/20/24,Patrick,Rafiq,Jeff,Mondrak,Jack F,Vishgraz,,,,,,,Jeff,Mondrak,8,7,combat damage,FFA,Mirror Entity +10/20/24,Anton,Skullbriar,Brandon,The First Sliver,Kyle,Zur the Enchanter,,,,,,,Brandon,The First Sliver,7,6,combat damage,FFA,Cascade popped off +10/23/24,Terence,Fourth Doctor/Susan Foreman,Patrick,Kamahl/Prava,Ryan,"Vito, Fanatic of Aclazotz",,,,,,,Terence,Fourth Doctor/Susan Foreman,10,10,combat damage,FFA,Traverse the Outlands for 7 with card draw to back it up +10/23/24,Ryan,Nekusar,Terence,Ashnod,Patrick,Kamahl/Prava,,,,,,,Patrick,Kamahl/Prava,7,7,combat damage,FFA,Rabble Rousing -> Harvest Season for 6 -> Kamahl +10/23/24,Ryan,Nekusar,Terence,Ashnod,Patrick,Duke Ulder Ravengard,,,,,,,Patrick,Duke Ulder Ravengard,9,9,combat damage,FFA,Teferi's Puzzle Box game. Sheoldred + Peer Into the Abyss ended Terence. Flameshadow Conjuring with Firbolg Flutist. +11/10/24,Patrick,Havi,Jeff,Ulalek,Jack J,Tekuthal,Brandon,Dr. Madison Li,,,,,Brandon,Dr. Madison Li,16,12,combat damage,FFA,Myr Battlesphere; Havi 1v3'ed for 4 turns before Evacuation reset +11/10/24,Patrick,Ashling,Jeff,Hylda,Jack J,Sidar Kondo/Tevesh Szat,Brandon,Magus Lucea Kane,,,,,Patrick,Ashling,15,12,aristocrats/burn,FFA,Basilisk Collar +11/10/24,Patrick,Jared [Jegantha],Jeff,Go-Shintai of Life's Origin,Jack J,Evra,Brandon,Yawgmoth,,,,,Brandon,Yawgmoth,10,8,combat damage,FFA,"Undying loops w/ Yawgmoth, Grave Pact, and Massacre Wurm" +11/14/24,Ryan,Reyav,Patrick,Ashling,Terence,Satya,,,,,,,Terence,Satya,10,8,combat damage,FFA,Tempt with Mayhem on a Price of Progress with a +2 damage effect +11/14/24,Patrick,Havi,Terence,Raffine,Ryan,Sythis,,,,,,,Terence,Raffine,8,7,21+ commander,FFA,Knowledge is Power supercharged attacks; even rebuilt post board wipe diff --git a/tests/elo/test_elo.py b/tests/elo/test_elo.py index 0ce44ef..b06b54b 100644 --- a/tests/elo/test_elo.py +++ b/tests/elo/test_elo.py @@ -5,8 +5,13 @@ from app.elo import rerank def test(): # From https://github.com/sublee/elo/blob/master/elotests.py - assert _almost_equal(rerank([1200, 800], 1), [1190.909, 809.091]) - # Couldn't find any test-cases for multiplayer games. + assert _almost_equal(rerank([1200, 800], [1]), [1190.909, 809.091]) + # Couldn't find any test-cases for multiplayer games, so I made this by-hand. + # The logic I chose for ELO calculation in the face of multiple possible winners has the nice property that a + # player's outcome score is independent of any other player's outcome - if you win, you get the same ELO score + # whether "you beat everyone else" or "everybody won". So I could calculate the expected scores by running `rerank` + # in the single-victory case. + assert _almost_equal(rerank([1200, 800, 500], [0, 2]), [1203.694, 796.866, 509.438]) def _almost_equal(actual: Iterable, expected: Iterable) -> bool: diff --git a/tests/test_fresh_db_tests.py b/tests/test_fresh_db_tests.py index 81881bd..1e849e7 100644 --- a/tests/test_fresh_db_tests.py +++ b/tests/test_fresh_db_tests.py @@ -7,6 +7,12 @@ from app import app client = TestClient(app) +# These tests run in a clean database. +# Note, however, that they do not _each_ run in a clean database - it persists between executions. +# Note the use of `cleanups` (defined in `conftest.py`) to allow for cleanup operations that should leave the database +# in a clean state after each test - but also, note the comment above the commented-out +# `test_adding_games_with_ties` + def test_add_and_retrieve_player(test_client: TestClient, cleanups): response = _json_get(test_client, "/player/1") @@ -63,8 +69,8 @@ def test_add_and_retrieve_deck(test_client: TestClient, cleanups): def test_incremental_add_of_games(test_client: TestClient, cleanups): - latest_deck_response = _json_get(test_client, "/game/latest_game") - assert latest_deck_response.status_code == 404 + latest_game_response = _json_get(test_client, "/game/latest_game") + assert latest_game_response.status_code == 404 # https://github.com/tiangolo/fastapi/issues/1536#issuecomment-640781718 with open("seed-data/all-in-one.csv", "rb") as f: @@ -73,10 +79,10 @@ def test_incremental_add_of_games(test_client: TestClient, cleanups): files={"file": ("fake_all_in_one_filename.csv", f, "text/csv")}, ) - latest_deck_response = _json_get(test_client, "/game/latest_game") - assert latest_deck_response.status_code == 200 - print(latest_deck_response.json()) - assert latest_deck_response.json()["date"] == "2024-07-05T00:00:00" + latest_game_response = _json_get(test_client, "/game/latest_game") + assert latest_game_response.status_code == 200 + print(latest_game_response.json()) + assert latest_game_response.json()["date"] == "2024-07-05T00:00:00" # then seed again, and check that it successfully gets the expected latest with open( @@ -91,6 +97,49 @@ def test_incremental_add_of_games(test_client: TestClient, cleanups): assert latest_deck_response.status_code == 200 assert latest_deck_response.json()["date"] == "2024-07-25T00:00:00" + def success_cleanup(): + games = _json_get(test_client, "/game/list") + for game in games.json(): + _json_delete(test_client, f"/game/{game['id']}") + + decks = _json_get(test_client, "/deck/list") + for deck in decks.json(): + _json_delete(test_client, f"/deck/{deck['id']}") + + players = _json_get(test_client, "/player/list") + for player in players.json(): + _json_delete(test_client, f"/player/{player['id']}") + + cleanups.add_success(success_cleanup) + + +# TODO - this test is valid and correct, but I can't find a way to run it. +# The "cleanups" can only interact with the database via APIs (not directly), and the preceding test adds content to the +# database that cannot (currently) be nuked (specifically - sequence numbers). +# Either: +# * Add a full "nuke the database" API (probably not a good thing to expose!) +# * Find a way to initialize a full fresh database for each _test_ (see `isolated_database` in `tests/sql/test_crud.py` +# and `tests/routers/test_stats.py` for inspiration ) +# +# def test_adding_games_with_ties(test_client: TestClient, cleanups): +# latest_game_response = _json_get(test_client, "/game/latest_game") +# assert latest_game_response.status_code == 404 + +# with open("seed-data/all-in-one-with-tied-games.csv", "rb") as f: +# test_client.post( +# "/api/seed/all_in_one", +# files={"file": ("fake_all_in_one_filename.csv", f, "text/csv")}, +# ) + +# tied_game_response = _json_get(test_client, "/game/141") +# assert tied_game_response.status_code == 200 + +# winning_deck_id = tied_game_response.json()["winning_deck_id"] +# other_winning_deck_ids = tied_game_response.json()["other_winning_deck_ids"].split(',') +# assert _json_get(test_client, f"/deck/{winning_deck_id}").json()['name'] == "Narset, Enlightened Exile" +# assert len(other_winning_deck_ids) == 1 +# assert _json_get(test_client, f"/deck/{other_winning_deck_ids[0]}").json()['name'] == "Aminatou, Veil Piercer" + def _json_get(c: TestClient, path: str) -> httpx.Response: return c.get(f"/api{path}", headers={"Content-Type": "application/json"})