Spieler und Befehle

Ein Spieler hat eine dauerhafte Kennung, einen Namen, eine
Werkzeugstufe und ein Inventar. Befehle (Beitreten, Verlassen, Graben)
sind der einzige Weg, die Welt zu ändern, und lassen sich kodieren.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Steven Wroblewski
2026-09-30 20:56:51 +02:00
parent 1a0be06c3b
commit 7f904b5e1d
5 changed files with 310 additions and 0 deletions
+96
View File
@@ -0,0 +1,96 @@
use grovewright_core::codec::{DecodeError, Reader, Writer};
use grovewright_core::command::Command;
use grovewright_core::map::Pos;
use grovewright_core::player::{PlayerId, MAX_NAME_BYTES};
fn round_trip(command: &Command) -> Command {
let mut w = Writer::new();
command.encode(&mut w);
let bytes = w.into_bytes();
let mut r = Reader::new(&bytes);
let decoded = Command::decode(&mut r).expect("command decodes");
r.finish().expect("no bytes are left over");
decoded
}
#[test]
fn every_command_survives_encoding() {
let commands = [
Command::Join {
key: [7; 16],
name: "Ada".to_owned(),
},
Command::Leave {
player: PlayerId(3),
},
Command::Dig {
player: PlayerId(1),
pos: Pos::new(-4, 1_000),
},
];
for command in &commands {
assert_eq!(&round_trip(command), command);
}
}
#[test]
fn an_overlong_name_is_shortened_when_encoded() {
let command = Command::Join {
key: [1; 16],
name: "x".repeat(MAX_NAME_BYTES + 10),
};
assert_eq!(
round_trip(&command),
Command::Join {
key: [1; 16],
name: "x".repeat(MAX_NAME_BYTES),
}
);
}
#[test]
fn an_unknown_command_is_rejected() {
assert_eq!(
Command::decode(&mut Reader::new(&[0xEE])),
Err(DecodeError::InvalidValue("command tag"))
);
}
#[test]
fn a_cut_off_command_is_rejected() {
let mut w = Writer::new();
Command::Dig {
player: PlayerId(0),
pos: Pos::new(1, 2),
}
.encode(&mut w);
let bytes = w.into_bytes();
assert_eq!(
Command::decode(&mut Reader::new(&bytes[..bytes.len() - 1])),
Err(DecodeError::UnexpectedEnd)
);
}
#[test]
fn several_commands_can_follow_each_other_in_one_buffer() {
let commands = vec![
Command::Leave {
player: PlayerId(2),
},
Command::Dig {
player: PlayerId(0),
pos: Pos::new(5, 6),
},
];
let mut w = Writer::new();
for command in &commands {
command.encode(&mut w);
}
let bytes = w.into_bytes();
let mut r = Reader::new(&bytes);
let mut decoded = Vec::new();
while r.remaining() > 0 {
decoded.push(Command::decode(&mut r).unwrap());
}
assert_eq!(decoded, commands);
}
+57
View File
@@ -0,0 +1,57 @@
use grovewright_core::codec::{DecodeError, Reader, Writer};
use grovewright_core::inventory::ITEM_STONE;
use grovewright_core::player::{clamp_name, Player, MAX_NAME_BYTES, STARTING_TOOL_TIER};
#[test]
fn a_new_player_is_online_with_a_stone_pickaxe_and_empty_pockets() {
let player = Player::new([5; 16], "Ada");
assert_eq!(player.key, [5; 16]);
assert_eq!(player.name, "Ada");
assert!(player.online);
assert_eq!(player.tool_tier, STARTING_TOOL_TIER);
assert_eq!(player.next_action_tick, 0);
assert!(player.inventory.slots().iter().all(Option::is_none));
}
#[test]
fn long_names_are_cut_at_a_character_boundary() {
// "ä" takes two bytes, so twenty of them are forty bytes.
let long = "ä".repeat(20);
let clamped = clamp_name(&long);
assert_eq!(clamped, "ä".repeat(16));
assert_eq!(clamped.len(), MAX_NAME_BYTES);
// With one byte in front, the limit falls into the middle of a character.
let odd = format!("a{}", "ä".repeat(20));
assert_eq!(clamp_name(&odd), format!("a{}", "ä".repeat(15)));
assert_eq!(clamp_name("Ada"), "Ada");
assert_eq!(Player::new([0; 16], &long).name, clamped);
}
#[test]
fn a_player_survives_encoding() {
let mut player = Player::new([9; 16], "Grace");
player.online = false;
player.next_action_tick = 1234;
player.inventory.add(ITEM_STONE, 17);
let mut w = Writer::new();
player.encode(&mut w);
let bytes = w.into_bytes();
let mut r = Reader::new(&bytes);
assert_eq!(Player::decode(&mut r), Ok(player));
assert_eq!(r.finish(), Ok(()));
}
#[test]
fn decoding_rejects_an_overlong_name() {
let mut w = Writer::new();
w.raw(&[0; 16]);
w.str(&"x".repeat(MAX_NAME_BYTES + 1));
let bytes = w.into_bytes();
assert_eq!(
Player::decode(&mut Reader::new(&bytes)),
Err(DecodeError::InvalidValue("player name"))
);
}