From 52bdbc67246e55910e6ed1b4fe2c31d438969f48 Mon Sep 17 00:00:00 2001 From: Nick Black Date: Sun, 22 Mar 2020 17:33:08 -0400 Subject: [PATCH] ncplane_translate() accept NULL dest as standard plane #408 (#411) * tetris man page * tetris basic skeleton * tetris: Ticker() * README: fix up some obsolete terminology * tetris: draw the game board * tetris: add NewPiece() * tetris: draw tetriminos * tetris: check for stuck piece, move it down * Accept NULL dst in ncplane_translate() #408 --- CMakeLists.txt | 22 ++++ doc/man/index.html | 1 + doc/man/man1/notcurses-tetris.1.md | 32 +++++ doc/man/man3/notcurses_ncplane.3.md | 8 ++ include/ncpp/Cell.hh | 2 +- include/ncpp/Plane.hh | 17 +-- include/notcurses/notcurses.h | 2 +- src/lib/fill.c | 2 +- src/lib/internal.h | 1 - src/lib/notcurses.c | 3 + src/lib/render.c | 3 + src/tetris/main.cpp | 189 ++++++++++++++++++++++++++++ 12 files changed, 271 insertions(+), 11 deletions(-) create mode 100644 doc/man/man1/notcurses-tetris.1.md create mode 100644 src/tetris/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 18804c24c..7180da5fd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -448,6 +448,27 @@ target_compile_definitions(notcurses-ncreel FORTIFY_SOURCE=2 ) +file(GLOB TETRISSRC CONFIGURE_DEPENDS src/tetris/*.cpp) +add_executable(notcurses-tetris ${TETRISSRC}) +target_include_directories(notcurses-tetris + PRIVATE + include + "${PROJECT_BINARY_DIR}/include" +) +target_link_libraries(notcurses-tetris + PRIVATE + Threads::Threads + notcurses++ +) +target_compile_options(notcurses-tetris + PRIVATE + -Wall -Wextra -W -Wshadow ${DEBUG_OPTIONS} +) +target_compile_definitions(notcurses-tetris + PRIVATE + FORTIFY_SOURCE=2 +) + # notcurses-view file(GLOB VIEWSRCS CONFIGURE_DEPENDS src/view/*.cpp) if(${USE_FFMPEG}) @@ -682,6 +703,7 @@ install(TARGETS notcurses-demo DESTINATION bin) install(TARGETS notcurses-input DESTINATION bin) install(TARGETS notcurses-ncreel DESTINATION bin) install(TARGETS notcurses-tester DESTINATION bin) +install(TARGETS notcurses-tetris DESTINATION bin) if(${USE_FFMPEG}) install(TARGETS notcurses-view DESTINATION bin) endif() diff --git a/doc/man/index.html b/doc/man/index.html index 99bc1f7d6..1315690b1 100644 --- a/doc/man/index.html +++ b/doc/man/index.html @@ -22,6 +22,7 @@ notcurses-ncreel—experiments with ncreels
notcurses-pydemo—validates the Python wrappers
notcurses-tester—unit test driver
+ notcurses-tetris—Tetris in the terminal
notcurses-view—renders images and video to the terminal

C library (section 3)

