From ffa5e67b940ce7395c30529f7f03a2cf1cb6d521 Mon Sep 17 00:00:00 2001 From: Anthony Leedom Date: Wed, 14 Feb 2024 14:04:01 -0800 Subject: [PATCH] Add PONG example --- CMakeLists.txt | 1 + README.md | 11 + examples/CMakeLists.txt | 17 + examples/pong.cpp | 840 +++++++++++++++++++++++++++++++++++++++ include/esc/esc.hpp | 1 + include/esc/key.hpp | 158 ++++---- include/esc/point.hpp | 10 + include/esc/sequence.hpp | 13 - src/io.cpp | 175 ++++---- tests/CMakeLists.txt | 1 - 10 files changed, 1046 insertions(+), 181 deletions(-) create mode 100644 examples/CMakeLists.txt create mode 100644 examples/pong.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d9412b1..7c8b578 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,4 +82,5 @@ if (NOT TARGET Catch2) add_subdirectory(external/Catch2) endif() +add_subdirectory(examples) add_subdirectory(tests) \ No newline at end of file diff --git a/README.md b/README.md index b4ee35a..12d1b2c 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,20 @@ presentation and behavior. - [ICU Library](https://icu.unicode.org/): For Unicode support. +## Build Instructions + + git clone https://github.com/a-n-t-h-o-n-y/Escape.git + git submodule update --init --recursive # Pull in Dependencies + mkdir Escape/build && cd Escape/build + cmake .. + make escape # Build Library + make escape.tests.unit # Build Unit Tests (Optional) + make escape.examples.pong # Build PONG Game (Optional) + ## Example Code - [tools/termcaps.cpp](./tools/termcaps.cpp) +- [examples/pong.cpp](./examples/pong.cpp) - [tests/glyph.test.cpp](./tests/glyph.test.cpp) ## Planned Enhancements diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt new file mode 100644 index 0000000..84fa405 --- /dev/null +++ b/examples/CMakeLists.txt @@ -0,0 +1,17 @@ +add_executable(escape.examples.pong EXCLUDE_FROM_ALL + pong.cpp +) + +target_compile_options( + escape.examples.pong + PRIVATE + -Wall + -Wextra + -Wpedantic +) + +target_link_libraries( + escape.examples.pong + PRIVATE + escape +) \ No newline at end of file diff --git a/examples/pong.cpp b/examples/pong.cpp new file mode 100644 index 0000000..f25f4f4 --- /dev/null +++ b/examples/pong.cpp @@ -0,0 +1,840 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace esc; + +auto screen_dimensions = Area{.width = 0, .height = 0}; + +constexpr auto logo = std::string_view{ + R"('||''|. ..|''|| '|. '|' ..|'''.| + || || .|' || |'| | .|' ' + ||...|' || || | '|. | || .... + || '|. || | ||| '|. || +.||. ''|...|' .|. '| ''|...'| +)"}; + +// This is a std::variant based state machine, one struct per screen type with +// get_display_bytes() to retrieve the display for each state type and process() +// to handle events for each state. + +// STATES ---------------------------------------------------------------------- + +struct SplashScreen { + std::uint16_t hue{0}; + std::string_view display = logo; +}; + +struct Menu { + std::string title{"PONG"}; + + std::vector options{ + "Play", + "How-To", + "Quit", + }; + std::size_t selected{0}; +}; + +struct Game { + static constexpr auto game_space = Area{.width = 81, .height = 25}; + + struct AI { + int action_interval; + float reaction_threshold; + float velocity; + }; + + struct Player { + std::string name; + std::optional ai; + }; + + struct Location { + float x; + float y; + }; + + struct Paddle { + Location top; + float dy; + static constexpr auto height = 4.f; + }; + + struct { + std::size_t score; + Player player; + Paddle paddle{ + .top = {.x = 1, .y = (game_space.height - Paddle::height) / 2}, + .dy = 0, + }; + } left; + + struct { + std::size_t score; + Player player; + Paddle paddle{ + .top = {.x = game_space.width - 2, + .y = (game_space.height - Paddle::height) / 2}, + .dy = 0, + }; + } right; + + struct Ball { + Location at = {.x = game_space.width / 2, .y = game_space.height / 2}; + struct Velocity { + float dx = 0; + float dy = 0; + } velocity; + } ball; +}; + +struct PlayerSelectMenu { + std::string title{"Select Players"}; + + Game::Player left_player = { + .name = "Human", + .ai = std::nullopt, + }; + Game::Player right_player = { + .name = "Human", + .ai = std::nullopt, + }; + + bool left_selected{true}; + + std::vector options{ + { + "Slow AI", + Game::AI{ + .action_interval = 7, + .reaction_threshold = 60.f, + .velocity = 0.1f, + }, + }, + { + "Medium AI", + Game::AI{ + .action_interval = 5, + .reaction_threshold = 30.f, + .velocity = 0.2f, + }, + }, + { + "Fast AI", + Game::AI{ + .action_interval = 3, + .reaction_threshold = 25.f, + .velocity = 0.25f, + }, + }, + { + "Aggressive AI", + Game::AI{ + .action_interval = 1, + .reaction_threshold = 10.f, + .velocity = 0.75f, + }, + }, + { + "Human", + std::nullopt, + }, + }; + std::size_t selected{0}; +}; + +struct HowTo { + std::string title{"How to Play"}; + std::vector> instructions{ + {"Up/Down Arrow Keys", "Move the right paddle"}, + {"w/s Keys", "Move the left paddle"}, + {"Esc Key", "Return to the main menu"}, + {"Enter Key", "Start next round"}, + }; +}; + +using State = std::variant; + +// EVENTS ---------------------------------------------------------------------- + +// Event handlers return the next State. +// std::nullopt is a quit request +using EventResponse = std::optional; + +[[nodiscard]] auto process(KeyPress event, SplashScreen state) -> EventResponse +{ + switch (event.key) { + case Key::Enter: return Menu{}; + default: return state; + } +} + +[[nodiscard]] auto process(KeyPress event, Menu state) -> EventResponse +{ + switch (event.key) { + case Key::ArrowUp: + case Key::k: { + state.selected = (state.selected + state.options.size() - 1) % + state.options.size(); + return state; + } + + case Key::ArrowDown: + case Key::j: { + state.selected = (state.selected + 1) % state.options.size(); + return state; + } + + case Key::Enter: + switch (state.selected) { + case 0: return PlayerSelectMenu{}; + case 1: return HowTo{}; + case 2: return std::nullopt; + default: return state; + } + + default: return state; + } +} + +[[nodiscard]] auto generate_initial_velocity() -> Game::Ball::Velocity +{ + using int_dist = std::uniform_int_distribution; + using float_dist = std::uniform_real_distribution; + + auto rng = std::mt19937{std::random_device{}()}; + auto dx = float_dist{0.5, 0.8}(rng); + auto dy = float_dist{0.1, 0.4}(rng); + if (int_dist{0, 1}(rng) == 0) { + dx = -dx; + } + if (int_dist{0, 1}(rng) == 0) { + dy = -dy; + } + return {dx, dy}; +} + +[[nodiscard]] auto process(KeyPress event, Game state) -> EventResponse +{ + constexpr auto speed_increment = 0.5f; + + switch (event.key) { + case Key::Escape: return Menu{}; + + case Key::Enter: { + auto& ball = state.ball; + if (ball.velocity.dx == 0 && ball.velocity.dy == 0) { + ball.velocity = generate_initial_velocity(); + } + return state; + } + + case Key::ArrowUp: { + state.right.paddle.dy -= speed_increment; + return state; + } + case Key::ArrowDown: { + state.right.paddle.dy += speed_increment; + return state; + } + + case Key::w: { + state.left.paddle.dy -= speed_increment; + return state; + } + case Key::s: { + state.left.paddle.dy += speed_increment; + return state; + } + + default: return state; + } +} + +[[nodiscard]] auto process(KeyPress event, PlayerSelectMenu state) + -> EventResponse +{ + switch (event.key) { + case Key::Escape: return Menu{}; + + case Key::ArrowUp: + case Key::k: { + state.selected = (state.selected + state.options.size() - 1) % + state.options.size(); + return state; + } + + case Key::ArrowDown: + case Key::j: { + state.selected = (state.selected + 1) % state.options.size(); + return state; + } + + case Key::Enter: { + if (state.left_selected) { + state.left_player = state.options[state.selected]; + state.left_selected = false; + return state; + } + else { + state.right_player = state.options[state.selected]; + return Game{ + .left = {.player = state.left_player}, + .right = {.player = state.right_player}, + }; + } + } + default: return state; + } +} + +[[nodiscard]] auto process(KeyPress event, HowTo state) -> EventResponse +{ + switch (event.key) { + case Key::Escape: return Menu{}; + default: return state; + } +} + +[[nodiscard]] auto process(Resize event, auto state) -> EventResponse +{ + screen_dimensions = event.area; + return state; +} + +// Catch-All +[[nodiscard]] auto process(auto, auto state) -> EventResponse { return state; } + +// DISPLAY --------------------------------------------------------------------- + +[[nodiscard]] auto get_display_bytes(SplashScreen const& state, Area dimensions) + -> std::string +{ + std::uint16_t hue = state.hue; + + auto const padding = [&] { + auto const width = (int)state.display.find('\n'); + return std::string(std::max((dimensions.width - width) / 2, 0), ' '); + }(); + + auto hsl = HSL{ + .hue = hue, + .saturation = 90, + .lightness = 70, + }; + + auto bytes = escape(Cursor{.x = 0, .y = 6}, Trait::Bold); + bytes += escape(fg(TrueColor{hsl})) + padding; + for (char c : state.display) { + bytes += c; + if (c == '\n') { + hsl.hue = (hsl.hue + 20) % 360; + bytes += escape(fg(TrueColor{hsl})) + padding; + } + } + + bytes += escape(Brush{}); + + auto const enter = std::string{"Press Enter to continue"}; + bytes += escape(Cursor{ + .x = (screen_dimensions.width - (int)enter.size()) / 2, + .y = screen_dimensions.height - 2, + }); + return bytes += escape(Trait::Dim) + enter + escape(Trait::None); +} + +[[nodiscard]] auto get_display_bytes(Menu const& state, Area dimensions) + -> std::string +{ + auto const padding = [&] { + auto const width = (int)logo.find('\n'); + return std::string((dimensions.width - width) / 2, ' '); + }(); + + auto bytes = escape(Cursor{.x = 0, .y = 6}); + bytes += padding; + for (char c : logo) { + bytes += c; + if (c == '\n') { + bytes += padding; + } + } + + for (std::size_t i = 0; i < state.options.size(); ++i) { + bytes += escape(Cursor{ + .x = (dimensions.width - (int)state.options[i].size()) / 2, + .y = 14 + (int)i, + }); + if (i == state.selected) { + bytes += escape(Trait::Standout); + } + bytes += state.options[i] + escape(Trait::None); + } + + return bytes; +} + +[[nodiscard]] auto velocity_to_color(Game::Ball::Velocity velocity) -> Color +{ + auto const speed = + std::sqrt(velocity.dx * velocity.dx + velocity.dy * velocity.dy); + + return TrueColor{HSL{ + .hue = std::uint16_t((90 + (int)(speed * 160)) % 360), + .saturation = 80, + .lightness = 70, + }}; +} + +[[nodiscard]] auto get_display_bytes(Game const& state, Area dimensions) + -> std::string +{ + static constexpr auto display_space = Game::game_space; + + if (dimensions.width < display_space.width || + dimensions.height < display_space.height) { + return escape(Cursor{.x = 0, .y = 0}) + + "Terminal too small to display game"; + } + + auto const offset = Point{ + .x = (dimensions.width - display_space.width) / 2, + .y = (dimensions.height - display_space.height) / 2, + }; + + auto bytes = std::string{}; + + { // Net + bytes += escape(Trait::Dim); + for (int i = 0; i < display_space.height; ++i) { + bytes += escape(offset + Point{display_space.width / 2, i}) + "╳"; + } + bytes += escape(Trait::None); + } + + { // Ball + auto const glyph = [&] { + auto const y = state.ball.at.y; + auto const i = (std::size_t)((y - std::floor(y)) * 8); + return std::array{"▄", "▃", "▂", "▁", "█", "▇", "▆", "▅"}[i]; + }(); + + auto cursor = offset + Point{.x = (int)std::round(state.ball.at.x), + .y = (int)std::round(state.ball.at.y)}; + + bytes += escape(fg(velocity_to_color(state.ball.velocity))); + bytes += escape(cursor, Trait::Inverse) + glyph; + cursor.y -= 1; + bytes += escape(cursor, Trait::None) + glyph; + bytes += escape(Brush{}); + } + + { // Paddles + auto const paint_paddle = [&](Game::Paddle const& paddle) { + auto cursor = Cursor{ + .x = offset.x + (int)std::floor(paddle.top.x), + .y = offset.y + (int)std::floor(paddle.top.y), + }; + + auto bytes = escape(cursor); + + auto const edge = [&] { + auto const y = paddle.top.y; + auto const i = (std::size_t)((y - std::floor(y)) * 8); + return std::array{"█", "▇", "▆", "▅", "▄", "▃", "▂", "▁"}[i]; + }(); + + bytes += edge; + cursor.y += 1; + + for (int i = 1; i < Game::Paddle::height; ++i) { + bytes += escape(cursor) + "█"; + cursor.y += 1; + } + + return bytes += + escape(cursor, Trait::Inverse) + edge + escape(Trait::None); + }; + + bytes += paint_paddle(state.left.paddle); + bytes += paint_paddle(state.right.paddle); + } + + { // Scores + bytes += escape(offset - Point{.x = 0, .y = 2}); + bytes += state.left.player.name + ": "; + bytes += std::to_string(state.left.score); + + auto const rhs = state.right.player.name + ": "; + bytes += + escape(offset + Point{ + .x = display_space.width - (int)rhs.size() - 1, + .y = -2, + }); + bytes += rhs; + bytes += std::to_string(state.right.score); + } + + { // Border + bytes += escape(offset - Point{.x = 1, .y = 1}) + escape(Trait::Dim); + bytes += "╭"; + + for (int i = 0; i < display_space.width; ++i) { + bytes += "─"; + } + bytes += "╮"; + + for (int i = 0; i < display_space.height; ++i) { + bytes += escape(offset + Point{.x = -1, .y = i}); + bytes += "│"; + + bytes += escape(offset + Point{.x = display_space.width, .y = i}); + bytes += "│"; + } + + bytes += escape(offset + Point{.x = -1, .y = display_space.height}); + bytes += "╰"; + + for (int i = 0; i < display_space.width; ++i) { + bytes += "─"; + } + bytes += "╯" + escape(Trait::None); + } + + bytes += escape(Cursor{.x = 0, .y = dimensions.height - 1}); + bytes += escape(Trait::Dim) + "Press Esc to return to the main menu"; + + auto const enter = std::string{"Press Enter to start next round"}; + bytes += escape(Cursor{ + .x = dimensions.width - (int)enter.size(), + .y = dimensions.height - 1, + }); + bytes += enter + escape(Trait::None); + + return bytes; +} + +[[nodiscard]] auto get_display_bytes(PlayerSelectMenu const& state, + Area dimensions) -> std::string +{ + auto bytes = escape(Cursor{ + .x = (dimensions.width - (int)state.title.size()) / 2, + .y = 2, + }); + + bytes += escape(Trait::Bold | Trait::Underline); + bytes += state.title; + bytes += escape(Trait::None); + + bytes += escape(Cursor{.x = (dimensions.width) / 2 - 25, .y = 4}); + bytes += "Left Player: "; + if (state.left_selected) { + bytes += escape(Trait::Standout); + } + bytes += state.left_player.name; + bytes += escape(Trait::None); + + bytes += escape(Cursor{.x = (dimensions.width) / 2 + 10, .y = 4}); + bytes += "Right Player: "; + if (!state.left_selected) { + bytes += escape(Trait::Standout); + } + bytes += state.right_player.name; + bytes += escape(Trait::None); + + for (std::size_t i = 0; i < state.options.size(); ++i) { + bytes += escape(Cursor{ + .x = (dimensions.width - (int)state.options[i].name.size()) / 2, + .y = 6 + (int)i, + }); + if (i == state.selected) { + bytes += escape(Trait::Standout); + } + bytes += state.options[i].name; + bytes += escape(Trait::None); + } + + bytes += escape(Cursor{.x = 0, .y = dimensions.height - 1}); + bytes += escape(Trait::Dim) + "Press Esc to return to the main menu" + + escape(Trait::None); + + return bytes; +} + +[[nodiscard]] auto get_display_bytes(HowTo const& state, Area dimensions) + -> std::string +{ + auto bytes = escape(Cursor{ + .x = (dimensions.width - (int)state.title.size()) / 2, + .y = 2, + }); + + bytes += escape(Trait::Bold | Trait::Underline) + state.title + + escape(Trait::None); + + for (std::size_t i = 0; i < state.instructions.size(); ++i) { + bytes += escape(Cursor{ + .x = (dimensions.width - 40) / 2, + .y = 4 + (int)i, + }); + bytes += escape(Trait::Bold) + state.instructions[i][0] + + escape(Trait::None); + bytes += escape(Cursor{ + .x = (dimensions.width + 8) / 2, + .y = 4 + (int)i, + }); + bytes += state.instructions[i][1]; + } + + // Border + bytes += escape(Cursor{.x = 0, .y = 0}, Trait::Dim); + bytes += "╭"; + for (int i = 0; i < dimensions.width - 2; ++i) { + bytes += "─"; + } + bytes += "╮"; + for (int i = 0; i < dimensions.height - 2; ++i) { + bytes += escape(Cursor{.x = 0, .y = i + 1}); + bytes += "│"; + bytes += escape(Cursor{.x = dimensions.width - 1, .y = i + 1}); + bytes += "│"; + } + bytes += escape(Cursor{.x = 0, .y = dimensions.height - 1}); + bytes += "╰"; + for (int i = 0; i < dimensions.width - 2; ++i) { + bytes += "─"; + } + bytes += "╯"; + bytes += escape(Trait::None); + + // Press Esc + bytes += escape(Cursor{.x = 2, .y = dimensions.height - 1}); + bytes += escape(Trait::Dim) + "Press Esc to return to the main menu" + + escape(Trait::None); + + return bytes; +} + +// INCREMENT ------------------------------------------------------------------- + +[[nodiscard]] auto increment_game_state(Game state) -> std::optional +{ + auto& ball = state.ball; + auto& left = state.left; + auto& right = state.right; + + // paddle/ball collisions + if (ball.velocity.dx < 0.f && ball.at.x < 2.f && + ball.at.y + 0.5f >= left.paddle.top.y && + ball.at.y - 0.5f <= left.paddle.top.y + Game::Paddle::height) { + ball.velocity.dx *= -1.07; + ball.velocity.dy += left.paddle.dy * 0.25f; + ball.at.x = 2.f; + } + else if (ball.velocity.dx > 0.f && + ball.at.x > Game::game_space.width - 3.f && + ball.at.y + 0.5f >= right.paddle.top.y && + ball.at.y - 0.5f <= right.paddle.top.y + Game::Paddle::height) { + ball.velocity.dx *= -1.07; + ball.velocity.dy += right.paddle.dy * 0.25f; + ball.at.x = Game::game_space.width - 3.f; + } + + // move paddles + left.paddle.top.y += left.paddle.dy; + right.paddle.top.y += right.paddle.dy; + + // move ball + ball.at.x += ball.velocity.dx; + ball.at.y += ball.velocity.dy; + + // slow down paddles + left.paddle.dy *= 0.9; + right.paddle.dy *= 0.9; + + // slow down ball + ball.velocity.dx *= 0.9999; + ball.velocity.dy *= 0.999; + + // check for ball/wall collisions + if (ball.at.y <= 0.5) { + ball.velocity.dy = -ball.velocity.dy; + ball.at.y = 0.51; + } + else if (ball.at.y >= Game::game_space.height - 0.5) { + ball.velocity.dy = -ball.velocity.dy; + ball.at.y = Game::game_space.height - 0.51; + } + + constexpr auto init_ball = Game::Ball{ + .at = {.x = Game::game_space.width / 2, + .y = Game::game_space.height / 2}, + .velocity = {0, 0}, + }; + + // check for score and reset + if (ball.at.x < 0) { + right.score++; + ball = init_ball; + } + else if (ball.at.x > Game::game_space.width - 1) { + left.score++; + ball = init_ball; + } + + // check for paddle/wall collisions + if (left.paddle.top.y < 0) { + left.paddle = { + .top = {.x = 1, .y = 0}, + .dy = 0, + }; + } + else if (left.paddle.top.y > + Game::game_space.height - Game::Paddle::height) { + left.paddle = { + .top = {.x = 1, + .y = Game::game_space.height - Game::Paddle::height}, + .dy = 0, + }; + } + + if (right.paddle.top.y < 0) { + right.paddle = { + .top = {.x = Game::game_space.width - 2, .y = 0}, + .dy = 0, + }; + } + else if (right.paddle.top.y > + Game::game_space.height - Game::Paddle::height) { + right.paddle = { + .top = {.x = Game::game_space.width - 2, + .y = Game::game_space.height - Game::Paddle::height}, + .dy = 0, + }; + } + + // AI Left Player + if (left.player.ai.has_value()) { + static auto frame_count = 0; + + auto& ai = *left.player.ai; + auto& paddle = left.paddle; + + if (ball.velocity.dx < 0.f && ball.at.x < ai.reaction_threshold) { + if (frame_count % ai.action_interval == 0) { + auto const paddle_center = paddle.top.y + paddle.height / 2.f; + auto const distance = ball.at.y - paddle_center; + paddle.dy += ai.velocity * std::min(distance, 5.f) / + ((ball.at.x / ai.reaction_threshold) + 1); + } + ++frame_count; + } + else { + frame_count = 0; + } + } + + // AI Right Player + if (right.player.ai.has_value()) { + static auto frame_count = 0; + + auto& ai = *right.player.ai; + auto& paddle = right.paddle; + + if (ball.velocity.dx > 0.f && + ball.at.x > Game::game_space.width - ai.reaction_threshold) { + if (frame_count % ai.action_interval == 0) { + auto const paddle_center = paddle.top.y + paddle.height / 2.f; + auto const distance = ball.at.y - paddle_center; + paddle.dy += ai.velocity * std::min(distance, 5.f) / + (((Game::game_space.width - ball.at.x) / + ai.reaction_threshold) + + 1); + } + ++frame_count; + } + else { + frame_count = 0; + } + } + + return state; +} + +// MAIN ------------------------------------------------------------------------ + +auto try_main() -> int +{ + constexpr auto init_timeout = std::chrono::milliseconds{30}; + + initialize_interactive_terminal(); + screen_dimensions = terminal_area(); + + std::optional state = SplashScreen{}; + + auto timeout = init_timeout; + + while (state.has_value() && sigint_flag == 0) { + write(escape(BlankScreen{})); + write(std::visit( + [](auto const& state) { + return get_display_bytes(state, screen_dimensions); + }, + *state)); + flush(); + + auto const begin = std::chrono::steady_clock::now(); + if (auto const event = read(timeout.count()); event.has_value()) { + state = std::visit( + [](auto event, auto current_state) { + return process(event, std::move(current_state)); + }, + *event, std::move(*state)); + timeout -= std::chrono::duration_cast( + std::chrono::steady_clock::now() - begin); + } + else { // Event Timeout + // state is guaranteed to not be std::nullopt here. + if (std::holds_alternative(*state)) { + state = increment_game_state(std::get(*state)); + } + else if (std::holds_alternative(*state)) { + auto& splash = std::get(*state); + splash.hue = (splash.hue + 1) % 360; + state = splash; + } + timeout = init_timeout; + } + } + + uninitialize_terminal(); + return 0; +} + +auto main() -> int +{ + try { + return try_main(); + } + catch (const std::exception& e) { + uninitialize_terminal(); + std::cerr << "Error: " << e.what() << std::endl; + return 1; + } + catch (...) { + uninitialize_terminal(); + std::cerr << "Unknown error" << std::endl; + return 1; + } +} \ No newline at end of file diff --git a/include/esc/esc.hpp b/include/esc/esc.hpp index 241e51c..005e4ca 100644 --- a/include/esc/esc.hpp +++ b/include/esc/esc.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include diff --git a/include/esc/key.hpp b/include/esc/key.hpp index 5a103d9..7962eb4 100644 --- a/include/esc/key.hpp +++ b/include/esc/key.hpp @@ -16,57 +16,57 @@ namespace esc { */ enum class Key : char32_t { // C0 Control Codes - Null = 0, // Ctrl + Space, or Ctrl + 2, OR Ctrl + @ - Start_of_heading, // Ctrl + a - Start_of_text, // Ctrl + b - End_of_text, // Ctrl + c - End_of_transmission, // Ctrl + d - Enquiry, // Ctrl + e - Acknowledge, // Ctrl + f - Bell, // Ctrl + g - Backspace_1, // Ctrl + h - Not necessarily the Backspace Key. - Tab, // Ctrl + i OR Tab Key - New_line, // Ctrl + j AKA Line Feed - Vertical_tab, // Ctrl + k - Form_feed, // Ctrl + l - Enter, // Ctrl + m - Mapped to Enter key - Shift_out, // Ctrl + n - Shift_in, // Ctrl + o - Data_link_escape, // Ctrl + p - Device_control_one, // Ctrl + q - Device_control_two, // Ctrl + r - Device_control_three, // Ctrl + s - Device_control_four, // Ctrl + t - Negative_acknowledge, // Ctrl + u - Synchronous_idle, // Ctrl + v - End_of_transmission_block, // Ctrl + w - Cancel, // Ctrl + x - End_of_medium, // Ctrl + y - Substitute, // Ctrl + z - Escape, // Ctrl + 3 OR Ctrl + [ - File_separator, // Ctrl + 4 OR Ctrl + Backslash - Group_separator, // Ctrl + 5 OR Ctrl + ] - Record_separator, // Ctrl + 6 OR Ctrl + ^ - Unit_separator, // Ctrl + 7 OR Ctrl + _ - Backspace = 127, // Ctrl + 8 + Null = 0, // Ctrl + Space, or Ctrl + 2, OR Ctrl + @ + StartOfHeading, // Ctrl + a + StartOfText, // Ctrl + b + EndOfText, // Ctrl + c + EndOfTransmission, // Ctrl + d + Enquiry, // Ctrl + e + Acknowledge, // Ctrl + f + Bell, // Ctrl + g + Backspace1, // Ctrl + h - Not necessarily the Backspace Key. + Tab, // Ctrl + i OR Tab Key + NewLine, // Ctrl + j AKA Line Feed + VerticalTab, // Ctrl + k + FormFeed, // Ctrl + l + Enter, // Ctrl + m - Mapped to Enter key + ShiftOut, // Ctrl + n + ShiftIn, // Ctrl + o + DataLinkEscape, // Ctrl + p + DeviceControlOne, // Ctrl + q + DeviceControlTwo, // Ctrl + r + DeviceControlThree, // Ctrl + s + DeviceControlFour, // Ctrl + t + NegativeAcknowledge, // Ctrl + u + SynchronousIdle, // Ctrl + v + EndOfTransmissionBlock, // Ctrl + w + Cancel, // Ctrl + x + EndOfMedium, // Ctrl + y + Substitute, // Ctrl + z + Escape, // Ctrl + 3 OR Ctrl + [ + FileSeparator, // Ctrl + 4 OR Ctrl + Backslash + GroupSeparator, // Ctrl + 5 OR Ctrl + ] + RecordSeparator, // Ctrl + 6 OR Ctrl + ^ + UnitSeparator, // Ctrl + 7 OR Ctrl + _ + Backspace = 127, // Ctrl + 8 // Graphic Characters Space = 32, - Exclamation_mark, - Double_quotation, + ExclamationMark, + DoubleQuotation, Hash, Dollar, Percent, Ampersand, Apostrophe, - Left_parenthesis, - Right_parenthesis, + LeftParenthesis, + RightParenthesis, Asterisk, Plus, Comma, Minus, Period, - Forward_slash, + ForwardSlash, Zero, One, Two, @@ -79,11 +79,11 @@ enum class Key : char32_t { Nine, Colon, Semicolon, - Less_than, + LessThan, Equals, - Greater_than, - Question_mark, - At_sign, + GreaterThan, + QuestionMark, + AtSign, A, B, C, @@ -110,9 +110,9 @@ enum class Key : char32_t { X, Y, Z, - Left_bracket, + LeftBracket, Backslash, - Right_bracket, + RightBracket, Caret, Underscore, Accent, @@ -142,9 +142,9 @@ enum class Key : char32_t { x, y, z, - Left_curly_bracket, + LeftCurlyBracket, Pipe, - Right_curly_bracket, + RightCurlyBracket, Tilde = 126, // Special Keys - These overlap with C1 (128 - 159) @@ -152,8 +152,8 @@ enum class Key : char32_t { Insert, Delete, End, - Page_up, - Page_down = 133, + PageUp, + PageDown = 133, // empty // empty // empty @@ -172,12 +172,12 @@ enum class Key : char32_t { // empty Function11 = 150, Function12, - Arrow_down, // Cursor Keys - Arrow_up, - Arrow_left, - Arrow_right, - Back_tab, // Shift + Tab - Backspace_2, // Numpad backspace and backspace on some laptops + ArrowDown, // Cursor Keys + ArrowUp, + ArrowLeft, + ArrowRight, + BackTab, // Shift + Tab + Backspace2, // Numpad backspace and backspace on some laptops // empty Begin = 159, // Middle Numpad Key @@ -189,39 +189,39 @@ enum class Key : char32_t { LCtrl = 0x40000, RCtrl, LShift, - LShift_fake, + LShiftFake, RShift, - RShift_fake, + RShiftFake, LAlt, RAlt, CapsLock, NumLock, ScrollLock, - Print_screen, - Print_screen_ctrl, - Print_screen_alt, - Print_screen_shift, + PrintScreen, + PrintScreenCtrl, + PrintScreenAlt, + PrintScreenShift, Pause, - Pause_ctrl, - Ctrl_break, - Home_gray, - Up_gray, - Page_up_gray, - Left_gray, - Right_gray, - End_gray, - Down_gray, - Page_down_gray, - Insert_gray, - Delete_gray, + PauseCtrl, + CtrlBreak, + HomeGray, + UpGray, + PageUpGray, + LeftGray, + RightGray, + EndGray, + DownGray, + PageDownGray, + InsertGray, + DeleteGray, - Keypad_enter, - Keypad_asterisk, - Keypad_minus, - Keypad_plus, - Keypad_period, - Keypad_forward_slash, + KeypadEnter, + KeypadAsterisk, + KeypadMinus, + KeypadPlus, + KeypadPeriod, + KeypadForwardSlash, Keypad0, Keypad1, Keypad2, @@ -232,7 +232,7 @@ enum class Key : char32_t { Keypad7, Keypad8, Keypad9, - Alt_system_request, + AltSystemRequest, }; /** diff --git a/include/esc/point.hpp b/include/esc/point.hpp index 16c8c88..84fc7ee 100644 --- a/include/esc/point.hpp +++ b/include/esc/point.hpp @@ -16,6 +16,16 @@ struct Point { auto constexpr operator==(Point const& other) const -> bool = default; auto constexpr operator!=(Point const& other) const -> bool = default; + + auto constexpr operator+(Point const& other) const -> Point + { + return Point{x + other.x, y + other.y}; + } + + auto constexpr operator-(Point const& other) const -> Point + { + return Point{x - other.x, y - other.y}; + } }; } // namespace esc \ No newline at end of file diff --git a/include/esc/sequence.hpp b/include/esc/sequence.hpp index 4b7ef7a..8d0f358 100644 --- a/include/esc/sequence.hpp +++ b/include/esc/sequence.hpp @@ -21,19 +21,6 @@ namespace esc { */ using Cursor = Point; -/** - * Tag type to move the cursor to a Point. - */ -// struct CursorPosition { -// public: -// constexpr CursorPosition(int x, int y) : at{.x = x, .y = y} {} - -// constexpr CursorPosition(Point p) : at{p} {} - -// public: -// Point at; -// }; - /** * Get the control sequence to move the cursor to the specified Point. * diff --git a/src/io.cpp b/src/io.cpp index 35e0c5b..e6c7f3d 100644 --- a/src/io.cpp +++ b/src/io.cpp @@ -57,8 +57,8 @@ constexpr auto keymap = Key::i, Key::o, Key::p, - Key::Left_bracket, - Key::Right_bracket, + Key::LeftBracket, + Key::RightBracket, Key::Enter, Key::LCtrl, Key::a, @@ -84,9 +84,9 @@ constexpr auto keymap = Key::m, Key::Comma, Key::Period, - Key::Forward_slash, + Key::ForwardSlash, Key::RShift, - Key::Keypad_asterisk, + Key::KeypadAsterisk, Key::LAlt, Key::Space, Key::CapsLock, @@ -105,18 +105,18 @@ constexpr auto keymap = Key::Keypad7, Key::Keypad8, Key::Keypad9, - Key::Keypad_minus, + Key::KeypadMinus, Key::Keypad4, Key::Keypad5, Key::Keypad6, - Key::Keypad_plus, + Key::KeypadPlus, Key::Keypad1, Key::Keypad2, Key::Keypad3, Key::Keypad0, - Key::Keypad_period, - Key::Alt_system_request, // "magic SysRq key" - Key::Null, // Not commonly used. + Key::KeypadPeriod, + Key::AltSystemRequest, // "magic SysRq key" + Key::Null, // Not commonly used. Key::Null, // Unlabeled key on non-us keyboards Key::Function11, Key::Function12, @@ -160,63 +160,62 @@ constexpr auto keymap = Key::Null, Key::Null}; -constexpr auto escape_keymap = - std::array{Key::Keypad_enter, - Key::RCtrl, - Key::Null, - Key::Null, - Key::Null, - Key::Null, - Key::Null, - Key::Null, - Key::Null, - Key::Null, - Key::Null, - Key::Null, - Key::Null, - Key::Null, - Key::Null, - Key::LShift_fake, - Key::Null, - Key::Null, - Key::Null, - Key::Null, - Key::Null, - Key::Null, - Key::Null, - Key::Null, - Key::Null, - Key::Null, - Key::Keypad_forward_slash, - Key::RShift_fake, - Key::Print_screen_ctrl, - Key::RAlt, - Key::Null, - Key::Null, - Key::Null, - Key::Null, - Key::Null, - Key::Null, - Key::Null, - Key::Null, - Key::Null, - Key::Null, - Key::Null, - Key::Null, - Key::Null, - Key::Ctrl_break, - Key::Home_gray, - Key::Up_gray, - Key::Page_up_gray, - Key::Null, - Key::Left_gray, - Key::Null, - Key::Right_gray, - Key::End_gray, - Key::Down_gray, - Key::Page_down_gray, - Key::Insert_gray, - Key::Delete_gray}; +constexpr auto escape_keymap = std::array{Key::KeypadEnter, + Key::RCtrl, + Key::Null, + Key::Null, + Key::Null, + Key::Null, + Key::Null, + Key::Null, + Key::Null, + Key::Null, + Key::Null, + Key::Null, + Key::Null, + Key::Null, + Key::Null, + Key::LShiftFake, + Key::Null, + Key::Null, + Key::Null, + Key::Null, + Key::Null, + Key::Null, + Key::Null, + Key::Null, + Key::Null, + Key::Null, + Key::KeypadForwardSlash, + Key::RShiftFake, + Key::PrintScreenCtrl, + Key::RAlt, + Key::Null, + Key::Null, + Key::Null, + Key::Null, + Key::Null, + Key::Null, + Key::Null, + Key::Null, + Key::Null, + Key::Null, + Key::Null, + Key::Null, + Key::Null, + Key::CtrlBreak, + Key::HomeGray, + Key::UpGray, + Key::PageUpGray, + Key::Null, + Key::LeftGray, + Key::Null, + Key::RightGray, + Key::EndGray, + Key::DownGray, + Key::PageDownGray, + Key::InsertGray, + Key::DeleteGray}; /** * Return true if there is nothing to read from file descriptor \p fd. @@ -502,21 +501,21 @@ auto parse_key(ControlSequence cs) -> esc::Key { switch (cs.final_byte) { using Key = esc::Key; - case 'A': return Key::Arrow_up; - case 'B': return Key::Arrow_down; - case 'C': return Key::Arrow_right; - case 'D': return Key::Arrow_left; + case 'A': return Key::ArrowUp; + case 'B': return Key::ArrowDown; + case 'C': return Key::ArrowRight; + case 'D': return Key::ArrowLeft; case 'E': return Key::Begin; case 'F': return Key::End; - case 'G': return Key::Page_down; + case 'G': return Key::PageDown; case 'H': return Key::Home; - case 'I': return Key::Page_up; + case 'I': return Key::PageUp; case 'L': return Key::Insert; case 'P': return Key::Function1; case 'Q': return Key::Function2; case 'R': return Key::Function3; case 'S': return Key::Function4; - case 'Z': return Key::Back_tab; + case 'Z': return Key::BackTab; case '~': return parse_tilde(cs.parameter_bytes); } throw std::runtime_error{"io.cpp parse_key(): Unknown final_byte: " + @@ -805,7 +804,7 @@ auto read_single_token() -> Token if (byte2 == 0x2A) { if (auto const byte3 = read_byte(fd); byte3 == 0xE0) { if (auto const byte4 = read_byte(fd); byte4 == 0x37) { - return KeyPress{Key::Print_screen}; + return KeyPress{Key::PrintScreen}; } } } @@ -814,39 +813,39 @@ auto read_single_token() -> Token if (auto const byte3 = read_byte(fd); byte3 == 0xE0) { if (auto const byte4 = read_byte(fd) - 0x80; byte4 == 0x37) { - return KeyRelease{Key::Print_screen}; + return KeyRelease{Key::PrintScreen}; } } } // Print Screen w/Shift: e0 37 else if (byte2 == 0x37) { - return KeyPress{Key::Print_screen_shift}; + return KeyPress{Key::PrintScreenShift}; } // Print Screen w/Shift: e0 37+0x80 else if ((byte2 & 0x7F) == 0x37) { - return KeyRelease{Key::Print_screen_shift}; + return KeyRelease{Key::PrintScreenShift}; } // Pause w/left or right ctrl: e0 46 e0 c6 else if (byte2 == 0x46) { if (auto const byte3 = read_byte(fd); byte3 == 0xE0) { if (auto const byte4 = read_byte(fd); byte4 == 0xC6) { - return KeyPress{Key::Pause_ctrl}; + return KeyPress{Key::PauseCtrl}; } } } switch (byte2) { - case 0x48: return KeyPress{Key::Arrow_up}; - case 0x48 + 0x80: return KeyRelease{Key::Arrow_up}; - case 0x50: return KeyPress{Key::Arrow_down}; - case 0x50 + 0x80: return KeyRelease{Key::Arrow_down}; - case 0x4D: return KeyPress{Key::Arrow_right}; - case 0x4D + 0x80: return KeyRelease{Key::Arrow_right}; - case 0x4B: return KeyPress{Key::Arrow_left}; - case 0x4B + 0x80: return KeyRelease{Key::Arrow_left}; + case 0x48: return KeyPress{Key::ArrowUp}; + case 0x48 + 0x80: return KeyRelease{Key::ArrowUp}; + case 0x50: return KeyPress{Key::ArrowDown}; + case 0x50 + 0x80: return KeyRelease{Key::ArrowDown}; + case 0x4D: return KeyPress{Key::ArrowRight}; + case 0x4D + 0x80: return KeyRelease{Key::ArrowRight}; + case 0x4B: return KeyPress{Key::ArrowLeft}; + case 0x4B + 0x80: return KeyRelease{Key::ArrowLeft}; default: return std::nullopt; } } @@ -875,8 +874,8 @@ auto read_single_token() -> Token : Event{KeyPress{escape_keymap[key_byte - 0x1C]}}; } - case 0x54: return KeyPress{Key::Print_screen_alt}; - case 0x54 + 0x80: return KeyRelease{Key::Print_screen_alt}; + case 0x54: return KeyPress{Key::PrintScreenAlt}; + case 0x54 + 0x80: return KeyRelease{Key::PrintScreenAlt}; case 0x00: // Keyboard Error case 0xAA: // Basic Assurance Test OK diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 10a8162..67ce7b0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,4 +1,3 @@ - add_executable(escape.tests.unit EXCLUDE_FROM_ALL glyph.test.cpp )