srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/server
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2025-12-04 20:58:00 +0200
committersrdusr <[email protected]>2025-12-04 20:58:00 +0200
commitf48fa830b10d4122d145e9822522d9b71e3f00fa (patch)
tree3ab1696b2fae87c49c056bbd56ad1975c3b7ca5d /crates/server
parent3d26cb457b2023d686136f1c9ee014c210951729 (diff)
downloadtyperpunk-f48fa830b10d4122d145e9822522d9b71e3f00fa.tar.gz
typerpunk-f48fa830b10d4122d145e9822522d9b71e3f00fa.zip
Fix the multiplayer race hand-off, fill the field, and polish controls
Standings on the end screen - A race used to end on your own numbers alone. The end screen now shows the whole field, filling in live as the slower racers come home. - Two teardown paths were killing the feed it depends on. app.js tears the race screen down before rendering the end screen, and that ran both the inner cleanup (unsubscribing progress and finished) and the outer one (closing the socket). Your own PlayerFinished arrives from the server a moment after your finish callback runs, so nobody - including you - ever got a place. The screen now marks the feed as handed off and leaves it to the end screen, which drops it on its own teardown. More racers - Up to four bots per room instead of two, biased towards a fuller lobby. With four, picking one of two speed tiers put pairs on top of each other, so a bot now takes a pace at least 9 wpm clear of every other bot in the room, drawn from the whole slow-to-fast span. Accuracy tracks speed rather than being rolled independently, which had been pairing 80 wpm with 88% and 35 wpm with 99%. Lobby - The countdown is the whole screen for its three seconds, instead of a small number under a large disabled button. Quick match says "Finding a race" rather than offering a room code to share that nobody needs. Controls - Keyboard focus was invisible almost everywhere: only two inputs and the theme toggle had a :focus rule, and four other controls set outline:none outright. A :focus-visible ring now covers every interactive control, drawn as a box-shadow so it survives those declarations. - Disabled buttons no longer keep the filled hover state of a live primary action, placeholders have a defined colour rather than a per-engine default, inputs gained hover feedback and a consistent width, and pressed controls acknowledge the press.
Diffstat (limited to 'crates/server')
-rw-r--r--crates/server/src/multiplayer.rs56
1 files changed, 35 insertions, 21 deletions
diff --git a/crates/server/src/multiplayer.rs b/crates/server/src/multiplayer.rs
index 54df6b2..8974c8c 100644
--- a/crates/server/src/multiplayer.rs
+++ b/crates/server/src/multiplayer.rs
@@ -40,11 +40,16 @@ const BOT_SLOW_WPM: (f32, f32) = (34.0, 48.0);
const BOT_FAST_WPM: (f32, f32) = (72.0, 88.0);
// Nobody types perfectly. A bot's accuracy sets both what it reports at the
// finish and how often it stalls mid-race to "correct" itself.
+// No two bots in a room may land within this of each other.
+const BOT_MIN_SEPARATION_WPM: f32 = 9.0;
const BOT_MIN_ACCURACY: f32 = 88.0;
const BOT_MAX_ACCURACY: f32 = 99.0;
// How long a correction costs, in ticks.
const BOT_CORRECTION_TICKS: u32 = 2;
-const MAX_BOTS_PER_ROOM: usize = 2;
+// Enough for a race that feels like a field rather than a duel. Bots are
+// still capped below QUICK_MATCH_CAPACITY so a real player arriving always
+// has a slot.
+const MAX_BOTS_PER_ROOM: usize = 4;
const BOT_NAMES: &[&str] = &[
"Ghostwire", "NullPointer", "Kanji", "Sable", "Vex", "Orbit", "Static",
"Halcyon", "Nyx", "Drift", "Ember", "Kilo", "Rune", "Zephyr", "Onyx",
@@ -454,7 +459,9 @@ async fn maybe_add_bot(room_arc: Arc<Mutex<Room>>, state: Arc<AppState>) {
if free == 0 {
return;
}
- let wanted = rand::thread_rng().gen_range(1..=free);
+ // Biased towards a fuller room: one opponent is a duel, and the point
+ // of filling an empty lobby is that it feels populated.
+ let wanted = rand::thread_rng().gen_range(free.min(2)..=free);
for _ in 0..wanted {
let taken: Vec<String> = room.players.values().map(|p| p.name.clone()).collect();
@@ -467,25 +474,24 @@ async fn maybe_add_bot(room_arc: Arc<Mutex<Room>>, state: Arc<AppState>) {
}
let name = available[rand::thread_rng().gen_range(0..available.len())];
- // Take whichever tier is not already represented, so the room's
- // bots never sit at similar speeds. With none present, pick at
- // random.
- let has_fast = room
+ // Spread across the whole slow-to-fast span, keeping a clear gap
+ // from every bot already in the room. Two tiers were enough when
+ // there were at most two bots; with four, picking a tier would put
+ // pairs on top of each other.
+ let existing: Vec<f32> = room
.players
.values()
- .any(|p| p.is_bot && p.target_wpm >= BOT_FAST_WPM.0);
- let has_slow = room
- .players
- .values()
- .any(|p| p.is_bot && p.target_wpm <= BOT_SLOW_WPM.1);
- let fast = if has_fast {
- false
- } else if has_slow {
- true
- } else {
- rand::thread_rng().gen_bool(0.5)
- };
- let (lo, hi) = if fast { BOT_FAST_WPM } else { BOT_SLOW_WPM };
+ .filter(|p| p.is_bot)
+ .map(|p| p.target_wpm)
+ .collect();
+ let mut target = rand::thread_rng().gen_range(BOT_SLOW_WPM.0..BOT_FAST_WPM.1);
+ for _ in 0..12 {
+ if existing.iter().all(|w| (w - target).abs() >= BOT_MIN_SEPARATION_WPM) {
+ break;
+ }
+ target = rand::thread_rng().gen_range(BOT_SLOW_WPM.0..BOT_FAST_WPM.1);
+ }
+ let lo = target;
// The receiver is dropped immediately: a bot has no socket, and
// every send to it fails silently, which is what broadcast expects.
@@ -498,8 +504,16 @@ async fn maybe_add_bot(room_arc: Arc<Mutex<Room>>, state: Arc<AppState>) {
ready: true,
sender: tx,
is_bot: true,
- target_wpm: rand::thread_rng().gen_range(lo..hi),
- target_accuracy: rand::thread_rng().gen_range(BOT_MIN_ACCURACY..BOT_MAX_ACCURACY),
+ target_wpm: lo,
+ target_accuracy: {
+ // Faster bots are also the more accurate ones, which
+ // is how real typists distribute - a random pairing
+ // produced 80wpm at 88% and 35wpm at 99%.
+ let span = BOT_FAST_WPM.1 - BOT_SLOW_WPM.0;
+ let t = ((lo - BOT_SLOW_WPM.0) / span).clamp(0.0, 1.0);
+ let base = BOT_MIN_ACCURACY + t * (BOT_MAX_ACCURACY - BOT_MIN_ACCURACY);
+ (base + rand::thread_rng().gen_range(-2.0..2.0)).clamp(BOT_MIN_ACCURACY, BOT_MAX_ACCURACY)
+ },
},
);
}