notcurses_cell—operations on cell objects
diff --git a/doc/man/man1/notcurses-tetris.1.md b/doc/man/man1/notcurses-tetris.1.md new file mode 100644 index 000000000..04d92af10 --- /dev/null +++ b/doc/man/man1/notcurses-tetris.1.md @@ -0,0 +1,32 @@ +% notcurses-tetris(1) +% nick black +% v1.2.3 + +# NAME + +notcurses-tetris - Render images and video to the console + +# SYNOPSIS + +**notcurses-tetris** [**-h|--help**] [**-l loglevel**] + +# DESCRIPTION + +**notcurses-tetris** implements Tetris using notcurses. + +# OPTIONS + +**-h**: Show help and exit. + +**-l loglevel**: Log everything (high log level) or nothing (log level 0) to stderr. + +# NOTES + +Optimal display requires a terminal advertising the **rgb** terminfo(5) +capability, or that the environment variable **COLORTERM** is defined to +**24bit** (and that the terminal honors this variable), along with a +fixed-width font with good coverage of the Unicode Block Drawing Characters. + +# SEE ALSO + +**notcurses(3)** diff --git a/doc/man/man3/notcurses_ncplane.3.md b/doc/man/man3/notcurses_ncplane.3.md index 6be4b6973..a52a400a8 100644 --- a/doc/man/man3/notcurses_ncplane.3.md +++ b/doc/man/man3/notcurses_ncplane.3.md @@ -151,6 +151,14 @@ It is an error for two threads to concurrently access a single ncplane. So long as rendering is not taking place, however, multiple threads may safely output to multiple ncplanes. +**ncplane_translate** translates coordinates expressed relative to the plane +**src**, and writes the coordinates of that cell relative to **dst**. The cell +need not intersect with **dst**, though this will yield coordinates which are +invalid for writing or reading on **dst**. If **dst** is **NULL**, it is taken +to refer to the standard plane. **ncplane_translate_abs** takes coordinates +expressed relative to the standard plane, and returns coordinates relative to +**dst**, returning **false** if the coordinates are invalid for **dst**. + **ncplane_mergedown** writes to **dst** the frame that would be rendered if only **src** and **dst** existed on the z-axis, ad **dst** represented the entirety of the rendering region. Only those cells where **src** intersects with **dst** diff --git a/include/ncpp/Cell.hh b/include/ncpp/Cell.hh index 695a290c7..4f4822fa2 100644 --- a/include/ncpp/Cell.hh +++ b/include/ncpp/Cell.hh @@ -121,7 +121,7 @@ namespace ncpp return cell_simple_p (&_cell); } - uint32_t get_edc_idx () const noexcept + uint32_t get_egc_idx () const noexcept { return cell_egc_idx (&_cell); } diff --git a/include/ncpp/Plane.hh b/include/ncpp/Plane.hh index 792e9baa2..1a7823d07 100644 --- a/include/ncpp/Plane.hh +++ b/include/ncpp/Plane.hh @@ -118,6 +118,14 @@ namespace ncpp return ncplane_pulse (plane, ts, fader, curry) != -1; } + bool mergedown (Plane* dst = nullptr) { + return ncplane_mergedown(*this, dst ? dst->plane : nullptr); + } + + bool mergedown (Plane& dst) { + return mergedown(&dst); + } + bool gradient (const char* egc, uint32_t attrword, uint64_t ul, uint64_t ur, uint64_t ll, uint64_t lr, int ystop, int xstop) const noexcept { return ncplane_gradient (plane, egc, attrword, ul, ur, ll, lr, ystop, xstop) != -1; @@ -860,9 +868,7 @@ namespace ncpp void translate (const Plane *dst, int *y = nullptr, int *x = nullptr) const { - if (dst == nullptr) - throw invalid_argument ("'dst' must be a valid pointer"); - translate (*this, *dst, y, x); + ncplane_translate(*this, dst ? dst->plane: nullptr, y, x); } void translate (const Plane &dst, int *y = nullptr, int *x = nullptr) noexcept @@ -875,10 +881,7 @@ namespace ncpp if (src == nullptr) throw invalid_argument ("'src' must be a valid pointer"); - if (dst == nullptr) - throw invalid_argument ("'dst' must be a valid pointer"); - - translate (*src, *dst, y, x); + ncplane_translate(*src, dst ? dst->plane : nullptr, y, x); } static void translate (const Plane &src, const Plane &dst, int *y = nullptr, int *x = nullptr) noexcept diff --git a/include/notcurses/notcurses.h b/include/notcurses/notcurses.h index f736daca5..e6d1067dd 100644 --- a/include/notcurses/notcurses.h +++ b/include/notcurses/notcurses.h @@ -410,7 +410,7 @@ API struct ncplane* ncplane_dup(struct ncplane* n, void* opaque); // provided a coordinate relative to the origin of 'src', map it to the same // absolute coordinate relative to thte origin of 'dst'. either or both of 'y' -// and 'x' may be NULL. +// and 'x' may be NULL. if 'dst' is NULL, it is taken to be the standard plane. API void ncplane_translate(const struct ncplane* src, const struct ncplane* dst, int* RESTRICT y, int* RESTRICT x); diff --git a/src/lib/fill.c b/src/lib/fill.c index f77dc2491..0d8c1643e 100644 --- a/src/lib/fill.c +++ b/src/lib/fill.c @@ -434,7 +434,7 @@ rotate_output(ncplane* dst, uint32_t tchan, uint32_t bchan){ // // Ideally, rotation through 360 degrees will restore the original 2x1 squre. // Unfortunately, the case where a half block occupies a cell having the same -// fore- and background will see it roated into a single full block. In +// fore- and background will see it rotated into a single full block. In // addition, lower blocks eventually become upper blocks with their channels // reversed. In general: // diff --git a/src/lib/internal.h b/src/lib/internal.h index 624859afc..93c431c4d 100644 --- a/src/lib/internal.h +++ b/src/lib/internal.h @@ -26,7 +26,6 @@ #include #include #include -#include #include "notcurses/notcurses.h" #include "egcpool.h" diff --git a/src/lib/notcurses.c b/src/lib/notcurses.c index 248f9945a..36a3cf343 100644 --- a/src/lib/notcurses.c +++ b/src/lib/notcurses.c @@ -1952,6 +1952,9 @@ bool ncplane_translate_abs(const ncplane* n, int* restrict y, int* restrict x){ void ncplane_translate(const ncplane* src, const ncplane* dst, int* restrict y, int* restrict x){ + if(dst == NULL){ + dst = ncplane_stdplane_const(src); + } if(y){ *y = src->absy - dst->absy + *y; } diff --git a/src/lib/render.c b/src/lib/render.c index c814f39eb..b09866b36 100644 --- a/src/lib/render.c +++ b/src/lib/render.c @@ -382,6 +382,9 @@ postpaint(cell* fb, cell* lastframe, int dimy, int dimx, // paint within the real viewport currently. int ncplane_mergedown(ncplane* restrict src, ncplane* restrict dst){ notcurses* nc = src->nc; + if(dst == NULL){ + dst = nc->stdscr; + } int dimy, dimx; ncplane_dim_yx(dst, &dimy, &dimx); cell* fb = malloc(sizeof(*fb) * dimy * dimx); diff --git a/src/tetris/main.cpp b/src/tetris/main.cpp new file mode 100644 index 000000000..552e6374c --- /dev/null +++ b/src/tetris/main.cpp @@ -0,0 +1,189 @@ +#include +#include +#include +#include +#include +#include +#include + +using namespace std::chrono_literals; + +// "North-facing" tetrimino forms (the form in which they are released from the +// top) are expressed in terms of two rows having between two and four columns. +// We map each game column to four columns and each game row to two rows. +// Each byte of the texture maps to one 4x4 component block (and wastes 7 bits). +static const struct tetrimino { + unsigned color; + const char* texture; +} tetriminos[] = { // OITLJSZ + { 0xcbc900, "****"}, { 0x009caa, " ****"}, { 0x952d98, " * ***"}, + { 0xcf7900, " ****"}, { 0x0065bd, "* ***"}, { 0x69be28, " **** "}, + { 0xbd2939, "** **"} }; + +class TetrisNotcursesErr : public std::runtime_error { +public: + TetrisNotcursesErr(char const* const message) throw() + : std::runtime_error(message) { + } + + virtual char const* what() const throw(){ + return exception::what(); + } +}; + +class Tetris { +public: + Tetris(ncpp::NotCurses& nc) : + nc_(nc), + score_(0), + msdelay_(10ms), + curpiece_(nullptr), + stdplane_(nc_.get_stdplane()) + { + curpiece_ = NewPiece(); + DrawBoard(); + } + + // 0.5 cell aspect: One board height == one row. One board width == two columns. + static constexpr auto BOARD_WIDTH = 10; + static constexpr auto BOARD_HEIGHT = 20; + + // FIXME ideally this would be called from constructor :/ + void Ticker(){ + std::chrono::milliseconds ms; + mtx_.lock(); + do{ + ms = msdelay_; + // FIXME loop and verify we didn't get a spurious wakeup + mtx_.unlock(); + std::this_thread::sleep_for(ms); + mtx_.lock(); + if(curpiece_){ + int y, x; + curpiece_->get_yx(&y, &x); + ++y; + if(PieceStuck()){ + // FIXME lock it into place, get next piece + }else{ + if(!curpiece_->move(y, x) || !nc_.render()){ + // FIXME + } + } + } + }while(ms != std::chrono::milliseconds::zero()); + } + + void Stop(){ + mtx_.lock(); + msdelay_ = std::chrono::milliseconds::zero(); // FIXME wake it up? + mtx_.unlock(); + } + +private: + ncpp::NotCurses& nc_; + uint64_t score_; + std::mutex mtx_; + std::chrono::milliseconds msdelay_; + std::unique_ptr curpiece_; + ncpp::Plane* stdplane_; + + void DrawBoard(){ + int y, x; + stdplane_->get_dim(&y, &x); + uint64_t channels = 0; + channels_set_fg(&channels, 0x00b040); + if(!stdplane_->cursor_move(y - (BOARD_HEIGHT + 2), x / 2 - (BOARD_WIDTH + 1))){ + throw TetrisNotcursesErr("cursor_move()"); + } + if(!stdplane_->rounded_box(0, channels, y - 1, x / 2 + BOARD_WIDTH + 1, NCBOXMASK_TOP)){ + throw TetrisNotcursesErr("rounded_box()"); + } + if(!nc_.render()){ + throw TetrisNotcursesErr("render()"); + } + } + + bool PieceStuck(){ + if(!curpiece_){ + return false; + } + // check for impact. iterate over bottom row of piece's plane, checking for + // presence of glyph. if there, check row below. if row below is occupied, + // we're stuck. + int y, x; + curpiece_->get_dim(&y, &x); + --y; + while(x--){ + int cmpy = y + 1, cmpx = x; // need absolute coordinates via translation + curpiece_->translate(nullptr, &cmpy, &cmpx); + ncpp::Cell c; + auto egc = nc_.get_at(cmpy, cmpx, c); + if(!egc){ + return false; // FIXME is this not indicative of an error? + } + if(*egc && *egc != ' '){ + return true; + } + } + return false; + } + + // tidx is an index into tetriminos. yoff and xoff are relative to the + // terminal's origin. returns colored north-facing tetrimino on a plane. + std::unique_ptr NewPiece(){ + const int tidx = random() % 7; + const struct tetrimino* t = &tetriminos[tidx]; + const size_t cols = strlen(t->texture); + int y, x; + stdplane_->get_dim(&y, &x); + const int xoff = x / 2 - BOARD_WIDTH + (random() % BOARD_WIDTH - 3); + const int yoff = y - (BOARD_HEIGHT + 4); + std::unique_ptr n = std::make_unique(2, cols, yoff, xoff, nullptr); + if(n){ + uint64_t channels = 0; + channels_set_bg_alpha(&channels, CELL_ALPHA_TRANSPARENT); + channels_set_fg_alpha(&channels, CELL_ALPHA_TRANSPARENT); + n->set_fg(t->color); + n->set_base(channels, 0, ""); + y = 0; + for(size_t i = 0 ; i < strlen(t->texture) ; ++i){ + if(t->texture[i] == '*'){ + if(n->putstr(y, x, "██") < 0){ + return NULL; + } + } + y += ((x = ((x + 2) % cols)) == 0); + } + } + return n; + } + +}; + +int main(void){ + if(setlocale(LC_ALL, "") == nullptr){ + return EXIT_FAILURE; + } + notcurses_options ncopts{}; + ncpp::NotCurses nc(ncopts); + Tetris t{nc}; + std::thread tid(&Tetris::Ticker, &t); + char32_t input; + ncinput ni; + while((input = nc.getc(true, &ni)) != (char32_t)-1){ + if(input == 'q'){ + break; + } + switch(input){ + case NCKEY_LEFT: break; + case NCKEY_RIGHT: break; + } + } + if(input == 'q'){ + t.Stop(); + tid.join(); + }else{ + return EXIT_FAILURE; + } + return nc.stop() ? EXIT_SUCCESS : EXIT_FAILURE; +}