From 6962d0935381bec40caf54f0b6b267f8fe7f79a2 Mon Sep 17 00:00:00 2001 From: James Holderness Date: Sun, 1 Dec 2024 16:58:49 +0000 Subject: [PATCH] Initial release --- .clang-format | 26 ++ .editorconfig | 9 + .gitattributes | 6 + .github/workflows/build.yml | 92 +++++ .gitignore | 1 + CMakeLists.txt | 36 ++ CMakeSettings.json | 26 ++ LICENSE.txt | 21 ++ README.md | 86 +++++ build/.gitignore | 2 + screenshot.png | 3 + src/capabilities.cpp | 150 ++++++++ src/capabilities.h | 32 ++ src/coloring.cpp | 37 ++ src/coloring.h | 71 ++++ src/engine.cpp | 155 ++++++++ src/engine.h | 27 ++ src/font.cpp | 723 ++++++++++++++++++++++++++++++++++++ src/font.h | 25 ++ src/levels.cpp | 454 ++++++++++++++++++++++ src/levels.h | 39 ++ src/main.cpp | 111 ++++++ src/options.cpp | 46 +++ src/options.h | 17 + src/os.cpp | 72 ++++ src/os.h | 12 + src/screen.cpp | 367 ++++++++++++++++++ src/screen.h | 106 ++++++ src/snake.cpp | 288 ++++++++++++++ src/snake.h | 52 +++ src/status.cpp | 150 ++++++++ src/status.h | 37 ++ 32 files changed, 3279 insertions(+) create mode 100644 .clang-format create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 CMakeSettings.json create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 build/.gitignore create mode 100644 screenshot.png create mode 100644 src/capabilities.cpp create mode 100644 src/capabilities.h create mode 100644 src/coloring.cpp create mode 100644 src/coloring.h create mode 100644 src/engine.cpp create mode 100644 src/engine.h create mode 100644 src/font.cpp create mode 100644 src/font.h create mode 100644 src/levels.cpp create mode 100644 src/levels.h create mode 100644 src/main.cpp create mode 100644 src/options.cpp create mode 100644 src/options.h create mode 100644 src/os.cpp create mode 100644 src/os.h create mode 100644 src/screen.cpp create mode 100644 src/screen.h create mode 100644 src/snake.cpp create mode 100644 src/snake.h create mode 100644 src/status.cpp create mode 100644 src/status.h diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..7af7d83 --- /dev/null +++ b/.clang-format @@ -0,0 +1,26 @@ +Language: Cpp +AccessModifierOffset: -4 +AlignAfterOpenBracket: DontAlign +AllowShortCaseLabelsOnASingleLine: true +AllowShortIfStatementsOnASingleLine: true +AlwaysBreakTemplateDeclarations: Yes +BraceWrapping: + AfterFunction: true +BreakBeforeBraces: Custom +ColumnLimit: 0 +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^<.*\.h>' + Priority: 2 + - Regex: '^<.*' + Priority: 3 + - Regex: '.*' + Priority: 1 +IndentCaseLabels: true +IndentWidth: 4 +KeepEmptyLinesAtTheStartOfBlocks: false +MaxEmptyLinesToKeep: 2 +NamespaceIndentation: All +PointerAlignment: Left +SpaceAfterCStyleCast: true +SpacesBeforeTrailingComments: 2 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5882e64 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +tab_width = 8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d62b5a1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +*.cpp text +*.h text +*.txt text +*.md text +.* text +*.png filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..53f82cb --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,92 @@ +name: Build and Release + +on: + push: + tags: + - 'v[0-9]+.*' + +permissions: + packages: read + contents: write + +jobs: + create_release: + name: Create Release + runs-on: ubuntu-latest + + outputs: + upload_url: ${{ steps.create_release.outputs.upload_url }} + + steps: + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + draft: false + prerelease: false + + release_assets: + name: Release Assets + needs: create_release + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + build_type: [Release] + cpp_compiler: [g++, cl] + include: + - os: windows-latest + cpp_compiler: cl + - os: ubuntu-latest + cpp_compiler: g++ + exclude: + - os: windows-latest + cpp_compiler: g++ + - os: ubuntu-latest + cpp_compiler: cl + + steps: + - uses: actions/checkout@v3 + + - name: Set Reusable Strings + id: strings + shell: bash + run: | + echo "build-output-dir=${{ github.workspace }}/build" >> "$GITHUB_OUTPUT" + + - name: Configure CMake + run: > + cmake -B ${{ steps.strings.outputs.build-output-dir }} + -DCMAKE_CXX_COMPILER=${{ matrix.cpp_compiler }} + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} + -S ${{ github.workspace }} + + - name: Build + run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config ${{ matrix.build_type }} + + - name: Upload Ubuntu Assets + if: ${{ matrix.os == 'ubuntu-latest' }} + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create_release.outputs.upload_url }} + asset_name: vtnibbler + asset_path: ${{ steps.strings.outputs.build-output-dir }}/vtnibbler + asset_content_type: application/octet-stream + + - name: Upload Windows Assets + if: ${{ matrix.os == 'windows-latest' }} + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create_release.outputs.upload_url }} + asset_name: vtnibbler.exe + asset_path: ${{ steps.strings.outputs.build-output-dir }}/Release/vtnibbler.exe + asset_content_type: application/octet-stream diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..88dbff1 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vs/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..2dae71e --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,36 @@ +cmake_minimum_required(VERSION 3.15) +project(vtnibbler) + +set( + MAIN_FILES + "src/main.cpp" + "src/capabilities.cpp" + "src/coloring.cpp" + "src/engine.cpp" + "src/font.cpp" + "src/levels.cpp" + "src/options.cpp" + "src/os.cpp" + "src/screen.cpp" + "src/snake.cpp" + "src/status.cpp" +) + +set( + DOC_FILES + "README.md" + "LICENSE.txt" +) + +if(WIN32) + set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded") +endif() + +add_executable(vtnibbler ${MAIN_FILES}) + +if(UNIX) + target_link_libraries(vtnibbler -lpthread) +endif() + +set_target_properties(vtnibbler PROPERTIES CXX_STANDARD 20 CXX_STANDARD_REQUIRED On) +source_group("Doc Files" FILES ${DOC_FILES}) diff --git a/CMakeSettings.json b/CMakeSettings.json new file mode 100644 index 0000000..01bc921 --- /dev/null +++ b/CMakeSettings.json @@ -0,0 +1,26 @@ +{ + "configurations": [ + { + "name": "x64-Debug", + "generator": "Ninja", + "configurationType": "Debug", + "inheritEnvironments": [ "msvc_x64_x64" ], + "buildRoot": "${projectDir}\\build\\${name}", + "installRoot": "${projectDir}\\build\\install\\${name}", + "cmakeCommandArgs": "", + "buildCommandArgs": "", + "ctestCommandArgs": "" + }, + { + "name": "x64-Release", + "generator": "Ninja", + "configurationType": "RelWithDebInfo", + "inheritEnvironments": [ "msvc_x64_x64" ], + "buildRoot": "${projectDir}\\build\\${name}", + "installRoot": "${projectDir}\\build\\install\\${name}", + "cmakeCommandArgs": "", + "buildCommandArgs": "", + "ctestCommandArgs": "" + } + ] +} \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..c56c192 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2024 James Holderness + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..dc18f6a --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +VT Nibbler +========== + +![Screenshot](screenshot.png) + +This is a clone of the 1980's [Nibbler] arcade game, designed to be played on +a DEC VT terminal. It requires at least a VT320 (or something of comparable +functionality), but a VT525 is best if you want color and sound effects. + +You'll also need at least a 19200 baud connection to play at the default +frame rate. If you find the input is lagging, try selecting a slower speed +using the command line option `--speed 4` or `--speed 3`. + +[Nibbler]: https://en.wikipedia.org/wiki/Nibbler_(video_game) + + +Controls +-------- + +Use the arrow keys to move, and `Q` to quit. + + +Download +-------- + +The latest binaries can be found on GitHub at the following url: + +https://github.com/j4james/vtnibbler/releases/latest + +For Linux download `vtnibbler`, and for Windows download `vtnibbler.exe`. + + +Build Instructions +------------------ + +If you want to build this yourself, you'll need [CMake] version 3.15 or later +and a C++ compiler supporting C++20 or later. + +1. Download or clone the source: + `git clone https://github.com/j4james/vtnibbler.git` + +2. Change into the build directory: + `cd vtnibbler/build` + +3. Generate the build project: + `cmake -D CMAKE_BUILD_TYPE=Release ..` + +4. Start the build: + `cmake --build . --config Release` + +[CMake]: https://cmake.org/ + + +Supported Terminals +------------------- + +| Terminal | Color | Sound | +|--------------------|:-----:|:-----:| +| DEC VT320 | no | no | +| DEC VT330/340 | no | no | +| DEC VT382 | no | no | +| DEC VT420 | no | no | +| DEC VT510/520 | no | no | +| DEC VT525 | yes | yes | +| KoalaTerm | no | no | +| MLTerm | part | no | +| PowerTerm | no | no | +| Reflection Desktop | no | no | +| RLogin | yes | yes | +| VTStar | part | no | +| Windows Terminal | yes | yes | + +You could also get by with a VT220 or VT240, but those terminals don't have +full-cell fonts, so the graphics will look a bit messed up. + +Terminals with *part* color support will render the graphics in color, but +won't show palette animations and the different level color schemes. + + +License +------- + +The VT Nibbler source code and binaries are released under the MIT License. +See the [LICENSE] file for full license details. + +[LICENSE]: LICENSE.txt diff --git a/build/.gitignore b/build/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/build/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..6a44e08 --- /dev/null +++ b/screenshot.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:48f0d8bc15b03b24fe4c9b39dc76bb2e76eb464d35b4c68fdbf92c1c37215544 +size 28002 diff --git a/src/capabilities.cpp b/src/capabilities.cpp new file mode 100644 index 0000000..8adfa40 --- /dev/null +++ b/src/capabilities.cpp @@ -0,0 +1,150 @@ +// VT Nibbler +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#include "capabilities.h" + +#include "os.h" + +#include +#include + +using namespace std::string_literals; + +capabilities::capabilities() +{ + // Save the cursor position. + std::cout << "\0337"; + // Request 7-bit C1 controls from the terminal. + std::cout << "\033 F"; + // Determine the screen size. + std::cout << "\033[999;999H\033[6n"; + const auto size = _query(R"(\x1B\[(\d+);(\d+)R)", false); + if (!size.empty()) { + height = std::stoi(size[1]); + width = std::stoi(size[2]); + } + // Check if 8-bit controls are supported. + std::cout << "\0338\2335n\033[1K"; + has_8bit = !_query(R"(\x1B\[\d*n)", true).empty(); + // Retrieve the device attributes report. + _query_device_attributes(); + // Retrieve the terminal id so we can guess the font size. + _query_terminal_id(); + // Restore the cursor position. + std::cout << "\0338"; +} + +std::optional capabilities::query_mode(const int mode) const +{ + std::cout << "\033[?" << mode << "$p"; + const auto report = _query(R"(\x1B\[\?(\d+);(\d+)\$y)", true); + if (!report.empty()) { + const auto returned_mode = std::stoi(report[1]); + const auto status = std::stoi(report[2]); + if (returned_mode == mode) { + if (status == 1) return true; + if (status == 2) return false; + } + } + return {}; +} + +std::string capabilities::query_setting(const std::string_view setting) const +{ + std::cout << "\033P$q" << setting << "\033\\"; + const auto report = _query(R"(\x1BP1\$r(.*)\x1B\\)", true); + if (!report.empty()) + return report[1]; + else + return {}; +} + +std::string capabilities::query_color_table() const +{ + std::cout << "\033[2;2$u"; + const auto report = _query(R"(\x1BP2\$s(.*)\x1B\\)", true); + if (!report.empty()) + return report[1]; + else + return {}; +} + +void capabilities::_query_device_attributes() +{ + std::cout << "\033[c"; + // The Reflection Desktop terminal sometimes uses comma separators + // instead of semicolons in their DA report, so we allow for either. + const auto report = _query(R"(\x1B\[\?(\d+)([;,\d]*)c)", false); + if (!report.empty()) { + // The first parameter indicates the terminal conformance level. + const auto level = std::stoi(report[1]); + // Level 4+ conformance implies support for features 28 and 32. + if (level >= 64) { + has_rectangle_ops = true; + has_macros = true; + } + // The remaining parameters indicate additional feature extensions. + const auto features = report[2].str(); + const auto digits = std::regex(R"(\d+)"); + auto it = std::sregex_iterator(features.begin(), features.end(), digits); + while (it != std::sregex_iterator()) { + const auto feature = std::stoi(it->str()); + switch (feature) { + case 7: has_soft_fonts = true; break; + case 22: has_color = true; break; + case 28: has_rectangle_ops = true; break; + case 32: has_macros = true; break; + } + it++; + } + } +} + +void capabilities::_query_terminal_id() +{ + std::cout << "\033[>c"; + const auto report = _query(R"(\x1B\[>(\d+)[;\d]*c)", true); + if (!report.empty()) { + terminal_id = std::stoi(report[1]); + } +} + +std::smatch capabilities::_query(const char* pattern, const bool may_not_work) +{ + auto final_char = pattern[strlen(pattern) - 1]; + if (may_not_work) { + // If we're uncertain this query is supported, we'll send an extra DA + // or DSR-CPR query to make sure that we get some kind of response. + if (final_char == 'R') { + final_char = 'c'; + std::cout << "\033[c"; + } else { + final_char = 'R'; + std::cout << "\033[6n"; + } + } + std::cout.flush(); + // This needs to be static so the returned smatch can reference it. + static auto response = std::string{}; + response.clear(); + auto last_escape = 0; + for (;;) { + const auto ch = os::getch(); + // Ignore XON, XOFF + if (ch == '\021' || ch == '\023') + continue; + // If we've sent an extra query, the last escape should be the + // start of that response, which we'll ultimately drop. + if (may_not_work && ch == '\033') + last_escape = response.length(); + response += ch; + if (ch == final_char) break; + } + // Drop the extra response if one was requested. + if (may_not_work) + response = response.substr(0, last_escape); + auto match = std::smatch{}; + std::regex_match(response, match, std::regex(pattern)); + return match; +} diff --git a/src/capabilities.h b/src/capabilities.h new file mode 100644 index 0000000..4047110 --- /dev/null +++ b/src/capabilities.h @@ -0,0 +1,32 @@ +// VT Nibbler +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#pragma once + +#include +#include +#include +#include + +class capabilities { +public: + capabilities(); + std::optional query_mode(const int mode) const; + std::string query_setting(const std::string_view setting) const; + std::string query_color_table() const; + + int width = 80; + int height = 24; + bool has_soft_fonts = false; + bool has_color = false; + bool has_rectangle_ops = false; + bool has_macros = false; + bool has_8bit = false; + int terminal_id = 0; + +private: + void _query_device_attributes(); + void _query_terminal_id(); + static std::smatch _query(const char* pattern, const bool may_not_work); +}; diff --git a/src/coloring.cpp b/src/coloring.cpp new file mode 100644 index 0000000..b1c0a5e --- /dev/null +++ b/src/coloring.cpp @@ -0,0 +1,37 @@ +// VT Nibbler +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#include "coloring.h" + +#include "capabilities.h" +#include "options.h" + +#include + +coloring::coloring(const capabilities& caps, const options& options) + : _using_colors{options.color && caps.has_color} +{ + if (_using_colors) { + // Save the current text color assignment. + _color_assignment = caps.query_setting("1,|"); + // Make sure the text color assignment is white on black. + std::cout << "\033[1;7;0,|"; + // Save the current color table. + _color_table = caps.query_color_table(); + // Set the fixed color table entries. + std::cout << "\033P2$p0;2;0;0;0/1;2;100;0;0/3;2;100;100;0/4;2;0;0;87/6;2;0;72;59/7;2;100;100;87\033\\"; + } +} + +coloring::~coloring() +{ + if (_using_colors) { + // Restore the original color assignment. + if (!_color_assignment.empty()) + std::cout << "\033[" << _color_assignment; + // Restore the original color table. + if (!_color_table.empty()) + std::cout << "\033P2$p" << _color_table << "\033\\"; + } +} diff --git a/src/coloring.h b/src/coloring.h new file mode 100644 index 0000000..02dc35a --- /dev/null +++ b/src/coloring.h @@ -0,0 +1,71 @@ +// VT Nibbler +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#pragma once + +#include + +class capabilities; +class options; + +enum class color { + unknown = 0, + + // Fixed colors + red = 1, + yellow = 3, + blue = 4, + cyan = 6, + white = 7, + snake = color::red, + + // Components which change color + text = 2, + time = 5, + crouton_1 = 9, + crouton_2 = 13, + wall = 15, + + // Monochrome attributes + mono_normal = 1, + mono_bright = 2, + mono_blinking = 3, +}; + +namespace palette { + static constexpr auto red = "72;0;0"; + static constexpr auto orange = "100;72;0"; + static constexpr auto green = "59;72;59"; + static constexpr auto blue = "0;0;87"; + static constexpr auto cyan = "0;72;59"; + static constexpr auto purple = "72;0;87"; + static constexpr auto gray = "72;72;59"; + + static constexpr auto bright_red = "100;0;0"; + static constexpr auto bright_yellow = "100;100;0"; + static constexpr auto bright_green = "0;100;0"; + static constexpr auto bright_cyan = "0;59;87"; + static constexpr auto bright_purple = "100;0;87"; + static constexpr auto white = "100;100;87"; + + static constexpr auto white_blue = "81;81;90"; + static constexpr auto red_purple = "84;25;41"; + static constexpr auto red_orange = "84;38;25"; + static constexpr auto green_blue = "58;65;74"; + static constexpr auto purple_orange = "84;38;73"; + static constexpr auto green_red = "38;81;25"; + static constexpr auto blue_yellow = "43;43;73"; + static constexpr auto cyan_red = "38;58;73"; +} // namespace palette + +class coloring { +public: + coloring(const capabilities& caps, const options& options); + ~coloring(); + +private: + bool _using_colors; + std::string _color_assignment; + std::string _color_table; +}; diff --git a/src/engine.cpp b/src/engine.cpp new file mode 100644 index 0000000..8660179 --- /dev/null +++ b/src/engine.cpp @@ -0,0 +1,155 @@ +// VT Nibbler +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#include "engine.h" + +#include "coloring.h" +#include "font.h" +#include "levels.h" +#include "options.h" +#include "screen.h" +#include "snake.h" +#include "status.h" + +#include + +using namespace std::chrono_literals; + +engine::engine(const capabilities& caps, const options& options, soft_font& font) + : _caps{caps}, _options{options}, _font{font} +{ +} + +bool engine::run() +{ + screen screen(_caps, _options); + status status{screen}; + + static auto chomp_macro = std::string{}; + static auto short_chomp_macro = std::string{}; + if (chomp_macro.empty()) { + chomp_macro = screen.define_macro(3, [&]() { + screen.play_sound(7); + screen.play_sound(10); + }); + short_chomp_macro = screen.define_macro(4, [&]() { + screen.play_sound(10); + }); + } + + for (auto wave = 1; !screen.exit_requested(); wave++) { + if (wave == 100) wave = 80; + if (wave % 4 == 0) status.gain_life(); + + // In the original arcade game the speed increases almost every wave, + // but our implementation is much simpler. The first four waves are + // about the same as the starting arcade speed, and then from wave + // five onwards it's 50% faster. + const auto frames_per_move = (wave > 4 ? 2 : 3); + + _font.init(wave); + level level{screen, wave}; + snake snake{screen, level}; + level.init_map(); + status.init(wave); + level.init_croutons(); + snake.init(); + + screen.reset_keys(); + while (!screen.exit_requested() && !level.complete()) { + auto reset_key = false; + switch (screen.key_pressed()) { + case key::up: + reset_key = snake.turn(-1, 0); + break; + case key::down: + reset_key = snake.turn(+1, 0); + break; + case key::right: + reset_key = snake.turn(0, +1); + break; + case key::left: + reset_key = snake.turn(0, -1); + break; + } + if (reset_key) screen.reset_keys(); + + snake.move(); + const auto [snake_y, snake_x] = snake.position(); + if (level.eat_crouton(snake_y, snake_x)) { + status.add_points(level.points_per_crouton()); + snake.grow(); + } + + status.update(frames_per_move); + level.update(frames_per_move); + + if (snake.is_dead() || status.out_of_time()) { + status.lose_life(); + if (snake.erase()) { + screen.reset(); + screen.set_palette(color::snake, palette::bright_red); + if (status.out_of_time()) { + status.init(wave); + _display_time_out(screen); + screen.reset(); + } + if (status.game_over()) { + status.init(wave); + _display_game_over(screen); + break; + } + status.reset_time(); + status.init(wave); + level.init_map(); + level.init_croutons(); + snake.init(); + screen.reset_keys(); + } + } + + // We're stretching the definition of a second here so the default + // frame rate is slow enough to support the two-note chomp sound. + const auto time_between_moves = frames_per_move * 1050ms / _options.fps; + if (snake.just_eaten()) { + if (time_between_moves >= 62ms) + screen.invoke_macro(chomp_macro); + else if (time_between_moves >= 31ms) + screen.invoke_macro(short_chomp_macro); + } + screen.pause(time_between_moves); + } + + if (status.game_over()) { + break; + } else if (level.complete()) { + status.apply_bonus(); + status.reset_time(); + screen.reset(); + } + } + + screen.shutdown_keyboard(); + return !screen.exit_requested(); +} + +void engine::_display_time_out(screen& screen) +{ + screen.set_charset("B"); + screen.write(11, 9, "", color::yellow); + for (auto ch : "NIBBLER RAN OUT OF TIME") { + screen.write(ch); + screen.pause(66ms); + } + screen.set_charset(" @"); + screen.pause(1s); +} + +void engine::_display_game_over(screen& screen) +{ + screen.set_charset("B"); + screen.write(12, 16, "GAME OVER", color::red); + screen.set_charset(" @"); + screen.flush(); +} diff --git a/src/engine.h b/src/engine.h new file mode 100644 index 0000000..f8dd93c --- /dev/null +++ b/src/engine.h @@ -0,0 +1,27 @@ +// VT Nibbler +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#pragma once + +class capabilities; +class options; +class screen; +class soft_font; + +class engine { +public: + static constexpr int width = 38; + static constexpr int height = 22; + + engine(const capabilities& caps, const options& options, soft_font& font); + bool run(); + +private: + void _display_time_out(screen& screen); + void _display_game_over(screen& screen); + + const capabilities& _caps; + const options& _options; + soft_font& _font; +}; diff --git a/src/font.cpp b/src/font.cpp new file mode 100644 index 0000000..0b78a0a --- /dev/null +++ b/src/font.cpp @@ -0,0 +1,723 @@ +// VT Nibbler +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#include "font.h" + +#include "capabilities.h" + +#include +#include +#include +#include + +constexpr auto font_8x10 = R"(0;1;1;4;0;0{ @ +~~??????/NN??????; +??__~~zz/MDDDBNNN; +zz~~__??/NNNBDDDM; +@AEMNN^Z/????????; +Z^NNMEA@/????????; +??????__/GCEFNNNL; +^N??????/????????; +__??????/LNNNFECG; +FIYY{~~~/????NNLL; +~~~{YYIF/LLNN????; +????????/???????M; +??????_o/??????NN; +????_{~~/????NNLL; +o_??????/NN??????; +wggwwgn~/????????; +w{CCCC{w/?@@@@@@?; +??C{{???/?@@@@@@?; +ccSSSS[G/@@@@@@@@; +?CCSSS{k/@@@@@@@?; +ooggc{{_/?????@@?; +[SSSSSs_/@@@@@@@?; +w{SSSSs_/?@@@@@@?; +CCCcsSKK/???@@???; +g{SSSS{g/?@@@@@@?; +G[SSSS{w/?@@@@@@?; +wggwwggw/??????NN; +wggwwggw/NN??????; +??????Ow/????????; +wggwwggw/????????; +wO??????/????????; +????????/M???????; +~~{_????/LLNN????; +owgccgwo/@@????@@; +????????/????????; +w{CCCCCC/?@@@@@@@; +~ngwwggw/NN??????; +{{SSSSCC/@@@@@@@@; +{{SSSSCC/@@??????; +ggwwwgn~/??????NN; +{{OOOO{{/@@????@@; +?CC{{CC?/?@@@@@@?; +??__{{{{/MDDDBNNN; +{{{{__??/NNNBDDDM; +{{??????/@@@@@@@@; +{{WooW{{/@@????@@; +FIYY{~~~/????@@@@; +w{CCCC{w/?@@@@@@?; +{{SSSS[G/@@??????; +~~~{YYIF/@@@@????; +{{SSss[G/@@????@@; +G[SSSSsc/@@@@@@@?; +CCC{{CCC/???@@???; +~^F?????/B???????; +[{_??_{[/???@@???; +{{_OO_{{/@@????@@; +???~HHH~/???NHHHN; +K[O__O[K/???@@???; +HHH~HHH~/HHHNHHHN; +????^~zz/?????BNN; +~ngwwggw/????????; +zz~^????/NNB?????; +???????F/????????; +F???????/????????; +??????N^/????????; +}}}}|zzx/BBBBDEEC; +|}mEEk{w/DBBBB@@?; +w{kEEm}|/?@@BBBBD; +xzz|}}}}/CEEDBBBB; +{IJJf~~~/?@BFFFNL; +~~~fJJI{/LNFFFB@?; +oGKM]}~z/BDLLMNNN; +z~}]MKGo/NNNMLLDB; +{kkCCkk{/@@@@@@@@; +????~~zz/????NNLL; +zz~~????/LLNN????; +????{{{{/????NNLL; +{kkCCkk{/LLNN@@@@; +{kkCCkk{/@@@@NNLL; +{{{{????/LLNN????; +{kkCFnjz/@@@@@@@@; +zz~~????/@@@@????; +????~~zz/????@@@@; +zjnFCkk{/@@@@@@@@; +{{{{{www/@@@@@???; +wwwOOOOO/????????; +OOOOOwww/????????; +www{{{{{/???@@@@@; +??????_{/?????MNN; +{_??????/NNM?????; +?????F^~/???????B; +????Oggs/???????@; +??????~~/??????NN; +[ggO????/@???????; +?___????/?@BB???? +)"; + +constexpr auto font_15x12 = R"(0;1;1;15;0;2;12;0{ @ +l~~~???????????/l~~~???????????; +???????~~~rr```/owSUUVFN~~~~~~~; +``rr~~~????????/~~~~~~NFVUUSwo?; +?@EMM]^^^~~zzpp/???????????????; +pzz~~^^^]MME@??/???????????????; +???????????????/?_W[[]}}}~~vvbb; +l~~~???????????/@@?????????????; +???????????????/bvv~~}}}][[W_??; +BFIYYyw{~~~~~~~/???????~~~rr```; +~~~~~~{wyYYIFB?/``rr~~~????????; +???????????????/??????????_w}~~; +??????????__ooo/??????????~~~ll; +????????w}~~~~~/???????~~~rr```; +oo__???????????/l~~~???????????; +ooOOOooooO^^~ll/@@@@@@@@@@@@@@@; +?oowGGGGGGwoo??/?@@BAAAAAAB@@??; +????GGwww??????/???AAABBBAAA???; +?GGGgggggwwwO??/?BBBBBAAAAAAA??; +?GGGgggwwwgg???/?AAAAAAAAABB@??; +??__oOWGGwww???/?@@@@@@@@BBB@??; +?wwwgggggggg???/?AAAAAAAAABB@??; +?owwgggggggg???/?@BBAAAAAABB@??; +?GGGGGGggwWWG??/?????BBB???????; +?OwwggggggwO???/?@BBAAAAAABB@??; +?Owwggggggwwo??/??AAAAAAAABB@??; +ooOOOooooOOOooo/@@@@@@@@@@~~~ll; +ooOOOooooOOOooo/l~~~@@@@@@@@@@@; +??????????__ooo/????????????@@@; +ooOOOooooOOOooo/@@@@@@@@@@@@@@@; +oo__???????????/@@?????????????; +???????????????/~}w_???????????; +~~~~}w?????????/``rr~~~????????; +?__oOWGGGWOo__?/?BBB@@@@@@@BBB?; +???????????????/???????????????; +?oowGGGGGGGGGG?/?@@BAAAAAAAAAA?; +l~^^OooooOOOooo/l~~~@@@@@@@@@@@; +??wwwggggggGGG?/??BBBAAAAAAAAA?; +??wwwggggggGGG?/??BBB??????????; +oOOOoooooO^^~ll/@@@@@@@@@@~~~ll; +?www_______www?/?BBB???????BBB?; +???GGGwwwGGG???/???AAABBBAAA???; +???????wwwwwwww/owSUUVFN~~~~~~~; +wwwwwww????????/~~~~~~NFVUUSwo?; +??www??????????/??BBBAAAAAAAAA?; +?wwwWoo_ooWwww?/?BBB???@???BBB?; +BFIYYyw{~~~~~~~/???????FFFFFFFF; +?owwGGGGGGGwwo?/?@BBAAAAAAABB@?; +?wwwgggggggwwO?/?BBB???????????; +~~~~~~{wyYYIFB?/FFFFFFF????????; +?wwwGGGGGGgwwo?/?BBB@@@@@@BAAA?; +?Owwggggggggg??/?AAAAAAAAAABB@?; +??GGGGwwwGGGG??/??????BBB??????; +~~~~^F?????????/~^F@???????????; +?Www__???__wwW?/????@@BBB@@????; +?www?__o__?www?/?@BB@@@?@@@BB@?; +?????}}}QQQQQ}}/?????^^^QQQQQ^^; +?WWw_______wWW?/??????BBB??????; +QQQQQ}}}QQQQQ}}/QQQQQ^^^QQQQQ^^; +???????~~~rr```/????????F^~~~~~; +l~^^OooooOOOooo/@@@@@@@@@@@@@@@; +``rr~~~????????/~~~~^F?????????; +??????????@F^~~/???????????????; +~^F@???????????/???????????????; +??????????~~~ll/????????????@@@; +{{}}}}}xxvvvppp/NN^^^^^ffzzzbbb; +pxx}]]MMK[[{wwo/bff^^^]]MNNNFFB; +oww{[[KMM]]}xxp/BFFNNNM]]^^^ffb; +pppvvvxx}}}}}{{/bbbzzzff^^^^^NN; +owSUUVFN~~~~~~~/?@EMM]^^^~~zzpp; +~~~~~~NFVUUSwo?/pzz~~^^^]MME@??; +?_W[[]}}}~~vvbb/BFIYYyw{~~~~~~~; +bvv~~}}}][[W_??/~~~~~~{wyYYIFB?; +wwwWWGGGGGWWwww/FFFEECCCCCEEFFF; +???????~~~rr```/???????~~~rr```; +``rr~~~????????/``rr~~~????????; +???????wwwwwwww/???????~~~rr```; +wwwWWGGGGGWWwww/``rq}{{CCCEEFFF; +wwwWWGGGGGWWwww/FFFEECC{{{qq```; +wwwwwww????????/``rr~~~????????; +wwwWWGGNNNRR```/FFFEECCCCCEEFFF; +``rr~~~????????/FFFFFFF????????; +???????~~~rr```/???????FFFFFFFF; +``rR^NNGGGWWwww/FFFEECCCCCEEFFF; +woooooooooooooo/FBBBBBBBBBBBBBB; +o______________/B@@@@@@@@@@@@@@; +______________o/@@@@@@@@@@@@@@B; +oooooooooooooow/BBBBBBBBBBBBBBF; +??????????_w}~~/????????w}~~~~~; +~}w_???????????/~~~~}w?????????; +????????F^~~~~~/??????????@F^~~; +???????oOOWggks/???????@@@BBBDD; +??????????~~~ll/??????????~~~ll; +sswwWOOo???????/DEAAB@@@???????; +???????????????/???BFFF???????? +)"; + +constexpr auto font_10x20 = R"(0;1;1;10;0;2;20;0{ @ +r~~???????/x~~???????/[~~???????/ABB???????; +?????~~NFB/???_o~~~}{/owvvvN~~~~/BB????BBBB; +BFN~~?????/{}~~~o_???/~~~~Nvvvwo/BBBB????BB; +@B[{{}~~NF/???@BFFNNM/??????????/??????????; +FN~~}{{[B@/MNNFFB@???/??????????/??????????; +??????????/?????__ooo/??w}~~~~r`/AB???@BBBB; +r~~???????/x~^???????/@?????????/??????????; +??????????/ooo__?????/`r~~~~}w??/BBBB@???BA; +N^kkko~~~~/??BFN~~~~~/?????~~r`?/?????BBBBB; +~~~~okkk^N/~~~~~NFB??/?`r~~?????/BBBBB?????; +??????????/?????????o/????????{~/???????ABB; +??????????/???????w{}/???????~~[/???????BBA; +??????o~~~/?????o~~~~/?????~~r`?/?????BBBBB; +??????????/}{w???????/[~~???????/ABB???????; +???????~~r/}ee}}}efrx/@@@@@@@@@@/??????????; +??_____???/}~????@~}?/?@BAAAA@??/??????????; +????__????/???@~~????/??AABBAA??/??????????; +?_______??/@`owWW[NF?/BBBAAAAAA?/??????????; +?________?/???GKMJxo?/@BAAAAAB@?/??????????; +?????___??/owkeb`~~_?/??????BB??/??????????; +________??/FFCCCCC{w?/@BAAAAAB@?/??????????; +??______??/}~HGGGGwo?/@BAAAAAB@?/??????????; +_________?/@@?owKEB@?/???BB?????/??????????; +?______???/vNKGGWWvo?/@AAAAAAB@?/??????????; +?_______??/FNGGGGG~~?/?AAAAAB@??/??????????; +??????????/}ee}}}eeMM/@@@@@@@~~[/???????BBA; +??????????/]Ee}}}ee}}/[~~@@@@@@@/ABB???????; +??????????/???????W{}/?????????@/??????????; +??????????/}ee}}}ee}}/@@@@@@@@@@/??????????; +??????????/}{W???????/@?????????/??????????; +??????????/o?????????/~{????????/BBA???????; +~~~o??????/~~~~o?????/?`r~~?????/BBBBB?????; +???___????/{}RPOPR}{?/BB?????BB?/??????????; +??????????/??????????/??????????/??????????; +??______??/}~@????@@?/?@BAAAAB@?/??????????; +r~~???????/PBf}}}ee}}/[~~@@@@@@@/ABB???????; +?________?/?~~GGGG???/?BBAAAAAA?/??????????; +?________?/?~~GGGG???/?BB???????/??????????; +???????~~r/}ee}}}efBH/@@@@@@@~~[/???????BBA; +__?????__?/~~GGGGG~~?/BB?????BB?/??????????; +??______??/????~~????/??AABBAA??/??????????; +?????_____/???_o~~~~~/owvvvN~~~~/BB????BBBB; +_____?????/~~~~~o_???/~~~~Nvvvwo/BBBB????BB; +?__???????/?~~???????/?BBAAAAAA?/??????????; +__?????__?/~~FM[MF~~?/BB?????BB?/??????????; +N^kkko~~~~/??BFN~~~~~/?????FFFFF/??????????; +?_______??/~~?????~~?/@BAAAAAB@?/??????????; +________??/~~OOOOO^N?/BB????????/??????????; +~~~~okkk^N/~~~~~NFB??/FFFFF?????/??????????; +________??/~~OOoowNN?/BB???@BBA?/??????????; +?______???/FNGGGGHxo?/@BAAAAAB@?/??????????; +?________?/????~~????/????BB????/??????????; +~~~~??????/~~^???????/~N????????/B?????????; +__?????__?/N^wo_ow^N?/???@B@????/??????????; +__?????__?/~~ow[wo~~?/?B@???@B??/??????????; +????{CCCC{/????~AAAA~/????~````~/??????????; +?__????__?/?BFKwwKFB?/????BB????/??????????; +CCCC{CCCC{/AAAA~AAAA~/````~````~/??????????; +?????~~NFB/?????N~~}{/??????N~~~/???????BBB; +r~~???????/pbf}}}ee}}/@@@@@@@@@@/??????????; +BFN~~?????/{}~~N?????/~~~N??????/BBB???????; +???????@~~/?????????N/??????????/??????????; +~~@???????/N?????????/??????????/??????????; +???????~~r/???????^~x/?????????@/??????????; +ow{{{a^^BB/~~~~~~~~~~/N^~~~FzzBB/?????@BBBB; +Ba{{{wwo_?/~~~fBBf~~}/BF~~~^^NF@/B@????????; +?_oww{{{aB/}~~fBBf~~~/@FN^^~~~FB/????????@B; +BB^^a{{{wo/~~~~~~~~~~/BBzzF~~~^N/BBBB@?????; +?_[]^~~~~~/^~BBB_~~~~/??F^~~~~r`/?????@@BBB; +~~~~~^][_?/~~~~_BBB~^/`r~~~~^F??/BBB@@?????; +??_w{}}~NF/w{BBBF~~~}/BFzzz{~~~~/???@BBBBBB; +FN~}}{w_??/}~~~FBBB{w/~~~~{zzzFB/BBBBBB@???; +__________/~~fB@@Bf~~/FFFFEEFFFF/??????????; +?????~~NFB/?????~~~}{/?????~~r`?/?????BBBBB; +BFN~~?????/{}~~~?????/?`r~~?????/BBBBB?????; +?????_____/?????~~~~~/?????~~r`?/?????BBBBB; +__________/~~fB@@Bf~~/?`r~}EFFFF/BBBBB?????; +__________/~~fB@@Bf~~/FFFFE}~r`?/?????BBBBB; +_____?????/~~~~~?????/?`r~~?????/BBBBB?????; +_____~~NFB/~~fB@@Bf}{/FFFFEEFFFF/??????????; +BFN~~?????/{}~~~?????/FFFFF?????/??????????; +?????~~NFB/?????~~~}{/?????FFFFF/??????????; +BFN~~_____/{}fB@@Bf~~/FFFFEEFFFF/??????????; +??????????/~~~~~~}}}}/BBBBBB@@@@/??????????; +??????????/}{{{{{WWWW/@?????????/??????????; +??????????/WWWW{{{{{}/?????????@/??????????; +??????????/}}}}~~~~~~/@@@@BBBBBB/??????????; +????????o~/???????w~~/??????{~~~/??????BBBB; +~o????????/~~w???????/~~~{??????/BBBB??????; +??????~~~~/???????^~~/????????N~/?????????B; +?????????_/?????Wkuz|/???????@BD/??????????; +???????~~r/???????~~x/???????~~[/???????BBA; +_?????????/}^msW?????/EB@???????/??????????; +??????????/??___?????/??BNN?????/?????????? +)"; + +constexpr auto font_12x30 = R"(0;1;1;12;0;2;30;0{ @ +b~~~????????/F~~~????????/^~~~????????/{~~~????????/w~~~????????; +??????~~~NFF/??????~~w_??/???oww~~~~~~/?oNNNN~~~~~~/~~FFFF?~~~~~; +FFN~~~??????/??_w~~??????/~~~~~~wwo???/~~~~~~NNNNo?/~~~~~?FFFF~~; +@Fwwww}~~~~^/??@N~~~~~~wo/?????@BBFFFF/????????????/????????????; +^~~~~}wwwwF@/ow~~~~~~N@??/FFFFBB@?????/????????????/????????????; +????????????/????????????/?????_oowwww/??_{~~~~~~FB/_wFFFF^~~~~}; +b~~~????????/F~~~????????/~~~N????????/@@??????????/????????????; +????????????/????????????/wwwwoo_?????/BF~~~~~~{_??/}~~~~^FFFFw_; +~~wwww?~~~~~/?B{{{{~~~~~~/???BFF~~~~~~/??????~~F@??/??????~~~{ww; +~~~~~?wwww~~/~~~~~~{{{{B?/~~~~~~FFB???/??@F~~??????/ww{~~~??????; +????????????/????????????/???????????w/??????????}~/?????????}~~; +????????????/?????????_oo/????????}~~^/????????~~~{/????????~~~w; +????????}~~~/???????{~~~~/??????w~~~~~/??????~~F@??/??????~~~{ww; +????????????/oo_?????????/^~~}????????/{~~~????????/w~~~????????; +????????~~~b/oooooooo~~~F/~~pp~~~~pp}~/@@@@@@@@@@@@/????????????; +????????????/?owEEEEE]w_?/?^~_?????~~?/??@FEEEEE@??/????????????; +????????????/????W}}?????/?????~~?????/???EEFFEE???/????????????; +????????????/?W]EEEEEE}w?/?_w[KEEEBB@?/?FFEEEEEEEE?/????????????; +????????????/??EEEEeu}]M?/?__?EFFFE}w?/?@FEEEEEEF@?/????????????; +????????????/???_ow[M}}??/?]^ZXWWW~~W?/????????FF??/????????????; +????????????/?}}EEEEEEE??/?bbBBBBBB~{?/?@FEEEEEEF@?/????????????; +????????????/?_w]EEEEEE??/?~~EEEEEE}w?/?@FEEEEEEF@?/????????????; +????????????/?]]EEEEEe}}?/?????{}FB@??/?????FF?????/????????????; +????????????/?w}EEEEEEw??/?xFFEEEMMxw?/?@EEEEEEEF@?/????????????; +????????????/?w}EEEEEE}w?/?@FEEEEEe~^?/??EEEEEEF@??/????????????; +????????????/oooooooooooo/~~pp~~~~ppn^/@@@@@@@@~~~{/????????~~~w; +????????????/oooooooooooo/^npp~~~~pp~~/{~~~@@@@@@@@/w~~~????????; +????????????/?????????_oo/????????M~~~/??????????@@/????????????; +????????????/oooooooooooo/~~pp~~~~pp~~/@@@@@@@@@@@@/????????????; +????????????/oo_?????????/~~~M????????/@@??????????/????????????; +????????????/????????????/w???????????/~}??????????/~~}?????????; +~~~}????????/~~~~{???????/~~~~~w??????/??@F~~??????/ww{~~~??????; +????????????/?ow[MEEM[wo?/?~~KKKKKK~~?/?FF??????FF?/????????????; +????????????/????????????/????????????/????????????/????????????; +????????????/?_w]EEEEE]W?/?^~_?????__?/??@FEEEEEF@?/????????????; +b~~~????????/F~~~oooooooo/^mpp~~~~pp~~/{~~~@@@@@@@@/w~~~????????; +????????????/??}}EEEEEEE?/??~~EEEEE???/??FFEEEEEEE?/????????????; +????????????/??}}EEEEEEE?/??~~EEEEE???/??FF????????/????????????; +????????~~~b/oooooooo~~~F/~~pp~~~~ppm^/@@@@@@@@~~~{/????????~~~w; +????????????/?}}??????}}?/?~~EEEEEE~~?/?FF??????FF?/????????????; +????????????/???EE}}EE???/?????~~?????/???EEFFEE???/????????????; +????????????/??????}}}}}}/???oww~~~~~~/?oNNNN~~~~~~/~~FFFF?~~~~~; +????????????/}}}}}}??????/~~~~~~wwo???/~~~~~~NNNNo?/~~~~~?FFFF~~; +????????????/??}}????????/??~~????????/??FFEEEEEEE?/????????????; +????????????/?}}w_??_w}}?/?~~@BFFB@~~?/?FF??????FF?/????????????; +~~wwww?~~~~~/?B{{{{~~~~~~/???BFF~~~~~~/??????NNNNNN/????????????; +????????????/?w}EEEEEE}w?/?~~??????~~?/?@FEEEEEEF@?/????????????; +????????????/?}}EEEEEE}w?/?~~KKKKKKNB?/?FF?????????/????????????; +~~~~~?wwww~~/~~~~~~{{{{B?/~~~~~~FFB???/NNNNNN??????/????????????; +????????????/?}}EEEEEE}w?/?~~KK[{{mFB?/?FF????@FFE?/????????????; +????????????/?w}EEEEE]W??/?`fEEEEEE}w?/?@FEEEEEEF@?/????????????; +????????????/?EEEE}}EEEE?/?????~~?????/?????FF?????/????????????; +~~~~~???????/~~~~@???????/~~~B????????/~~B?????????/~B??????????; +????????????/?}}??????}}?/?FN}woow}NF?/????@FF@????/????????????; +????????????/?}}??????}}?/?~~w{^^{w~~?/??FB????BF??/????????????; +???{KKK{KKK{/???~KKK~KKK~/???~KKK~KKK~/???~KKK~KKK~/???NKKKNKKKN; +????????????/?}}_????_}}?/??@BF}}FB@??/?????FF?????/????????????; +KKK{KKK{KKK{/KKK~KKK~KKK~/KKK~KKK~KKK~/KKK~KKK~KKK~/KKKNKKKNKKKN; +??????~~~NFF/??????~~w_??/??????F~~~~~/???????N~~~~/????????^~~~; +b~~~????????/F~~~oooooooo/~}pp~~~~pp~~/@@@@@@@@@@@@/????????????; +FFN~~~??????/??_w~~??????/~~~~~F??????/~~~~N???????/~~~^????????; +?????????^~~/??????????^~/???????????F/????????????/????????????; +~~^?????????/~^??????????/F???????????/????????????/????????????; +????????~~~b/????????~~~F/????????N~~~/??????????@@/????????????; +?oww{{A~~BBB/~~~~~~{xxwww/~~~~~~~~~~~~/^~~~~~FrrBBB/?@BBFFG^^WWW; +BA{{{wwo_???/w{~~~~~~~~{o/~~~~p__p~~~~/BF~~~~~~~^F@/WGFFFBB@????; +???_oww{{{AB/o{~~~~~~~~{w/~~~~p__p~~~~/@F^~~~~~~~FB/????@BBFFFGW; +BBB~~A{{wwo?/wwwxx{~~~~~~/~~~~~~~~~~~~/BBBrrF~~~~~^/WWW^^GFFBB@?; +??w}~~~~~~~~/w}xxxxF~~~~~/N~????o~~~~~/??N~~~~~~~FB/???@FN^^~~~}; +~~~~~~~~}w??/~~~~~Fxxxx}w/~~~~~o????~N/BF~~~~~~~N??/}~~~^^NF@???; +???_w{}}~~~^/??{~~~~~~~wo/{~????B~~~~~/F^ffffw~~~~~/??F^~~~~~~~~; +^~~~}}{w_???/ow~~~~~~~{??/~~~~~B????~{/~~~~~wffff^F/~~~~~~~~^F??; +????????????/}}}]MMMM]}}}/~~p??????p~~/NNNNMMMMNNNN/????????????; +??????~~~NFF/??????~~w_??/??????~~~~~~/??????~~F@??/??????~~~{ww; +FFN~~~??????/??_w~~??????/~~~~~~??????/??@F~~??????/ww{~~~??????; +????????????/??????}}}}}}/??????~~~~~~/??????~~F@??/??????~~~{ww; +????????????/}}}]MMMM]}}}/~~p??????p~~/??@F}}MMNNNN/ww{~~~??????; +????????????/}}}]MMMM]}}}/~~p??????p~~/NNNNMM}}F@??/??????~~~{ww; +????????????/}}}}}}??????/~~~~~~??????/??@F~~??????/ww{~~~??????; +??????~~~NFF/}}}]MMNNW_??/~~p??????p~~/NNNNMMMMNNNN/????????????; +FFN~~~??????/??_w~~??????/~~~~~~??????/NNNNNN??????/????????????; +??????~~~NFF/??????~~w_??/??????~~~~~~/??????NNNNNN/????????????; +FFN~~~??????/??_WNNMM]}}}/~~p??????p~~/NNNNMMMMNNNN/????????????; +????????????/{{{{{wwwwwoo/~~~~~~~~~~~~/FFFFFBBBBB@@/????????????; +????????????/oo____??????/~~~~~~^^^MMM/@@??????????/????????????; +????????????/??????____oo/MMM^^^~~~~~~/??????????@@/????????????; +????????????/oowwwww{{{{{/~~~~~~~~~~~~/@@BBBBBFFFFF/????????????; +??????????o~/?????????o~~/????????o~~~/???????_~~~~/???????~~~~~; +~o??????????/~~o?????????/~~~o????????/~~~~_???????/~~~~~???????; +???????~~~~~/???????@~~~~/????????B~~~/?????????B~~/??????????B~; +????????????/????????_oWK/??????]rx{}~/????????@BFH/????????????; +????????~~~b/????????~~~F/????????~~~^/????????~~~{/????????~~~w; +????????????/cwo_????????/~^Nfr]??????/KEB@????????/????????????; +????????????/????????????/??____??????/??FF~~??????/???????????? +)"; + +constexpr auto font_10x16 = R"(0;1;1;10;0;2;16;0{ @ +X~~???????/e~~???????/HNN???????; +?????~~fBB/?_W[[~~~~~/NNBBB?NNNN; +BBf~~?????/~~~~~[[W_?/NNNN?BBBNN; +?@E]}~~~vb/?????@@BBB/??????????; +bv~~~}]E@?/BBB@@?????/??????????; +??????????/???_oww{{[/?GEFFNNNMK; +X~~???????/]NF???????/??????????; +??????????/[{{wwo_???/KMNNNFFEG?; +N^kkko~~~~/??@BB~~^NN/?????NNMKK; +~~~~okkk^N/NN^~~BB@??/KKMNN?????; +??????????/????????_{/???????GNN; +?????????_/???????}~f/???????NNH; +??????w~~~/?????{~^NN/?????NNMKK; +_?????????/f~}???????/HNN???????; +_______~~X/^XX^^^XX[]/??????????; +?_OOOOo_??/FNWOOOONF?/??????????; +???_oo????/??OO^^OO??/??????????; +_oOOOOOo_?/W[[UQQRRP?/??????????; +?OOOOOooO?/GWOOQRR]K?/??????????; +???_oOoo??/CEFDCC^^C?/??????????; +ooOOOOOO??/HXPPPPP^M?/??????????; +?_oOOOOO??/N^QQQQQ]K?/??????????; +ooOOOOOoo?/???[]A@@??/??????????; +_oOOOOO_??/LRRQQQU\K?/??????????; +_oOOOOOo_?/@RQQQQYNF?/??????????; +__________/^XX^^^Xxrf/???????NNH; +__________/bxx^^^XX^^/HNN???????; +?????????_/???????EN^/??????????; +__________/^XX^^^XX^^/??????????; +_?????????/^NE???????/??????????; +??????????/{_????????/NNG???????; +~~~w??????/NN^~{?????/KKMNN?????; +??_oOo_???/]^DCCCD^]?/??????????; +??????????/??????????/??????????; +?_oOOOOo_?/FNWOOOOWG?/??????????; +X~~_______/exx^^^XX^^/HNN???????; +?ooOOOOOO?/?^^QQQQOO?/??????????; +?ooOOOOOO?/?^^AAAA???/??????????; +_______~~X/^XX^^^Xx~e/???????NNH; +oo?????oo?/^^AAAAA^^?/??????????; +??OOooOO??/??OO^^OO??/??????????; +?????ooooo/?_W[[~~~~~/NNBBB?NNNN; +ooooo?????/~~~~~[[W_?/NNNN?BBBNN; +?oo???????/?^^OOOOOO?/??????????; +ooo_?_ooo?/^^@BFB@^^?/??????????; +N^kkko~~~~/??@BB~~~~~/??????????; +_oOOOOOo_?/N^OOOOO^N?/??????????; +ooOOOOOo_?/^^CCCCCFB?/??????????; +~~~~okkk^N/~~~~~BB@??/??????????; +ooOOOOOo_?/^^CCCK]ZP?/??????????; +_oOOOOo_??/HZQQQQQ]K?/??????????; +?OOOooOOO?/????^^????/??????????; +~~~N??????/~~B???????/N?????????; +oo?????oo?/BFM[W[MFB?/??????????; +oo?????oo?/F^KMFMK^F?/??????????; +????}aaaa}/????~GGGG~/????BAAAAB; +?oo????oo?/??@B]]B@??/??????????; +aaaa}aaaa}/GGGG~GGGG~/AAAABAAAAB; +?????~~fBB/?????B~~~~/???????NNN; +X~~_______/[XX^^^XX^^/??????????; +BBf~~?????/~~~~B?????/NNN???????; +????????N~/?????????B/??????????; +~N????????/B?????????/??????????; +???????~~X/???????FN]/??????????; +w{}}}pnn``/~~~~~~^^^^/@BFFFGNNGG; +`p}}}{{wo?/^~~|ww|~~N/GGFFFBB@??; +?ow{{}}}p`/N~~|ww|~~^/??@BBFFFGG; +``nnp}}}{w/^^^^~~~~~~/GGNNGFFFB@; +owuvvN~~~~/BFWww{~~^N/???@BFFNNM; +~~~~Nvvuwo/N^~~{wwWFB/MNNFFB@???; +??_w{}}~nF/{}pppB~~~~/?@EMMNNNNN; +Fn~}}{w_??/~~~~Bppp}{/NNNNNMME@?; +oooooooooo/~~xoooox~~/??????????; +?????~~fBB/?????~~^NN/?????NNMKK; +BBf~~?????/NN^~~?????/KKMNN?????; +?????ooooo/?????~~^NN/?????NNMKK; +oooooooooo/NNXoooox~~/KKMNN?????; +oooooooooo/~~xooooXNN/?????NNMKK; +ooooo?????/NN^~~?????/KKMNN?????; +ooooo~~fBB/~~xoooox~~/??????????; +BBf~~?????/~~~~~?????/??????????; +?????~~fBB/?????~~~~~/??????????; +BBf~~ooooo/~~xoooox~~/??????????; +_______???/^^^^^^^NNN/??????????; +??????????/NNNNEEEEEE/??????????; +??????????/EEEEEENNNN/??????????; +??________/NN^^^^^^^^/??????????; +????????w~/??????_}~~/??????NNNN; +~w????????/~~}_??????/NNNN??????; +??????N~~~/???????B~~/?????????N; +????????_o/?????EJL\m/??????????; +???????~~X/???????~~e/???????NNH; +O_????????/vZJLE?????/??????????; +??????????/??[{{?????/?????????? +)"; + +constexpr auto crouton_sprites_8x10 = std::to_array({ + R"(????Oggs/???????@)", + R"([ggO????/@???????)", + R"(????Ow{C/??????@@)", + R"(C{wO????/@@??????)", + R"(????gg{w/??????@?)", + R"(w{gg????/?@??????)", + R"(????{{CS/????@@@@)", + R"(SC{{????/@@@@????)", + R"(????CCgW/?????@??)", + R"(ogC?????/??@@????)", + R"(????wkSk/?????@@@)", + R"(kSkw????/@@@?????)", + R"(????Owgk/???????@)", + R"(kgwO????/@???????)", + R"(????[cSc/?????BAA)", + R"(Sc[_????/AAAB????)", + R"(????CC{S/????@@@?)", + R"(O{?{????/?@@@????)", +}); + +constexpr auto crouton_sprites_15x12 = std::to_array({ + R"(???????oOOWggks/???????@@@BBBDD)", + R"(sswwWOOo???????/DEAAB@@@???????)", + R"(???????__wwGGGk/?????????BBAAAE)", + R"(kGGGww__???????/EAAABB?????????)", + R"(???????OOwwKKGg/???????@@BBEEAA)", + R"(gGKKwwOO???????/AAEEBB@@???????)", + R"(???????{{CCCssS/???????FFCCCDDD)", + R"(SssCCC{{???????/DDDCCCFF???????)", + R"(???????CCCKggwO/???????CEEBB@@@)", + R"(OoowWKKC???????/@BAAECCC???????)", + R"(???????wwkkSSkk/???????BBEEDDEE)", + R"(SSkk[[ww???????/DDEEFFBB???????)", + R"(???????ooow??ss/???????@@@B??DD)", + R"(ss??wooo???????/DD??B@@@???????)", + R"(???????[[ccSScc/?????????BBAAAA)", + R"(ggGGww?????????/CCDDCCFF???????)", + R"(???????CCCC{{cc/???????EECCFF??)", + R"(__{{??{{???????/??FFCCFF???????)", +}); + +constexpr auto crouton_sprites_10x20 = std::to_array({ + R"(?????????_/?????Wkuz|/???????@BD/??????????)", + R"(_?????????/}^msW?????/EB@???????/??????????)", + R"(?????????_/?????W~@||/??????BAAE/??????????)", + R"(_?????????/||@~W?????/EAAB??????/??????????)", + R"(???????_??/?????A~@tt/?????@BEAA/??????????)", + R"(??_???????/tt@~A?????/AAEB@?????/??????????)", + R"(?????_____/?????~?}AY/?????FCDDD/??????????)", + R"(_____?????/YA}?~?????/DDDCF?????/??????????)", + R"(?????__???/??????`x~e/?????EB??@/??????????)", + R"(????_?????/e{[F@?????/@BAEC?????/??????????)", + R"(???????___/?????}~jTj/?????@BEFE/??????????)", + R"(___???????/TjT~}?????/FEFB@?????/??????????)", + R"(?????????_/?????W{?||/????????AE/??????????)", + R"(_?????????/||?{W?????/EA????????/??????????)", + R"(?????_____/?????NoMqi/??????BAAA/??????????)", + R"(??????????/TLpNo?????/DDDCF?????/??????????)", + R"(?????_____/???????~OO/?????ECF??/??????????)", + R"(?_??_?????/O~??~?????/?FCCF?????/??????????)", +}); + +constexpr auto crouton_sprites_12x30 = std::to_array({ + R"(????????????/????????_oWK/??????]rx{}~/????????@BFH/????????????)", + R"(????????????/cwo_????????/~^Nfr]??????/KEB@????????/????????????)", + R"(????????????/???????wwW[[/??????]~~?~~/???????FFEMM/????????????)", + R"(????????????/[[Www???????/~~?~~]??????/MMEFF???????/????????????)", + R"(????????????/??????_ww[[W/??????`~~?zz/??????@FFMME/????????????)", + R"(????????????/W[[ww_??????/zz?~~`??????/EMMFF@??????/????????????)", + R"(????????????/??????{{Kkkk/??????~~?~`l/??????NNKLLL/????????????)", + R"(????????????/kkkK{{??????/l`~?~~??????/LLLKNN??????/????????????)", + R"(????????????/??????CKWwoo/????????o~~p/??????KFB@@@/????????????)", + R"(????????????/___owK??????/b~~B????????/BBFEKG??????/????????????)", + R"(????????????/??????_ww[{[/??????~~~TiT/??????@FFNMN/????????????)", + R"(????????????/{[{ww_??????/iTi~~~??????/MNMFF@??????/????????????)", + R"(????????????/??????????W[/??????]~~?~~/??????????EM/????????????)", + R"(????????????/[W??????????/~~?~~]??????/ME??????????/????????????)", + R"(????????????/??????{CsSSS/??????FwFwFX/???????FCDDD/????????????)", + R"(????????????/gggGw???????/ewFwFw??????/IIIJGN??????/????????????)", + R"(????????????/??????KKK{KK/?????????~WW/??????NKKN??/????????????)", + R"(????????????/?{???{??????/W~???~??????/?NKKKN??????/????????????)", +}); + +constexpr auto crouton_sprites_10x16 = std::to_array({ + R"(????????_o/?????EJL\m/??????????)", + R"(O_????????/vZJLE?????/??????????)", + R"(??????___o/?????E^OUu/??????????)", + R"(o___??????/uUO^E?????/??????????)", + R"(??????_o__/?????H^oUU/??????????)", + R"(__o_??????/UUo^H?????/??????????)", + R"(?????oOOOO/?????~_nhh/??????????)", + R"(OOOOo?????/hhn_~?????/??????????)", + R"(?????Oo__?/?????oWKFL/??????????)", + R"(???_o?????/J]Rp_?????/??????????)", + R"(??????_ooo/?????N^tyt/??????????)", + R"(ooo_??????/yty^N?????/??????????)", + R"(????????_o/?????EN?Uu/??????????)", + R"(o_????????/uU?NE?????/??????????)", + R"(?????oOOOO/?????B[RTT/??????????)", + R"(____??????/iikb{?????/??????????)", + R"(?????OOoOO/?????o_~CC/??????????)", + R"(?o??o?????/C~__~?????/??????????)", +}); + +namespace { + + constexpr auto croutons = std::to_array({0, 0, 1, 2, 3, 4, 5, 0, 6, 7, 2, 1, 4, 5, 3, 2, 7, 8, 1, 4, 3, 5, 6, 2, 4, 0, 5, 2, 4, 7, 8, 5}); + + auto guess_font_size(const int terminal_id) + { + switch (terminal_id) { + case 1: // VT220 - 8x10 + case 2: // VT240 - 8x10 + return soft_font::size_8x10; + case 24: // VT320 - 15x12 + case 42: // VT1000 - unknown, assumed VT320 compatible + return soft_font::size_15x12; + case 18: // VT330 - 10x20 + case 19: // VT340 - 10x20 + return soft_font::size_10x20; + case 32: // VT382J - 12x30 + case 44: // VT382T - 12x30 + return soft_font::size_12x30; + case 41: // VT420 - 10x16 + case 61: // VT510 - 10x16 + case 64: // VT520 - 10x16 + case 65: // VT525 - 10x16 + case 66: // VTStar - 10x16 + default: // Unknown - assume 10x16 + return soft_font::size_10x16; + } + } + + std::string get_font(const auto font_size) + { + switch (font_size) { + case soft_font::size_8x10: + return font_8x10; + case soft_font::size_15x12: + return font_15x12; + case soft_font::size_10x20: + return font_10x20; + case soft_font::size_12x30: + return font_12x30; + case soft_font::size_10x16: + default: + return font_10x16; + } + } + + std::string_view get_font_header(const auto font_size) + { + switch (font_size) { + case soft_font::size_8x10: + return "1;4;0;0"; + case soft_font::size_15x12: + return "1;15;0;2;12;0"; + case soft_font::size_10x20: + return "1;10;0;2;20;0"; + case soft_font::size_12x30: + return "1;12;0;2;30;0"; + case soft_font::size_10x16: + default: + return "1;10;0;2;16;0"; + } + } + + const auto& get_crouton_sprites(const auto font_size) + { + switch (font_size) { + case soft_font::size_8x10: + return crouton_sprites_8x10; + case soft_font::size_15x12: + return crouton_sprites_15x12; + case soft_font::size_10x20: + return crouton_sprites_10x20; + case soft_font::size_12x30: + return crouton_sprites_12x30; + case soft_font::size_10x16: + default: + return crouton_sprites_10x16; + } + } + +} // namespace + +soft_font::soft_font(const capabilities& caps) + : _font_size{guess_font_size(caps.terminal_id)} +{ + if (caps.has_soft_fonts) { + auto font_data = get_font(_font_size); + // Some terminals (like RLogin) will not cope with DECDLD content + // containing newlines, so we need to strip those out first. + for (auto i = 0; i < font_data.size(); i++) + if (font_data[i] == '\n') + font_data.erase(i--, 1); + std::cout << "\033P" << font_data << "\033\\"; + // VTStar seems to get itself stuck when downloading a soft font, but + // that can be fixed by flooding it with a bunch of SGR sequences. + for (auto i = 0; i < 100; i++) + std::cout << "\033[0;1m"; + std::cout << "\033[m"; + // We enable the new font by default. + std::cout << "\033( @"; + } +} + +soft_font::~soft_font() +{ + // Make sure the ASCII character set is restored on exit. + std::cout << "\033(B"; +} + +void soft_font::init(const int wave) +{ + // Each wave potentially has a differently style of crouton, and we can't + // fit them all in the same font, so we redefine the crouton sprites at the + // start of every level. + const auto index = croutons[(wave - 1) % croutons.size()]; + const auto font_header = get_font_header(_font_size); + const auto& crouton_sprites = get_crouton_sprites(_font_size); + std::cout << "\033P0;91;" << font_header << "{ @" << crouton_sprites[index * 2] << "\033\\"; + std::cout << "\033P0;93;" << font_header << "{ @" << crouton_sprites[index * 2 + 1] << "\033\\"; +} diff --git a/src/font.h b/src/font.h new file mode 100644 index 0000000..daa54e4 --- /dev/null +++ b/src/font.h @@ -0,0 +1,25 @@ +// VT Nibbler +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#pragma once + +class capabilities; + +class soft_font { +public: + enum size { + size_8x10, + size_15x12, + size_10x20, + size_12x30, + size_10x16 + }; + + soft_font(const capabilities& caps); + ~soft_font(); + void init(const int wave); + +private: + const size _font_size; +}; diff --git a/src/levels.cpp b/src/levels.cpp new file mode 100644 index 0000000..895f2e2 --- /dev/null +++ b/src/levels.cpp @@ -0,0 +1,454 @@ +// VT Nibbler +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#include "levels.h" + +#include "coloring.h" +#include "screen.h" + +#include + +namespace { + + constexpr auto map_1 = std::to_array({ + "lqwwqwwqk", + "x xtwux x", + "tqjxxxmqu", + "xlqvnvqkx", + "tvqwvwqvu", + "tqwu twqu", + "x xtqux x", + "tqjx xmqu", + "mqqvqvqqj", + }); + + constexpr auto map_2 = std::to_array({ + "lqqwwwqqk", + "tqwjxmwqu", + "x tqvqu x", + "mwvqwqvwj", + "lvqqnqqvk", + "tqqkxlqqu", + "twktnulwu", + "xxmjxmjxx", + "mvqqvqqvj", + }); + + constexpr auto map_3 = std::to_array({ + "lwwwwwwwk", + "tnnnnnnnu", + "tnnnnnnnu", + "tnnnnnnnu", + "tnnnnnnnu", + "tnnnnnnnu", + "tnnnnnnnu", + "tnnnnnnnu", + "mvvvvvvvj", + }); + + constexpr auto map_4 = std::to_array({ + "lqwwqwwqk", + "tqutqutqu", + "xlvnqnvkx", + "tulu tktu", + "tnutqutnu", + "tumu tjtu", + "xmwnqnwjx", + "tqutqutqu", + "mqvvqvvqj", + }); + + constexpr auto map_11 = std::to_array({ + "lklwqwklk", + "xtnulnnux", + "tjtvnvumu", + "mwvwvkmwj", + " twu twu ", + "lnumwjtnk", + "mnnwnwnnj", + "lutumnjtk", + "mvvvqvqvj", + }); + + constexpr auto map_13 = std::to_array({ + "lwqwwwqwk", + "tjluxtkmu", + "tquxxxtqu", + "xlvjxmvkx", + "tnqqnqqnu", + "xmwkxlwjx", + "tquxxxtqu", + "tkmuxtjlu", + "mvqvvvqvj", + }); + + constexpr auto map_15 = std::to_array({ + "lqqwwwqqk", + "tqwjxmwqu", + "x tqvqu x", + "mqvqwqvqj", + "lqqqnqqqk", + "tqqkxlqqu", + "twktnulwu", + "xxmjxmjxx", + "mvqqvqqvj", + }); + + constexpr auto map_16 = std::to_array({ + "lwwwqwwwk", + "tjxtquxmu", + "twvnwnvwu", + "xtquxtqux", + "tvwnvnwvu", + "tkxtquxlu", + "tnvnwnvnu", + "xtquxtqux", + "mvqvvvqvj", + }); + + constexpr auto croutons_1 = std::to_array({2, 6, 10, 14, 34, 38, 46, 50, 74, 78, 102, 105, 115, 118, 127, 139, 149, 172, 184, 212, 221, 225, 227, 231, 233, 237, 255, 271}); + constexpr auto croutons_4 = std::to_array({2, 8, 14, 23, 27, 36, 42, 48, 68, 70, 76, 82, 84, 91, 95, 140, 144, 148, 193, 197, 204, 212, 220, 225, 233, 240, 246, 252}); + constexpr auto croutons_7 = std::to_array({2, 6, 10, 14, 38, 46, 74, 78, 102, 105, 115, 118, 127, 139, 149, 159, 163, 172, 184, 212, 221, 225, 227, 231, 233, 237, 255, 271}); + constexpr auto croutons_8 = std::to_array({2, 6, 10, 14, 25, 34, 38, 46, 50, 74, 78, 102, 105, 115, 118, 127, 139, 149, 172, 184, 212, 221, 225, 227, 231, 233, 237, 255, 271}); + constexpr auto croutons_11 = std::to_array({0, 4, 12, 16, 40, 42, 70, 78, 80, 82, 102, 107, 112, 118, 157, 165, 170, 176, 180, 186, 204, 212, 220, 244, 250, 257, 269, 272, 288}); + constexpr auto croutons_12 = std::to_array({23, 27, 35, 38, 42, 46, 49, 72, 74, 78, 80, 85, 101, 121, 133, 161, 175, 181, 212, 238, 240, 242, 250, 252, 254, 263}); + constexpr auto croutons_13 = std::to_array({36, 38, 46, 48, 51, 59, 67, 72, 80, 104, 108, 112, 116, 139, 144, 149, 172, 176, 180, 184, 208, 216, 221, 229, 237, 240, 242, 250, 252}); + constexpr auto croutons_16 = std::to_array({23, 27, 36, 38, 42, 46, 48, 74, 78, 106, 110, 114, 121, 133, 142, 146, 172, 174, 178, 182, 184, 210, 214, 223, 235, 242, 246, 250}); + constexpr auto croutons_17 = std::to_array({6, 10, 17, 19, 31, 33, 51, 53, 65, 67, 74, 78, 93, 108, 112, 138, 142, 146, 170, 176, 180, 182, 186, 195, 242, 244, 250, 255, 265}); + constexpr auto croutons_20 = std::to_array({2, 8, 14, 36, 42, 48, 55, 63, 76, 103, 117, 123, 131, 137, 142, 146, 151, 157, 165, 171, 185, 212, 225, 233, 240, 246, 252}); + constexpr auto croutons_30 = std::to_array({8, 35, 39, 45, 49, 53, 57, 61, 65, 71, 81, 93, 123, 131, 136, 144, 152, 157, 165, 195, 207, 217, 223, 227, 231, 235, 239, 243, 249, 253}); + + const auto level_number(const int wave) + { + return (wave - 1) % 32 + 1; + } + + const auto& map_for_wave(const int wave) + { + switch (level_number(wave)) { + case 1: return map_1; + case 2: return map_2; + case 3: return map_3; + case 4: return map_4; + case 5: return map_2; + case 6: return map_3; + case 7: return map_1; + case 8: return map_2; + case 9: return map_3; + case 10: return map_4; + case 11: return map_11; + case 12: return map_2; + case 13: return map_13; + case 14: return map_1; + case 15: return map_15; + case 16: return map_16; + case 17: return map_11; + case 18: return map_3; + case 19: return map_13; + case 20: return map_4; + case 21: return map_11; + case 22: return map_16; + case 23: return map_15; + case 24: return map_1; + case 25: return map_13; + case 26: return map_11; + case 27: return map_16; + case 28: return map_4; + case 29: return map_15; + case 30: return map_3; + case 31: return map_11; + case 32: return map_16; + default: return map_1; + } + } + + const auto map_palette_for_wave(const int wave) + { + switch (level_number(wave)) { + case 1: return palette::white_blue; + case 2: return palette::red_purple; + case 3: return palette::red; + case 4: return palette::green_blue; + case 5: return palette::red_orange; + case 6: return palette::purple; + case 7: return palette::green_red; + case 8: return palette::red_purple; + case 9: return palette::purple; + case 10: return palette::blue_yellow; + case 11: return palette::red_purple; + case 12: return palette::green_blue; + case 13: return palette::cyan_red; + case 14: return palette::purple_orange; + case 15: return palette::red_orange; + case 16: return palette::blue_yellow; + case 17: return palette::green_red; + case 18: return palette::cyan_red; + case 19: return palette::red_purple; + case 20: return palette::green_blue; + case 21: return palette::red_purple; + case 22: return palette::purple_orange; + case 23: return palette::red_orange; + case 24: return palette::red_purple; + case 25: return palette::green_red; + case 26: return palette::green_blue; + case 27: return palette::purple_orange; + case 28: return palette::cyan_red; + case 29: return palette::purple_orange; + case 30: return palette::red; + case 31: return palette::blue_yellow; + case 32: return palette::green_red; + default: return palette::white; + } + } + + + const std::span croutons_for_wave(const int wave) + { + switch (level_number(wave)) { + case 1: return croutons_1; + case 2: return croutons_1; + case 3: return croutons_1; + case 4: return croutons_4; + case 5: return croutons_1; + case 6: return croutons_1; + case 7: return croutons_7; + case 8: return croutons_8; + case 9: return croutons_1; + case 10: return croutons_4; + case 11: return croutons_11; + case 12: return croutons_12; + case 13: return croutons_13; + case 14: return croutons_7; + case 15: return croutons_1; + case 16: return croutons_16; + case 17: return croutons_17; + case 18: return croutons_4; + case 19: return croutons_13; + case 20: return croutons_20; + case 21: return croutons_11; + case 22: return croutons_16; + case 23: return croutons_1; + case 24: return croutons_7; + case 25: return croutons_13; + case 26: return croutons_17; + case 27: return croutons_16; + case 28: return croutons_20; + case 29: return croutons_1; + case 30: return croutons_30; + case 31: return croutons_17; + case 32: return croutons_16; + default: return croutons_1; + } + } + + const std::array crouton_palette_for_wave(const int wave) + { + switch (level_number(wave)) { + case 1: return {palette::bright_green, palette::bright_purple}; + case 2: return {palette::orange, palette::white}; + case 3: return {palette::white, palette::bright_purple}; + case 4: return {palette::bright_purple, palette::bright_yellow}; + case 5: return {palette::bright_yellow, palette::bright_cyan}; + case 6: return {palette::green, palette::red}; + case 7: return {palette::red, palette::orange}; + case 8: return {palette::orange, palette::white}; + case 9: return {palette::bright_purple, palette::orange}; + case 10: return {palette::orange, palette::purple}; + case 11: return {palette::purple, palette::green}; + case 12: return {palette::bright_green, palette::bright_purple}; + case 13: return {palette::bright_purple, palette::white}; + case 14: return {palette::red, palette::orange}; + case 15: return {palette::bright_purple, palette::blue}; + case 16: return {palette::blue, palette::white}; + case 17: return {palette::orange, palette::purple}; + case 18: return {palette::bright_yellow, palette::bright_yellow}; + case 19: return {palette::green, palette::purple}; + case 20: return {palette::bright_yellow, palette::bright_cyan}; + case 21: return {palette::purple, palette::bright_yellow}; + case 22: return {palette::blue, palette::orange}; + case 23: return {palette::bright_red, palette::blue}; + case 24: return {palette::bright_purple, palette::white}; + case 25: return {palette::bright_green, palette::bright_purple}; + case 26: return {palette::bright_purple, palette::bright_yellow}; + case 27: return {palette::bright_yellow, palette::bright_cyan}; + case 28: return {palette::bright_purple, palette::bright_yellow}; + case 29: return {palette::red, palette::orange}; + case 30: return {palette::orange, palette::purple}; + case 31: return {palette::red, palette::red}; + case 32: return {palette::purple, palette::bright_yellow}; + default: return {palette::white, palette::white}; + } + } + + constexpr auto crouton_sprite = "{}"; + constexpr auto wall_sprites = std::to_array({ + " ", + "|D", + "G!", + "|!", + ":;", + ",;", + ":.", + ",.", + "/\\", + "`\\", + "/'", + "`'", + "==", + "<=", + "=>", + "<>", + }); + +} // namespace + +level::level(screen& screen, const int wave) + : _screen{screen}, _wave{wave} +{ + _build_map(); + _build_croutons(); + _build_palette(); +} + +int level::points_per_crouton() const +{ + return _wave * 10; +} + +void level::init_map() +{ + _screen.set_palette(color::wall, map_palette_for_wave(_wave)); + for (auto y = 0; y < 19; y++) { + _screen.write(3 + y, 1, "", color::wall); + for (auto x = 0; x < 19; x++) { + const auto shape = _wall_shape(y, x); + _screen.write(wall_sprites[shape]); + } + _screen.flush(); + } +} + +void level::init_croutons() +{ + const auto crouton_palette = crouton_palette_for_wave(_wave); + _screen.set_palette(color::crouton_1, crouton_palette[0]); + _screen.set_palette(color::crouton_2, crouton_palette[1]); + for (auto i = 0; i < _croutons.size(); i++) { + if (_croutons[i]) { + const auto y = 4 + (i / 17); + const auto x = 3 + (i % 17) * 2; + _screen.write(y, x, crouton_sprite[0], color::crouton_1); + _screen.write(y, x + 1, crouton_sprite[1], color::crouton_2); + _screen.flush(); + } + } + _frame = 0; + _screen.wait_for_terminal(); +} + +void level::update(const int elapsed_frames) +{ + constexpr auto blink_rate = 16; + const auto last_frame = _frame; + _frame += elapsed_frames; + if (_frame / blink_rate > last_frame / blink_rate && _screen.blink_allowed()) { + const auto tick = _frame / blink_rate; + _screen.invoke_macro(tick % 2 == 0 ? _palette_macro_1 : _palette_macro_2); + } +} + +bool level::is_path(const int snake_y, const int snake_x) const +{ + return _is_path((snake_y >> 1) + 1, (snake_x >> 1) + 1); +} + +bool level::eat_crouton(const int snake_y, const int snake_x) +{ + if (snake_y % 2 || snake_x % 2) return false; + auto& crouton = _croutons[(snake_y >> 1) * 17 + (snake_x >> 1)]; + if (!crouton) return false; + crouton = false; + _croutons_remaining--; + return true; +} + +bool level::complete() const +{ + return _croutons_remaining <= 0; +} + +void level::_build_map() +{ + static constexpr auto exit_table = std::to_array({5, 9, 10, 6, 15, 3, 3, 3, 3, 3, 14, 13, 7, 11, 12}); + const auto& map = map_for_wave(_wave); + const auto exits = [&](const auto y, const auto x) { + if (x < 1 && y < 1) return 0b1010; + if (x < 1 && y > 9) return 0b0110; + if (x < 1 || x > 9) return 0b1100; + if (y < 1 || y > 9) return 0b0011; + const auto c = map[y - 1][x - 1]; + return c == ' ' ? 1 : exit_table[c - 'j']; + }; + const auto test_if_path = [&](const auto y, const auto x) { + if ((y % 2) && (x % 2)) + return false; + else if (y % 2) + return (exits(y / 2, x / 2) & 8) != 0; + else if (x % 2) + return (exits(y / 2, x / 2) & 2) != 0; + else + return exits(y / 2, x / 2) != 0; + }; + for (auto y = -1; y < 20; y++) { + for (auto x = -1; x < 20; x++) { + _is_path(y, x) = test_if_path(y + 1, x + 1); + } + } +} + +void level::_build_croutons() +{ + const auto& croutons = croutons_for_wave(_wave); + for (auto crouton : croutons) + _croutons[crouton] = true; + _croutons_remaining = croutons.size(); +} + +void level::_build_palette() +{ + if (_screen.blink_allowed()) { + constexpr auto time_palette = std::to_array({palette::bright_yellow, palette::blue}); + const auto crouton_palette = crouton_palette_for_wave(_wave); + _palette_macro_1 = _screen.define_macro(1, [&]() { + _screen.set_palette(color::time, time_palette[0]); + _screen.set_palette(color::crouton_1, crouton_palette[0]); + _screen.set_palette(color::crouton_2, crouton_palette[1]); + }); + _palette_macro_2 = _screen.define_macro(2, [&]() { + _screen.set_palette(color::time, time_palette[1]); + _screen.set_palette(color::crouton_1, crouton_palette[1]); + _screen.set_palette(color::crouton_2, crouton_palette[0]); + }); + } +} + +int level::_wall_shape(const int y, const int x) const +{ + if (_is_path(y, x)) return 0; + const auto left = _is_path(y + 0, x - 1); + const auto right = _is_path(y + 0, x + 1); + const auto top = _is_path(y - 1, x + 0); + const auto bottom = _is_path(y + 1, x + 0); + return left + (right << 1) + (top << 2) + (bottom << 3); +} + +bool level::_is_path(const int y, const int x) const +{ + return _path[(y + 1) * 21 + (x + 1)]; +} + +bool& level::_is_path(const int y, const int x) +{ + return _path[(y + 1) * 21 + (x + 1)]; +} diff --git a/src/levels.h b/src/levels.h new file mode 100644 index 0000000..df014da --- /dev/null +++ b/src/levels.h @@ -0,0 +1,39 @@ +// VT Nibbler +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#pragma once + +#include +#include + +class screen; + +class level { +public: + level(screen& screen, const int wave); + int points_per_crouton() const; + void init_map(); + void init_croutons(); + void update(const int elapsed_frames); + bool is_path(const int snake_y, const int snake_x) const; + bool eat_crouton(const int snake_y, const int snake_x); + bool complete() const; + +private: + void _build_map(); + void _build_croutons(); + void _build_palette(); + int _wall_shape(const int y, const int x) const; + bool _is_path(const int y, const int x) const; + bool& _is_path(const int y, const int x); + + screen& _screen; + int _wave = 0; + std::string _palette_macro_1; + std::string _palette_macro_2; + std::array _path = {}; + std::array _croutons = {}; + int _croutons_remaining = 0; + int _frame = 0; +}; diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..5311498 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,111 @@ +// VT Nibbler +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#include "capabilities.h" +#include "coloring.h" +#include "engine.h" +#include "font.h" +#include "options.h" +#include "os.h" + +#include +#include +#include + +using namespace std::chrono_literals; + +static bool check_compatibility(const capabilities& caps, const options& options) +{ + if (!caps.has_soft_fonts && !options.yolo) { + std::cout << "VT Nibbler requires a VT320-compatible terminal or better.\n"; + std::cout << "Try 'vtnibbler --yolo' to bypass the compatibility checks.\n"; + return false; + } + if (caps.height < engine::height) { + std::cout << "VT Nibbler requires a minimum screen height of " << engine::height << ".\n"; + return false; + } + if (caps.width < engine::width) { + std::cout << "VT Nibbler requires a minimum screen width of " << engine::width << ".\n"; + return false; + } + return true; +} + +static auto title_banner(const capabilities& caps) +{ + constexpr auto title = "VT NIBBLER"; + const auto y = caps.height / 2; + const auto x = caps.width / 4 - 4; + std::cout << "\033[" << y << ';' << x << "H\033#3" << title; + std::cout << "\033[" << (y + 1) << ';' << x << "H\033#4" << title; + std::cout.flush(); + + return [=]() { + std::this_thread::sleep_for(3s); + // MLTerm doesn't reset double-width lines correctly, so we need to + // manually reset the title banner line before starting the game. + std::cout << "\033[" << y << "H\033[2K\033#5"; + std::cout << "\033[" << (y + 1) << "H\033[2K\033#5"; + std::cout.flush(); + }; +} + +int main(const int argc, const char* argv[]) +{ + os os; + + options options(argc, argv); + if (options.exit) + return 1; + + capabilities caps; + if (!check_compatibility(caps, options)) + return 1; + + // Set the window title. + std::cout << "\033]21;VT Nibbler\033\\"; + // Set default attributes. + std::cout << "\033[m"; + // Clear the screen. + std::cout << "\033[2J"; + // Save the modes and settings that we're going to change. + const auto original_decawm = caps.query_mode(7); + const auto original_decssdt = caps.query_setting("$~"); + // Hide the cursor. + std::cout << "\033[?25l"; + // Disable line wrapping. + std::cout << "\033[?7l"; + // Hide the status line. + std::cout << "\033[0$~"; + // Display title banner + const auto clear_banner = title_banner(caps); + // Load the soft font. + auto font = soft_font{caps}; + // Setup the color assignment and palette. + const auto colors = coloring{caps, options}; + // Clear the title banner + clear_banner(); + + auto game_engine = engine{caps, options, font}; + while (game_engine.run()) { + } + + // Clear the window title. + std::cout << "\033]21;\033\\"; + // Set default attributes. + std::cout << "\033[m"; + // Clear the screen. + std::cout << "\033[H\033[J"; + // Reapply line wrapping if not originally reset. + if (original_decawm != false) + std::cout << "\033[?7h"; + // Restore the original status display type. + if (!original_decssdt.empty()) + std::cout << "\033[" << original_decssdt; + // Show the cursor. + std::cout << "\033[?25h"; + + return 0; +} diff --git a/src/options.cpp b/src/options.cpp new file mode 100644 index 0000000..494477e --- /dev/null +++ b/src/options.cpp @@ -0,0 +1,46 @@ +// VT Nibbler +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#include "options.h" + +#include +#include +#include + +options::options(const int argc, const char* argv[]) +{ + auto ignore_compatibility = false; + for (auto i = 1; i < argc; i++) { + const auto arg = std::string{argv[i]}; + if (arg == "--mono") { + color = false; + } else if (arg == "--mute") { + sound = false; + } else if (arg == "--noblink") { + blink = false; + } else if (arg == "--yolo") { + yolo = true; + } else if (arg == "--speed" && i + 1 < argc) { + try { + fps = std::stoi(argv[++i]) * 10; + fps = std::clamp(fps, 1, 100); + } catch (std::exception) { + // ignore invalid speed + } + } else if (arg == "--help") { + std::cout << "Usage: vtnibbler [OPTION]...\n\n"; + std::cout << " --mono no colors\n"; + std::cout << " --mute no sound effects\n"; + std::cout << " --noblink no blinking effects\n"; + std::cout << " --speed N set initial speed (1 to 10)\n"; + std::cout << " --yolo bypass compatibility checks\n"; + std::cout << " --help display this help and exit\n"; + exit = true; + } else { + std::cout << "VT Nibbler: unrecognized option '" << arg << "'\n"; + std::cout << "Try 'vtnibbler --help' for more information.\n"; + exit = true; + } + } +} diff --git a/src/options.h b/src/options.h new file mode 100644 index 0000000..d83a556 --- /dev/null +++ b/src/options.h @@ -0,0 +1,17 @@ +// VT Nibbler +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#pragma once + +class options { +public: + options(const int argc, const char* argv[]); + + bool color = true; + bool sound = true; + bool blink = true; + bool yolo = false; + bool exit = false; + int fps = 50; +}; diff --git a/src/os.cpp b/src/os.cpp new file mode 100644 index 0000000..e46cc38 --- /dev/null +++ b/src/os.cpp @@ -0,0 +1,72 @@ +// VT Nibbler +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#include "os.h" + +#ifdef _WIN32 + +#include + +DWORD output_mode; +DWORD input_mode; + +os::os() +{ + HANDLE output_handle = GetStdHandle(STD_OUTPUT_HANDLE); + GetConsoleMode(output_handle, &output_mode); + SetConsoleMode(output_handle, output_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN); + HANDLE input_handle = GetStdHandle(STD_INPUT_HANDLE); + GetConsoleMode(input_handle, &input_mode); + SetConsoleMode(input_handle, input_mode & ~ENABLE_LINE_INPUT & ~ENABLE_ECHO_INPUT & ~ENABLE_PROCESSED_INPUT | ENABLE_VIRTUAL_TERMINAL_INPUT); +} + +os::~os() +{ + HANDLE output_handle = GetStdHandle(STD_OUTPUT_HANDLE); + SetConsoleMode(output_handle, output_mode); + HANDLE input_handle = GetStdHandle(STD_INPUT_HANDLE); + SetConsoleMode(input_handle, input_mode); +} + +int os::getch() +{ + char ch; + DWORD chars_read = 0; + HANDLE input_handle = GetStdHandle(STD_INPUT_HANDLE); + ReadConsoleA(input_handle, &ch, 1, &chars_read, NULL); + return chars_read == 1 ? static_cast(ch) : -1; +} + +#endif + +#ifdef __linux__ + +#include +#include +#include + +#include + +struct termios term_attributes; + +os::os() +{ + tcgetattr(STDIN_FILENO, &term_attributes); + auto new_term_attributes = term_attributes; + new_term_attributes.c_lflag &= ~(ICANON | ISIG | ECHO | IEXTEN); + new_term_attributes.c_iflag &= ~(IXON); + tcsetattr(STDIN_FILENO, TCSAFLUSH, &new_term_attributes); +} + +os::~os() +{ + tcsetattr(STDIN_FILENO, TCSAFLUSH, &term_attributes); +} + +int os::getch() +{ + return getchar(); +} + +#endif diff --git a/src/os.h b/src/os.h new file mode 100644 index 0000000..91132a6 --- /dev/null +++ b/src/os.h @@ -0,0 +1,12 @@ +// VT Nibbler +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#pragma once + +class os { +public: + os(); + ~os(); + static int getch(); +}; diff --git a/src/screen.cpp b/src/screen.cpp new file mode 100644 index 0000000..235f402 --- /dev/null +++ b/src/screen.cpp @@ -0,0 +1,367 @@ +// VT Nibbler +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#include "screen.h" + +#include "capabilities.h" +#include "engine.h" +#include "options.h" +#include "os.h" + +#include + +using namespace std::chrono_literals; + +screen::screen(const capabilities& caps, const options& options) + : _caps{caps}, _using_colors{options.color && caps.has_color}, + _using_sound{options.sound && caps.has_macros}, + _blink_allowed{options.blink}, _fps{options.fps}, + _keyboard_thread{&screen::_key_reader, this} +{ + _ri = caps.has_8bit ? "\215" : "\033M"; + _dcs = caps.has_8bit ? "\220" : "\033P"; + _csi = caps.has_8bit ? "\233" : "\033["; + _st = caps.has_8bit ? "\234" : "\033\\"; + _y_indent = std::max((caps.height - engine::height) / 2, 0); + _x_indent = std::max((caps.width - engine::width) / 4 * 2, 0); + _clear_macros(); + reset(); +} + +bool screen::blink_allowed() const +{ + return _blink_allowed; +} + +void screen::reset() +{ + _last_y = -1; + _last_x = -1; + _last_color = color::unknown; + _sgr(color::white); + _write(_csi, "999;999H"); + _write(_csi, "1J"); + wait_for_terminal(); +} + +void screen::clear_line(const int y) +{ + _cup(y, 1); + _write(_csi, 'K'); +} + +void screen::write(const char c) +{ + _write(c); + _last_x++; +} + +void screen::write(const std::string_view s) +{ + for (auto c : s) + write(c); +} + +void screen::write(const int y, const int x, const char c, const color color) +{ + _sgr(color); + _cup(y, x); + _write(c); + _last_x++; +} + +void screen::write(const int y, const int x, const std::string_view s, const color color) +{ + _sgr(color); + _cup(y, x); + for (auto c : s) { + _write(c); + _last_x++; + } +} + +void screen::fill_color(const int top, const int left, const int bottom, const int right, const color color) +{ + if (_using_colors && _caps.has_rectangle_ops) { + const auto abs_top = top + _y_indent; + const auto abs_left = left + _x_indent; + const auto abs_bottom = bottom + _y_indent; + const auto abs_right = right + _x_indent; + const auto attrs = 30 + (int(color) & 7); + _write(_csi, abs_top, ';', abs_left, ';', abs_bottom, ';', abs_right, ";0;", attrs, "$r"); + } +} + +void screen::set_palette(const color color, const std::string_view rgb) +{ + // VTStar can't handle DECCTR and will echo the palette to the screen, so + // even though it supports color, which shouldn't attempt palette changes. + if (_using_colors && _caps.terminal_id != 66) + _write(_dcs, "2$p", int(color), ";2;", rgb, _st); +} + +void screen::set_charset(const std::string_view id) +{ + _write("\033(", id); +} + +void screen::play_sound(const int pitch) +{ + if (_using_sound) + _write(_csi, "4;1;", pitch, ",~"); +} + +// static int max_used = 0; + +void screen::pause(const std::chrono::milliseconds milliseconds) +{ + flush(); + if (!_exit_requested) + std::this_thread::sleep_for(milliseconds); +} + +void screen::flush() +{ + if (_buffer_index) { + if (!_exit_requested) { + std::cout.write(&_buffer[0], _buffer_index); + std::cout.flush(); + } + + // max_used = std::max(max_used, _buffer_index); + // std::cout << "\0337\033[H\033[m\033(B" << _buffer_index << " \t" << max_used << " "; + // if (_caps.has_8bit) + // std::cout << "\r\n8-bit"; + // else + // std::cout << "\r\n7-bit"; + // std::cout << "\0338"; + // std::cout.flush(); + // if (_buffer_index > 100) { + // max_used = 0; + // } + + _buffer_index = 0; + } +} + +void screen::shutdown_keyboard() +{ + _keyboard_shutdown = true; + _keyboard_thread.join(); +} + +void screen::wait_for_terminal() +{ + auto lock = std::unique_lock{_cpr_mutex}; + if (!_exit_requested) { + _cpr_received = false; + _write(_csi, "6n"); + flush(); + // max_used = 0; + _cpr_condition.wait(lock, [this] { return _cpr_received; }); + } +} + +void screen::reset_keys() +{ + _key_pressed = key::none; +} + +key screen::key_pressed() const +{ + return _key_pressed; +} + +bool screen::exit_requested() const +{ + return _exit_requested; +} + +void screen::invoke_macro(const std::string macro) +{ + _write(macro.c_str()); +} + +void screen::_sgr(const color color) +{ + if (!_using_colors) { + auto mono_color = color::mono_normal; + if (color == color::wall) + mono_color = color::mono_bright; + if ((color == color::crouton_1 || color == color::crouton_2) && _blink_allowed) + mono_color = color::mono_blinking; + if (mono_color != _last_color) { + _last_color = mono_color; + switch (mono_color) { + case color::mono_normal: + _write(_csi, "m"); + break; + case color::mono_blinking: + _write(_csi, "0;5m"); + break; + case color::mono_bright: + _write(_csi, "0;1m"); + break; + } + } + } else if (color != _last_color) { + const auto bright = int(color) > 7; + const auto last_bright = int(_last_color) > 7; + const auto prefix = bright != last_bright ? (bright ? "1;" : ";") : ""; + _last_color = color; + switch (color) { + case color::red: + case color::crouton_1: + _write(_csi, prefix, "31m"); + break; + case color::yellow: + _write(_csi, prefix, "33m"); + break; + case color::blue: + _write(_csi, prefix, "34m"); + break; + case color::time: + case color::crouton_2: + _write(_csi, prefix, "35m"); + break; + case color::cyan: + _write(_csi, prefix, "36m"); + break; + case color::white: + _write(_csi, 'm'); + break; + case color::wall: + _write(_csi, prefix, "37m"); + break; + } + } +} + +void screen::_cup(const int y, const int x) +{ + const auto abs_y = y + _y_indent; + const auto abs_x = x + _x_indent; + const auto unknown = _last_y == -1 || _last_x == -1; + auto diff_y = unknown ? 9999 : abs_y - _last_y; + auto diff_x = unknown ? 9999 : abs_x - _last_x; + if (diff_y || diff_x) { + if (abs(diff_y) > 2 && abs(diff_x) > 2) + _write(_csi, abs_y, ';', abs_x, 'H'); + else { + _move_y_relative(diff_y); + _move_x_relative(diff_x); + } + _last_y = abs_y; + _last_x = abs_x; + } +} + +void screen::_move_y_relative(const int diff_y) +{ + if (diff_y == -1) + _write(_ri); + else if (diff_y == -2) + _write(_ri, _ri); + else if (diff_y == 1) + _write('\v'); + else if (diff_y == 2) + _write("\v\v"); + else if (diff_y > 0) + _write(_csi, diff_y, 'B'); + else if (diff_y < 0) + _write(_csi, -diff_y, 'A'); +} + +void screen::_move_x_relative(const int diff_x) +{ + if (diff_x == -1) + _write('\b'); + else if (diff_x == -2) + _write("\b\b"); + else if (diff_x == 1) + _write(_csi, 'C'); + else if (diff_x > 0) + _write(_csi, diff_x, 'C'); + else if (diff_x < 0) + _write(_csi, -diff_x, 'D'); +} + +void screen::_write() +{ +} + +template +void screen::_write(const int n, Args... args) +{ + _write(std::to_string(n)); + _write(args...); +} + +template +void screen::_write(const std::string_view s, Args... args) +{ + for (auto c : s) + _write(c); + _write(args...); +} + +template +void screen::_write(const char c, Args... args) +{ + _buffer[_buffer_index++] = c; + _write(args...); +} + +void screen::_key_reader() +{ + while (!_keyboard_shutdown) { + const auto ch = os::getch(); + if (ch == 'A') { + _key_pressed = key::up; + } else if (ch == 'B') { + _key_pressed = key::down; + } else if (ch == 'C') { + _key_pressed = key::right; + } else if (ch == 'D') { + _key_pressed = key::left; + } else if (ch == 'R') { + _notify_cpr_received(); + if (_exit_requested) break; + } else if (ch == 'Q' || ch == 'q' || ch == 3) { + _exit_requested = true; + if (_cpr_received) break; + } + } +} + +void screen::_notify_cpr_received() +{ + { + auto lock = std::lock_guard{_cpr_mutex}; + _cpr_received = true; + } + _cpr_condition.notify_one(); +} + +std::string screen::_define_macro(const int id, const std::string_view content) +{ + if (_caps.has_macros && content.size() > 0) { + static constexpr auto hex_digits = "0123456789ABCDEF"; + auto hex_content = std::string(content.size() * 2, ' '); + for (auto i = 0; i < content.size(); i++) { + hex_content[i * 2] = hex_digits[(content[i] >> 4) & 0x0F]; + hex_content[i * 2 + 1] = hex_digits[content[i] & 0x0F]; + } + _write(_dcs, id, ";0;1!z", hex_content, _st); + return _csi + std::to_string(id) + "*z"; + } else { + return std::string{content}; + } +} + +void screen::_clear_macros() +{ + if (_caps.has_macros) + _write(_dcs, "0;1;0!z", _st); +} diff --git a/src/screen.h b/src/screen.h new file mode 100644 index 0000000..32c637a --- /dev/null +++ b/src/screen.h @@ -0,0 +1,106 @@ +// VT Nibbler +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#pragma once + +#include "coloring.h" + +#include +#include +#include +#include +#include +#include +#include + +class capabilities; +class options; + +enum class key { + none, + left, + right, + up, + down +}; + +class screen { +public: + screen(const capabilities& caps, const options& options); + bool blink_allowed() const; + void reset(); + void clear_line(const int y); + void write(const char c); + void write(const std::string_view s); + void write(const int y, const int x, const char c, const color color); + void write(const int y, const int x, const std::string_view s, const color color); + void fill_color(const int top, const int left, const int bottom, const int right, const color color); + void set_palette(const color color, const std::string_view rgb); + void set_charset(const std::string_view id); + void play_sound(const int pitch); + void pause(const std::chrono::milliseconds milliseconds); + void flush(); + + void shutdown_keyboard(); + void wait_for_terminal(); + void reset_keys(); + key key_pressed() const; + bool exit_requested() const; + + template + std::string define_macro(const int id, T&& lambda); + void invoke_macro(const std::string macro); + +private: + void _sgr(const color color); + void _cup(const int y, const int x); + void _move_y_relative(const int diff_y); + void _move_x_relative(const int diff_y); + void _write(); + template + void _write(const int n, Args... args); + template + void _write(const std::string_view s, Args... args); + template + void _write(const char c, Args... args); + void _key_reader(); + void _notify_cpr_received(); + std::string _define_macro(const int id, const std::string_view content); + void _clear_macros(); + + const capabilities& _caps; + const bool _using_colors; + const bool _using_sound; + const bool _blink_allowed; + const int _fps; + const char* _ri; + const char* _dcs; + const char* _csi; + const char* _st; + int _y_indent; + int _x_indent; + int _last_y = -1; + int _last_x = -1; + color _last_color = color::unknown; + std::array _buffer = {}; + int _buffer_index = 0; + + volatile key _key_pressed = key::none; + volatile bool _keyboard_shutdown = false; + volatile bool _exit_requested = false; + std::thread _keyboard_thread; + bool _cpr_received = true; + std::condition_variable _cpr_condition; + std::mutex _cpr_mutex; +}; + +template +std::string screen::define_macro(const int id, T&& lambda) +{ + const auto start_index = _buffer_index; + lambda(); + const auto length = static_cast(_buffer_index - start_index); + _buffer_index = start_index; + return _define_macro(id, {&_buffer[start_index], length}); +} diff --git a/src/snake.cpp b/src/snake.cpp new file mode 100644 index 0000000..40d40c7 --- /dev/null +++ b/src/snake.cpp @@ -0,0 +1,288 @@ +// VT Nibbler +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#include "snake.h" + +#include "levels.h" +#include "screen.h" + +using namespace std::literals; + +namespace { + + constexpr auto grid_sprite = "XZZZZZZZZZZZZ"sv; + + constexpr auto head_down_sprites = std::to_array({"lmJK"sv, "jk\"#"sv, "noJK"sv, "ef$%"sv}); + constexpr auto head_up_sprites = std::to_array({"rsNQ"sv, "jk)*"sv, "pqNQ"sv, "gh&("sv}); + constexpr auto head_right_sprites = std::to_array({"ml"sv, "ii"sv, "sr"sv, "ab"sv}); + constexpr auto head_left_sprites = std::to_array({"no"sv, "ii"sv, "pq"sv, "cd"sv}); + + constexpr auto tail_down_sprites = std::to_array({"pqtu"sv, "-@xy"sv, "rsvw"sv, "+? "sv}); + constexpr auto tail_up_sprites = std::to_array({"notu"sv, "[]zU"sv, "lmvw"sv, "^_ "sv}); + constexpr auto tail_right_sprites = std::to_array({"vpzU"sv, "vwvw"sv, "vnxy"sv}); + constexpr auto tail_left_sprites = std::to_array({"suzU"sv, "tutu"sv, "muxy"sv}); + + constexpr auto snake_erase_palette = std::to_array({ + palette::white, + palette::gray, + palette::bright_yellow, + palette::bright_red, + palette::blue, + palette::purple, + palette::bright_yellow, + palette::purple, + }); + + constexpr auto text_erase_palette = std::to_array({ + palette::purple, + palette::cyan, + palette::blue, + palette::white, + palette::gray, + palette::bright_yellow, + palette::bright_red, + palette::bright_yellow, + }); + + int sign(const int n) + { + return (n > 0) - (n < 0); + } + +} // namespace + +snake::snake(screen& screen, const level& level) + : _screen{screen}, _level{level} +{ +} + +void snake::init() +{ + const auto y = 32; + const auto x = 10; + + _body.clear(); + for (auto i = 0; i < 11; i++) + _body.emplace_back(y, x + i); + + std::fill(_occupied.begin(), _occupied.end(), false); + + auto snake_sprite = std::string{}; + snake_sprite += tail_right_sprites[1].substr(0, 2); + snake_sprite += std::string(8, head_right_sprites[1][0]); + snake_sprite += head_right_sprites[3]; + snake_sprite += ' '; + + const auto slow_render = [&](const std::string_view sprite, const auto color, int pitch) { + _render(y, x, "", color); + for (auto c : sprite) { + _screen.write(c); + _screen.play_sound(pitch++); + _screen.pause(32ms); + } + }; + slow_render(grid_sprite, color::yellow, 1); + _screen.pause(200ms); + slow_render(snake_sprite, color::blue, 4); + _screen.pause(200ms); + slow_render(snake_sprite, color::snake, 8); + _screen.wait_for_terminal(); + + _dy = 0; + _dx = 1; + _paused = 0; + _growing = 0; + _just_eaten = false; + _dead = false; +} + +void snake::move() +{ + _just_eaten = false; + if (_can_move(_dy, _dx)) { + auto& head = _body.back(); + _dead = _is_occupied(head.y + _dy * 2, head.x + _dx * 2); + if (_dead) return; + _body.emplace_back(head.y + _dy, head.x + _dx); + _render_head(); + if (_growing > 0) + _growing--; + else { + _render_tail(); + _body.erase(_body.cbegin()); + _paused = 0; + } + _screen.flush(); + } else if (++_paused == 5) { + static constexpr auto dx = std::to_array({0, 0, -1, 1}); + static constexpr auto dy = std::to_array({-1, 1, 0, 0}); + auto dir = -1; + for (auto i = 0; i < 4; i++) { + if (_can_move(dy[i], dx[i])) { + dir = (dir < 0 ? i : -1); + if (dir < 0) break; + } + } + if (dir >= 0) { + _dy = dy[dir]; + _dx = dx[dir]; + move(); + } + } +} + +bool snake::turn(const int dy, const int dx) +{ + const auto& head = _body.back(); + if ((head.y % 2) == 0 && (head.x % 2) == 0 && _can_move(dy, dx)) { + _dy = dy; + _dx = dx; + return true; + } + return false; +} + +void snake::grow() +{ + _growing = 3; + _just_eaten = true; +} + +bool snake::erase() +{ + _screen.set_palette(color::snake, snake_erase_palette[0]); + for (auto i = 1; !_screen.exit_requested(); i++) { + if (i < 26) _screen.play_sound(26 - i); + _screen.pause(32ms); + if (_body.empty()) break; + if (i % 3 == 0 && _screen.blink_allowed()) { + const auto palette_index = i / 3 % snake_erase_palette.size(); + _screen.set_palette(color::snake, snake_erase_palette[palette_index]); + _screen.set_palette(color::text, text_erase_palette[palette_index]); + } + const auto& tail = _body.front(); + if (_body.size() == 1) + _render(tail.y, tail.x, " "); + else { + const auto& next_tail = _body[1]; + const auto dy = next_tail.y - tail.y; + const auto dx = next_tail.x - tail.x; + if (dx) + _render(tail.y, tail.x + (dx < 0), " "); + else if (tail.y % 2) + _render(tail.y + (dy < 0), tail.x, " "); + } + _body.erase(_body.cbegin()); + } + return !_screen.exit_requested(); +} + +std::tuple snake::position() const +{ + const auto& head = _body.back(); + return {head.y, head.x}; +} + +bool snake::just_eaten() const +{ + return _just_eaten; +} + +bool snake::is_dead() const +{ + return _dead; +} + +bool snake::_can_move(const int dy, const int dx) const +{ + const auto& head = _body.back(); + const auto next_y = head.y + dy; + const auto next_x = head.x + dx; + const auto clear1 = _level.is_path(next_y, next_x); + const auto clear2 = _level.is_path(next_y + 1, next_x + 1); + const auto reversing = dx * _dx < 0 || dy * _dy < 0; + return clear1 && clear2 && !reversing; +} + +void snake::_render_head() +{ + const auto& head = _body.back(); + const auto& last_head = _body[_body.size() - 2]; + const auto& further_back = _body[_body.size() - 4]; + const auto dy = head.y - last_head.y; + const auto dx = head.x - last_head.x; + if (dy) { + const auto turn = sign(head.x - further_back.x) + 1; + const auto sprite_offset = (head.y % 2) * 2; + if (dy > 0) { // facing down + _render(head.y - 1, head.x, head_down_sprites[turn].substr(sprite_offset, 2)); + _render(head.y + 1, head.x, head_down_sprites[3].substr(sprite_offset, 2)); + } else { // facing up + _render(head.y, head.x, head_up_sprites[3].substr(sprite_offset, 2)); + _render(head.y + 2, head.x, head_up_sprites[turn].substr(sprite_offset, 2)); + } + } else { + const auto turn = sign(head.y - further_back.y) + 1; + const auto sprite_offset = (head.x % 2); + if (dx > 0) { // facing right + _render(head.y, head.x - 1, head_right_sprites[turn].substr(sprite_offset, 1)); + _render(head.y, head.x, head_right_sprites[3]); + } else { // facing left + _render(head.y, head.x, head_left_sprites[3]); + _render(head.y, head.x + 2, head_left_sprites[turn].substr(sprite_offset, 1)); + } + } + if (head.y % 2 == 0 && head.x % 2 == 0) + _track_occupation(head.y, head.x, true); +} + +void snake::_render_tail() +{ + const auto& tail = _body.front(); + const auto& next_tail = _body[1]; + const auto& further_forward = _body[3]; + const auto dy = next_tail.y - tail.y; + const auto dx = next_tail.x - tail.x; + if (dy) { + const auto turn = sign(further_forward.x - tail.x) + 1; + const auto sprite_offset = (tail.y % 2) * 2; + if (dy > 0) { // facing down + _render(tail.y, tail.x, tail_down_sprites[3].substr(sprite_offset, 2)); + _render(tail.y + 2, tail.x, tail_down_sprites[turn].substr(sprite_offset, 2)); + } else { // facing up + _render(tail.y - 1, tail.x, tail_up_sprites[turn].substr(sprite_offset, 2)); + _render(tail.y + 1, tail.x, tail_up_sprites[3].substr(sprite_offset, 2)); + } + } else { + const auto turn = sign(further_forward.y - tail.y) + 1; + const auto sprite_offset = (tail.x % 2) * 2; + if (dx > 0) { // facing right + _render(tail.y, tail.x, " "); + _render(tail.y, tail.x + 1, tail_right_sprites[turn].substr(sprite_offset, 2)); + } else { // facing left + _render(tail.y, tail.x - 1, tail_left_sprites[turn].substr(sprite_offset, 2)); + _render(tail.y, tail.x + 1, " "); + } + } + if (tail.y % 2 != 0 || tail.x % 2 != 0) + _track_occupation(tail.y - dy, tail.x - dx, false); +} + +void snake::_render(const int y, const int x, const std::string_view s, const color color) +{ + const auto sy = 4 + y / 2; + const auto sx = 3 + x; + _screen.write(sy, sx, s, color); +} + +void snake::_track_occupation(const int y, const int x, const bool occupied) +{ + _occupied[y / 2 * 17 + x / 2] = occupied; +} + +bool snake::_is_occupied(const int y, const int x) const +{ + if (y % 2 != 0 || x % 2 != 0) return false; + return _occupied[y / 2 * 17 + x / 2]; +} diff --git a/src/snake.h b/src/snake.h new file mode 100644 index 0000000..b31b73d --- /dev/null +++ b/src/snake.h @@ -0,0 +1,52 @@ +// VT Nibbler +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#pragma once + +#include "coloring.h" + +#include +#include +#include +#include + +class level; +class screen; + +class snake { +public: + snake(screen& screen, const level& level); + void init(); + bool turn(const int dy, const int dx); + void move(); + void grow(); + bool erase(); + std::tuple position() const; + bool just_eaten() const; + bool is_dead() const; + +private: + struct segment { + int y; + int x; + }; + + bool _can_move(const int dy, const int dx) const; + void _render_head(); + void _render_tail(); + void _render(const int y, const int x, const std::string_view s, const color color = color::red); + void _track_occupation(const int y, const int x, const bool occupied); + bool _is_occupied(const int y, const int x) const; + + screen& _screen; + const level& _level; + std::vector _body; + std::array _occupied = {}; + int _dy = 0; + int _dx = 0; + int _paused = 0; + int _growing = 0; + bool _just_eaten = false; + bool _dead = false; +}; diff --git a/src/status.cpp b/src/status.cpp new file mode 100644 index 0000000..17bae19 --- /dev/null +++ b/src/status.cpp @@ -0,0 +1,150 @@ +// VT Nibbler +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#include "status.h" + +#include "screen.h" + +#include +#include +#include + +using namespace std::chrono_literals; + +namespace { + + std::string format_number(const int n) + { + auto str = std::to_string(n); + for (auto i = 3; str.size() > i; i += 4) + str.insert(str.size() - i, "~"); + return str; + } + +} // namespace + +int status::_high_score = 50000; + +status::status(screen& screen) + : _screen{screen} +{ +} + +status::~status() +{ + _high_score = std::max(_high_score, _score); +} + +void status::init(const int wave) +{ + _frame = 0; + _last_score_frame = 0; + _screen.set_palette(color::time, palette::bright_yellow); + _screen.write(1, 2, "PLAYER1", color::yellow); + _render_score(); + _screen.write(1, 31, "LEFT", color::yellow); + _render_lives(); + _screen.write(2, 2, "HISCORE", color::red); + _render_high_score(); + _screen.write(2, 30, "TIME", color::time); + _render_time(); + _screen.write(22, 16, "WAVE", color::white); + _render_wave(wave); + _screen.flush(); +} + +void status::update(const int elapsed_frames) +{ + const auto last_frame = _frame; + _frame += elapsed_frames; + const auto tick_rate = (_frame - _last_score_frame) >= 220 ? 5 : 30; + if (_frame / tick_rate > last_frame / tick_rate) { + _time -= 10; + _render_time(); + } +} + +void status::add_points(const int points) +{ + _last_score_frame = _frame; + _score += points; + _render_score(); +} + +void status::lose_life() +{ + _screen.set_palette(color::text, palette::purple); + _screen.fill_color(1, 2, 2, 37, color::text); + _screen.fill_color(22, 16, 22, 23, color::text); + _screen.flush(); + _lives--; +} + +void status::gain_life() +{ + _lives++; +} + +void status::apply_bonus() +{ + for (auto i = 0; _time > 0; i++) { + const auto bonus = std::min(_time, 40); + _time -= bonus; + _score += bonus; + _render_score(); + _render_time(); + if (i % 2 == 0) _screen.play_sound(7); + _screen.pause(32ms); + } +} + +void status::reset_time() +{ + _time = 990; +} + +bool status::out_of_time() const +{ + return _time < 0; +} + +bool status::game_over() const +{ + return _lives <= 0; +} + +void status::_render_score() +{ + const auto score_string = format_number(_score); + const auto x = 23 - score_string.length(); + _screen.write(1, x, score_string, color::white); +} + +void status::_render_high_score() +{ + const auto score_string = format_number(_high_score); + const auto x = 23 - score_string.length(); + _screen.write(2, x, score_string, color::cyan); +} + +void status::_render_lives() +{ + auto lives_string = std::to_string(std::clamp(_lives - 1, 0, 99)); + lives_string.insert(0, 2 - lives_string.length(), ' '); + _screen.write(1, 36, lives_string, color::white); +} + +void status::_render_time() +{ + auto time_string = std::to_string(std::max(_time, 0)); + time_string.insert(0, 3 - time_string.length(), ' '); + _screen.write(2, 35, time_string, color::white); +} + +void status::_render_wave(const int wave) +{ + const auto wave_string = std::to_string(wave); + const auto x = 24 - wave_string.length(); + _screen.write(22, x, wave_string, color::white); +} diff --git a/src/status.h b/src/status.h new file mode 100644 index 0000000..0df7b8f --- /dev/null +++ b/src/status.h @@ -0,0 +1,37 @@ +// VT Nibbler +// Copyright (c) 2024 James Holderness +// Distributed under the MIT License + +#pragma once + +class screen; + +class status { +public: + status(screen& screen); + ~status(); + void init(const int wave); + void update(const int elapsed_frames); + void add_points(const int points); + void lose_life(); + void gain_life(); + void apply_bonus(); + void reset_time(); + bool out_of_time() const; + bool game_over() const; + +private: + void _render_score(); + void _render_high_score(); + void _render_lives(); + void _render_time(); + void _render_wave(const int wave); + + screen& _screen; + static int _high_score; + int _score = 0; + int _lives = 3; + int _time = 990; + int _frame = 0; + int _last_score_frame = 0; +};