Skip to content

Commit

Permalink
feat: garbage
Browse files Browse the repository at this point in the history
  • Loading branch information
mgerhold committed Oct 26, 2024
1 parent 9e3d4d0 commit 98ef1b1
Show file tree
Hide file tree
Showing 18 changed files with 326 additions and 60 deletions.
3 changes: 2 additions & 1 deletion src/obpf/include/obpf/tetromino_type.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ extern "C" {
OBPF_TETROMINO_TYPE_S,
OBPF_TETROMINO_TYPE_T,
OBPF_TETROMINO_TYPE_Z,
OBPF_TETROMINO_TYPE_LAST = OBPF_TETROMINO_TYPE_Z,
OBPF_TETROMINO_TYPE_GARBAGE,
OBPF_TETROMINO_TYPE_LAST = OBPF_TETROMINO_TYPE_GARBAGE,
} ObpfTetrominoType;

#ifdef __cplusplus
Expand Down
7 changes: 5 additions & 2 deletions src/obpf/simulator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,10 @@ uint64_t obpf_tetrion_get_next_frame(ObpfTetrion const* const tetrion) try {
}

void obpf_tetrion_simulate_next_frame(ObpfTetrion* const tetrion, ObpfKeyState const key_state) try {
tetrion->simulate_next_frame(KeyState::from_bitmask(key_state.bitmask).value());
// Ignoring the return value because the client application should not need to know anything about
// the garbage that was sent. This should all be handled internally by the MultiplayerTetrion (in
// case of being a client) or directly through the C++ API (in case of being a server).
std::ignore = tetrion->simulate_next_frame(KeyState::from_bitmask(key_state.bitmask).value());
} catch (std::exception const& e) {

spdlog::error("Failed to simulate next frame: {}", e.what());
Expand Down Expand Up @@ -345,7 +348,7 @@ ObpfStats obpf_tetrion_get_stats(ObpfTetrion const* tetrion) try {
}

bool obpf_tetrion_is_game_over(ObpfTetrion const* const tetrion) try {
return tetrion->is_game_over();
return tetrion->game_over_since_frame().has_value();
} catch (std::exception const& e) {

spdlog::error("Failed to check if game is over: {}", e.what());
Expand Down
55 changes: 48 additions & 7 deletions src/server/server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ void Server::process_client(std::stop_token const& stop_token, Server& self, std
spdlog::info("heartbeat message frame: {}", heartbeat_message.frame);

self.m_client_infos.apply([&heartbeat_message, index](std::vector<ClientInfo>& client_infos) {
auto& client_info = client_infos.at(index);

auto& tetrion = client_info.tetrion;
for (auto const key_state : heartbeat_message.key_states) {
client_info.key_states.push_back(key_state);
tetrion.simulate_next_frame(key_state);
// Queue all the key states and simulate the frames on the main thread when
// the states of all clients have arrived. We cannot simulate the tetrions right
// here because they can influence each other via sent garbage. This has to
// be synchronized.
client_infos.at(index).key_states.push_back(key_state);
}
});
}
Expand Down Expand Up @@ -82,6 +82,10 @@ void Server::process_client(std::stop_token const& stop_token, Server& self, std
void Server::keep_broadcasting(std::stop_token const& stop_token, Server& self) {
using namespace std::chrono_literals;

while (self.m_expected_player_count == 0) {
std::this_thread::sleep_for(10ms);
}

// wait for all clients to connect
while (not stop_token.stop_requested()) {
auto const num_connected_clients =
Expand Down Expand Up @@ -121,6 +125,41 @@ void Server::keep_broadcasting(std::stop_token const& stop_token, Server& self)
return num_clients_connected;
}

// go through all the connected clients and determine the minimum number of key states
// that have been queued up for all clients
auto const min_num_key_states_queued = std::ranges::min(
client_infos | std::views::filter([](auto const& client_info) { return client_info.is_connected; })
| std::views::transform([](auto const& client_info) { return client_info.key_states.size(); })
);

for (auto i = usize{ 0 }; i < min_num_key_states_queued; ++i) {
auto garbage_send_events = std::unordered_map<u8, GarbageSendEvent>{};
for (auto& client_info : client_infos) {
if (not client_info.is_connected) {
continue;
}
auto const key_state = client_info.key_states.at(i);
auto& tetrion = client_info.tetrion;
// clang-format off
if (
auto const garbage_send_event = tetrion.simulate_next_frame(key_state);
garbage_send_event.has_value()
) { // clang-format on
garbage_send_events.emplace(client_info.id, garbage_send_event.value());
}
}
auto const tetrions =
client_infos | std::views::transform([](auto& client_info) { return &client_info.tetrion; })
| std::ranges::to<std::vector>();
for (auto const [sender_client_id, garbage_send_event] : garbage_send_events) {
auto target_tetrion =
determine_garbage_target(tetrions, sender_client_id, garbage_send_event.frame);
if (target_tetrion.has_value()) {
target_tetrion.value().receive_garbage(garbage_send_event);
}
}
}

// first we need to find the minimum number of frames simulated by any client that is connected
auto const min_num_frames_simulated = std::ranges::min(
client_infos | std::views::filter([](auto const& client_info) { return client_info.is_connected; })
Expand All @@ -133,8 +172,10 @@ void Server::keep_broadcasting(std::stop_token const& stop_token, Server& self)
while (client_info.tetrion.next_frame() < min_num_frames_simulated) {
static constexpr auto key_state = KeyState{};
client_info.key_states.push_back(key_state);
// we simulate the frame here, because we won't receive any key states from the client
client_info.tetrion.simulate_next_frame(key_state);
// We simulate the frame here, because we won't receive any key states from the client.
// We ignore the return value because this client is not allowed to send any garbage since
// it is not connected anymore.
std::ignore = client_info.tetrion.simulate_next_frame(key_state);
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions src/server/server.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include <spdlog/spdlog.h>
#include <atomic>
#include <cstdint>
#include <lib2k/random.hpp>
#include <simulator/tetrion.hpp>
Expand All @@ -23,9 +24,9 @@ class Server final {
c2k::ServerSocket m_server_socket;
std::vector<c2k::ClientSocket> m_client_sockets;
c2k::Synchronized<std::vector<ClientInfo>> m_client_infos;
std::atomic_size_t m_expected_player_count = 0;
std::vector<std::jthread> m_client_threads;
std::jthread m_broadcasting_thread;
std::size_t m_expected_player_count;
std::uint8_t m_next_client_id = 0;
std::atomic_flag m_should_stop;
c2k::Random::Seed m_seed;
Expand All @@ -45,7 +46,7 @@ class Server final {
m_seed{ c2k::Random{}.next_integral<c2k::Random::Seed>() } {
// todo: timeout
m_expected_player_count = static_cast<std::size_t>(m_lobby_socket.value().receive<std::uint16_t>().get());
spdlog::info("expected player count: {}", m_expected_player_count);
spdlog::info("expected player count: {}", m_expected_player_count.load());

m_client_sockets.reserve(m_expected_player_count);
m_client_infos.apply([this](std::vector<ClientInfo>& client_infos) {
Expand All @@ -69,7 +70,7 @@ class Server final {
m_seed{ c2k::Random{}.next_integral<c2k::Random::Seed>() } {
// todo: timeout
m_expected_player_count = num_expected_players;
spdlog::info("expected player count: {}", m_expected_player_count);
spdlog::info("expected player count: {}", m_expected_player_count.load());

m_client_sockets.reserve(m_expected_player_count);
m_client_infos.apply([this](std::vector<ClientInfo>& client_infos) {
Expand Down
4 changes: 3 additions & 1 deletion src/simulator/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ add_library(simulator STATIC
multiplayer_tetrion.cpp
include/simulator/observer_tetrion.hpp
observer_tetrion.cpp
include/simulator/garbage.hpp
garbage.cpp
)

target_include_directories(simulator
Expand All @@ -41,8 +43,8 @@ target_link_system_libraries(simulator
PUBLIC
Microsoft.GSL::GSL
lib2k
tl::optional
PRIVATE
spdlog::spdlog
tl::optional
magic_enum::magic_enum
)
40 changes: 40 additions & 0 deletions src/simulator/garbage.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#include <cassert>
#include <map>
#include <ranges>
#include <simulator/garbage.hpp>
#include <simulator/tetrion.hpp>

[[nodiscard]] tl::optional<ObpfTetrion&> determine_garbage_target(
std::vector<ObpfTetrion*> const& tetrions,
u8 const sender_tetrion_id,
u64 const frame
) {
if (tetrions.size() < 2) {
assert(tetrions.empty() or tetrions.front()->id() == sender_tetrion_id);
return tl::nullopt;
}

auto alive_tetrions = std::map<u8, ObpfTetrion*>{};
for (auto const tetrion : tetrions) {
if (tetrion->id() == sender_tetrion_id) {
continue;
}
if (auto const game_over_since_frame = tetrion->game_over_since_frame();
game_over_since_frame.has_value() and frame >= game_over_since_frame.value()) {
continue;
}
alive_tetrions[tetrion->id()] = tetrion;
}
if (alive_tetrions.empty()) {
return tl::nullopt;
}
auto const find_iterator = std::ranges::find_if(alive_tetrions, [sender_tetrion_id](auto const& pair) {
auto const id = pair.first;
return id > sender_tetrion_id;
});
if (find_iterator != alive_tetrions.end()) {
return *(find_iterator->second);
}
assert(alive_tetrions.begin()->first != sender_tetrion_id);
return *(alive_tetrions.begin()->second);
}
21 changes: 21 additions & 0 deletions src/simulator/include/simulator/garbage.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#pragma once

#include <lib2k/types.hpp>
#include <tl/optional.hpp>
#include <vector>

struct GarbageSendEvent {
u64 frame;
u8 num_lines;

explicit constexpr GarbageSendEvent(u64 const frame, u8 const num_lines)
: frame{ frame }, num_lines{ num_lines } {}
};

struct ObpfTetrion;

[[nodiscard]] tl::optional<ObpfTetrion&> determine_garbage_target(
std::vector<ObpfTetrion*> const& tetrions,
u8 sender_tetrion_id,
u64 frame
);
8 changes: 7 additions & 1 deletion src/simulator/include/simulator/multiplayer_tetrion.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include <deque>
#include <lib2k/static_vector.hpp>
#include <network/constants.hpp>
#include <network/messages.hpp>
Expand All @@ -19,6 +20,7 @@ struct MultiplayerTetrion final : ObpfTetrion {
std::jthread m_receiving_thread;
std::vector<std::unique_ptr<ObserverTetrion>> m_observers;
c2k::Synchronized<std::deque<std::unique_ptr<AbstractMessage>>> m_message_queue{ {} };
c2k::Synchronized<std::deque<GarbageSendEvent>> m_outgoing_garbage_queue{ {} };

struct Key {};

Expand All @@ -45,10 +47,14 @@ struct MultiplayerTetrion final : ObpfTetrion {
m_receiving_thread{ keep_receiving, std::ref(m_socket), std::ref(m_message_queue) },
m_observers{ std::move(observers) } {}

void simulate_next_frame(KeyState key_state) override;
[[nodiscard]] std::optional<GarbageSendEvent> simulate_next_frame(KeyState key_state) override;
[[nodiscard]] std::vector<ObserverTetrion*> get_observers() const override;
void on_client_disconnected(u8 client_id) override;

[[nodiscard]] u8 id() const override {
return m_client_id;
}

private:
void send_heartbeat_message();
void process_state_broadcast_message(StateBroadcast const& message);
Expand Down
8 changes: 5 additions & 3 deletions src/simulator/include/simulator/observer_tetrion.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ struct ObserverTetrion final : ObpfTetrion {
ObserverTetrion(u64 const seed, u64 const start_frame, u8 const m_client_id, Key)
: ObpfTetrion{ seed, start_frame }, m_client_id{ m_client_id } {}

void simulate_next_frame(KeyState) override {}
[[nodiscard]] std::optional<GarbageSendEvent> simulate_next_frame(KeyState) override {
return std::nullopt;
}

[[nodiscard]] u8 client_id() const {
[[nodiscard]] u8 id() const override {
return m_client_id;
}

Expand All @@ -30,5 +32,5 @@ struct ObserverTetrion final : ObpfTetrion {
}

private:
void process_key_state(KeyState key_state);
[[nodiscard]] std::optional<GarbageSendEvent> process_key_state(KeyState key_state);
};
30 changes: 23 additions & 7 deletions src/simulator/include/simulator/tetrion.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <common/common.h>
#include <array>
#include <cstdint>
#include <deque>
#include <lib2k/random.hpp>
#include <lib2k/static_vector.hpp>
#include <lib2k/types.hpp>
Expand All @@ -12,6 +13,7 @@
#include "bag.hpp"
#include "delayed_auto_shift.hpp"
#include "entry_delay.hpp"
#include "garbage.hpp"
#include "input.hpp"
#include "key_state.hpp"
#include "line_clear_delay.hpp"
Expand All @@ -25,6 +27,7 @@ struct ObpfTetrion {
private:
static constexpr auto spawn_position = Vec2{ 3, 0 };
static constexpr auto spawn_rotation = Rotation::North;
static constexpr auto garbage_delay_frames = u64{ 90 };

ObpfActionHandler m_action_handler = nullptr;
void* m_action_handler_user_data = nullptr;
Expand All @@ -37,8 +40,9 @@ struct ObpfTetrion {
u64 m_start_frame;
u64 m_next_frame = 0;
KeyState m_last_key_state;
std::mt19937_64 m_random;
std::mt19937_64 m_bags_rng;
std::array<Bag, 2> m_bags;
std::mt19937_64 m_garbage_rng;
usize m_bag_index = 0;
DelayedAutoShiftState m_auto_shift_state;
LockDelayState m_lock_delay_state;
Expand All @@ -48,7 +52,8 @@ struct ObpfTetrion {
u64 m_score = 0;
u64 m_next_gravity_frame = gravity_delay_by_level(0); // todo: offset by starting frame given by the server
bool m_is_soft_dropping = false;
bool m_is_game_over = false;
std::optional<u64> m_game_over_since_frame;
std::deque<GarbageSendEvent> m_garbage_receive_queue;

static constexpr u64 gravity_delay_by_level(u32 const level) {
constexpr auto delays = std::array<u64, 13>{
Expand All @@ -64,7 +69,7 @@ struct ObpfTetrion {
};

explicit ObpfTetrion(u64 const seed, u64 const start_frame)
: m_start_frame{ start_frame }, m_random{ seed }, m_bags{ create_two_bags(m_random) } {
: m_start_frame{ start_frame }, m_bags_rng{ seed }, m_bags{ create_two_bags(m_bags_rng) }, m_garbage_rng{ seed } {
static_assert(std::same_as<std::remove_const_t<decltype(seed)>, c2k::Random::Seed>);
}

Expand All @@ -79,6 +84,10 @@ struct ObpfTetrion {
m_action_handler_user_data = user_data;
}

[[nodiscard]] virtual u8 id() const {
return 0;
}

[[nodiscard]] Matrix& matrix() {
return m_matrix;
}
Expand All @@ -95,13 +104,16 @@ struct ObpfTetrion {
return m_ghost_tetromino;
}

virtual void simulate_next_frame(KeyState key_state);
void apply_expired_garbage();
[[nodiscard]] virtual std::optional<GarbageSendEvent> simulate_next_frame(KeyState key_state);
[[nodiscard]] virtual std::vector<ObserverTetrion*> get_observers() const;
virtual void on_client_disconnected(u8 client_id);
[[nodiscard]] LineClearDelay::State line_clear_delay_state() const;
[[nodiscard]] std::array<TetrominoType, 6> get_preview_tetrominos() const;
[[nodiscard]] std::optional<TetrominoType> hold_piece() const;

void receive_garbage(GarbageSendEvent garbage);

[[nodiscard]] u64 next_frame() const {
return m_next_frame;
}
Expand All @@ -116,8 +128,8 @@ struct ObpfTetrion {
return m_num_lines_cleared;
}

[[nodiscard]] bool is_game_over() const {
return m_is_game_over;
[[nodiscard]] std::optional<u64> game_over_since_frame() const {
return m_game_over_since_frame;
}

[[nodiscard]] virtual bool is_observer() const {
Expand Down Expand Up @@ -153,11 +165,15 @@ struct ObpfTetrion {
void rotate_counter_clockwise();
void hard_drop();
void hold();
void determine_lines_to_clear();
[[nodiscard]] bool determine_lines_to_clear();
[[nodiscard]] u64 score_for_num_lines_cleared(std::size_t num_lines_cleared) const;
void clear_lines(c2k::StaticVector<u8, 4> lines);
void refresh_ghost_tetromino();
void on_touch_event() const;

[[nodiscard]] bool is_game_over() const {
return m_game_over_since_frame.has_value();
}

[[nodiscard]] static std::array<Bag, 2> create_two_bags(std::mt19937_64& random);
};
Loading

0 comments on commit 98ef1b1

Please sign in to comment.