diff --git a/services/api/src/matches/daily.rs b/services/api/src/matches/daily.rs index d374169..c840369 100644 --- a/services/api/src/matches/daily.rs +++ b/services/api/src/matches/daily.rs @@ -195,8 +195,11 @@ pub fn score(mine: SideResult, theirs: SideResult) -> Option { /// This is also what a co-op daily needs: a second human seated on the player's /// team is summed into the same side rather than counted against it. /// -/// A row with no battle value is skipped rather than trusted: an observer has -/// none, and so does a result written before arena reported it. +/// A row with no battle value is skipped rather than trusted: a watcher seat +/// has none, and so does a result written before arena reported it. That is +/// also the whole of the test - the `observer` flag is not usable here, since +/// MegaMek sets it on a side that has lost every machine, and summing a +/// challenge without the side that lost is how a defeat scores as a walkover. pub fn sides_from_result(result: &serde_json::Value) -> Option<(SideResult, SideResult)> { let players = result.get("players")?.as_array()?; let mine_team = players @@ -214,9 +217,6 @@ pub fn sides_from_result(result: &serde_json::Value) -> Option<(SideResult, Side bv_left: 0, }; for row in players { - if row.get("observer").and_then(|o| o.as_bool()) == Some(true) { - continue; - } let (Some(team), Some(bv_start), Some(bv_left)) = ( row.get("team").and_then(|t| t.as_i64()), row.get("bvStart").and_then(|b| b.as_i64()), diff --git a/services/api/src/share.rs b/services/api/src/share.rs index a2ea148..784c34c 100644 --- a/services/api/src/share.rs +++ b/services/api/src/share.rs @@ -446,9 +446,14 @@ const PLAYER_NONE: i64 = -1; /// The seats MegaMek holds for us rather than for a player: the watcher that /// spools turn reports, and the seat a browser takes in a match nobody is -/// playing. Dropped by name as well as by the server's observer flag, which -/// is only set once every human seat has arrived - the same rule the match -/// page applies in arena's `suramadu/web/report.js`, and for the same reason. +/// playing. Dropped by name, and by never having owned a unit. +/// +/// **Not** by the `observer` flag, which cannot be used for this. MegaMek +/// sets it on any player holding no entities outside the lounge, so a force +/// that was wiped out ends the match flagged as a watcher - and arena records +/// each seat as the game last described it. A side that lost every machine is +/// the loser, not a spectator, and dropping it leaves a decided match with no +/// loser in it to name. const WATCHERS: [&str; 2] = ["ArenaObserver", "ArenaSpectator"]; struct Force { @@ -508,9 +513,6 @@ fn summarise(result: &serde_json::Value) -> Option { let players = result.get("players")?.as_array()?; let mut forces = Vec::new(); for player in players { - if player.get("observer").and_then(serde_json::Value::as_bool) == Some(true) { - continue; - } let name = player .get("name") .and_then(serde_json::Value::as_str) @@ -2628,6 +2630,40 @@ letter-spacing:.06em}\ #[cfg(test)] mod tests { + + /// A side that lost every machine is flagged `observer` by MegaMek, and + /// arena records each seat as the game last described it. Dropping it took + /// the loser out of a decided match, which then read as a draw with one + /// force in it - match 06e4fd45, where the OPFOR seat held three Meks and + /// 3,142 battle value and did not appear on the report at all. + #[test] + fn a_wiped_out_side_is_the_loser_not_a_watcher() { + let result = serde_json::json!({ + "round": 5, + "victoryTeam": 1, + "victoryPlayerId": -1, + "players": [ + {"id": 0, "name": "@permadeath.com", "slot": "Challenger", "observer": false, + "team": 1, "unitsStart": 1, "unitsRemaining": 1, + "bvStart": 3132, "bvRemaining": 2766, "units": []}, + {"id": 1, "name": "@lance.blue", "slot": "OPFOR", "observer": true, + "team": 2, "unitsStart": 3, "unitsRemaining": 0, + "bvStart": 3142, "bvRemaining": 0, "units": []}, + {"id": 3, "name": "ArenaObserver", "slot": "ArenaObserver", "observer": true, + "team": -1, "unitsStart": 0, "unitsRemaining": 0, + "bvStart": 0, "bvRemaining": 0, "units": []} + ] + }); + + let summary = summarise(&result).expect("a summary"); + let names: Vec<&str> = summary.forces.iter().map(|f| f.name.as_str()).collect(); + assert_eq!( + names, + vec!["@permadeath.com", "@lance.blue"], + "the watcher goes, both fighters stay" + ); + assert_eq!(summary.title(), "@permadeath.com beat @lance.blue"); + } use super::*; /// One real machine, as arena's UnitReport wrote it: a Griffin with its