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/data/tetris-background.jpeg b/data/tetris-background.jpeg new file mode 100644 index 000000000..e7df7d3ad Binary files /dev/null and b/data/tetris-background.jpeg differ diff --git a/doc/Doxyfile b/doc/Doxyfile index 2e8b41f84..618e08312 100644 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -829,7 +829,7 @@ WARN_LOGFILE = # spaces. See also FILE_PATTERNS and EXTENSION_MAPPING # Note: If this tag is empty the current directory is searched. -INPUT = ../src/lib ../include/notcurses.h +INPUT = ../src/lib ../include # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses 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..ac053fda0 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; @@ -295,6 +303,20 @@ namespace ncpp return move_above (*above); } + bool mergedown (Plane &dst) const + { + if (plane == dst.plane) + throw invalid_argument ("'dst' must refer to a differnt plane than the one this method is called on"); + return ncplane_mergedown (plane, dst.plane) != -1; + } + + bool mergedown (Plane *dst) const + { + if (dst == nullptr) + throw invalid_argument ("'dst' must be a valid pointer"); + return mergedown (*dst); + } + bool cursor_move (int y, int x) const noexcept { return ncplane_cursor_move_yx (plane, y, x) != -1; @@ -860,9 +882,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 +895,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..ad69d6e2a 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); @@ -556,7 +556,7 @@ ncplane_move_below(struct ncplane* n, struct ncplane* below){ return ncplane_move_below_unsafe(n, below); } -// Return the plane above this one, or NULL if this is at the top. +// Return the plane below this one, or NULL if this is at the bottom. API struct ncplane* ncplane_below(struct ncplane* n); // Rotate the plane pi/2 radians clockwise or counterclockwise. Note that @@ -958,7 +958,8 @@ API int ncplane_stain(struct ncplane* n, int ystop, int xstop, uint64_t ul, // rendering region." Merging is independent of the position of 'src' viz 'dst' // on the z-axis. If 'src' does not intersect with 'dst', 'dst' will not be // changed, but it is not an error. The source plane still exists following -// this operation. Do not supply the same plane for both 'src' and 'dst'. +// this operation. If 'dst' is NULL, it will be interpreted as the standard +// plane. Do not supply the same plane for both 'src' and 'dst'. API int ncplane_mergedown(struct ncplane* RESTRICT src, struct ncplane* RESTRICT dst); // Erase every cell in the ncplane, resetting all attributes to normal, all @@ -1975,13 +1976,13 @@ ncvisual_simple_streamer(struct notcurses* nc, struct ncvisual* ncv, void* curry // Stream the entirety of the media, according to its own timing. Blocking, // obviously. streamer may be NULL; it is otherwise called for each frame, and -// its return value handled as outlined for stream cb. Pretty raw; beware. -// If streamer() returns non-zero, the stream is aborted, and that value is -// returned. By convention, return a positive number to indicate intentional -// abort from within streamer(). 'timescale' allows the frame duration time to -// be scaled. For a visual naturally running at 30FPS, a 'timescale' of 0.1 -// will result in 300FPS, and a 'timescale' of 10 will result in 3FPS. It is an -// error to supply 'timescale' less than or equal to 0. +// its return value handled as outlined for stream cb. If streamer() returns +// non-zero, the stream is aborted, and that value is returned. By convention, +// return a positive number to indicate intentional abort from within +// streamer(). 'timescale' allows the frame duration time to be scaled. For a +// visual naturally running at 30FPS, a 'timescale' of 0.1 will result in +// 300FPS, and a 'timescale' of 10 will result in 3FPS. It is an error to +// supply 'timescale' less than or equal to 0. API int ncvisual_stream(struct notcurses* nc, struct ncvisual* ncv, int* averr, float timescale, streamcb streamer, void* curry); diff --git a/src/lib/fill.c b/src/lib/fill.c index f77dc2491..6f95077d8 100644 --- a/src/lib/fill.c +++ b/src/lib/fill.c @@ -57,9 +57,11 @@ ncplane_polyfill_recurse(ncplane* n, int y, int x, const cell* c){ // at the initial step only, invalid y, x is an error, so explicitly check. int ncplane_polyfill_yx(ncplane* n, int y, int x, const cell* c){ int ret = -1; - if(y < n->leny && x < n->lenx){ - if(y >= 0 && x >= 0){ - ret = ncplane_polyfill_recurse(n, y, x, c); + if(c->gcluster){ // can't polyfill with a null EGC + if(y < n->leny && x < n->lenx){ + if(y >= 0 && x >= 0){ + ret = ncplane_polyfill_recurse(n, y, x, c); + } } } return ret; @@ -434,7 +436,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..6eb2b1ba9 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" @@ -545,6 +544,20 @@ cell_debug(const egcpool* p, const cell* c){ } } +static inline void +plane_debug(const ncplane* n){ + int dimy, dimx; + ncplane_dim_yx(n, &dimy, &dimx); + fprintf(stderr, "p: %p dim: %d/%d poolsize: %d\n", n, dimy, dimx, n->pool.poolsize); + for(int y = 0 ; y < 1 ; ++y){ + for(int x = 0 ; x < 10 ; ++x){ + const cell* c = &n->fb[fbcellidx(y, dimx, x)]; + fprintf(stderr, "[%03d/%03d] ", y, x); + cell_debug(&n->pool, c); + } + } +} + // True if the cell does not generate background pixels. Only the FULL BLOCK // glyph has this property, AFAIK. // FIXME set a bit, doing this at load time @@ -572,7 +585,6 @@ cell_duplicate_far(egcpool* tpool, cell* targ, const ncplane* splane, const cell return !!c->gcluster; } size_t ulen = strlen(extended_gcluster(splane, c)); -//fprintf(stderr, "[%s] (%zu)\n", egcpool_extended_gcluster(&splane->pool, c), strlen(egcpool_extended_gcluster(&splane->pool, c))); int eoffset = egcpool_stash(tpool, extended_gcluster(splane, c), ulen); if(eoffset < 0){ return -1; 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..92dbd1ae9 100644 --- a/src/lib/render.c +++ b/src/lib/render.c @@ -203,19 +203,21 @@ lock_in_highcontrast(cell* targc, struct crender* crender){ } } -// Paints a single ncplane into the provided framebuffer 'fb'. Whenever a cell -// is locked in, it is compared against the last frame. If it is different, the -// 'damagevec' bitmap is updated with a 1. 'pool' is typically nc->pool, but can +// Paints a single ncplane into the provided scratch framebuffer 'fb', and +// ultimately 'lastframe' (we can't always write directly into 'lastframe', +// because we need build state to solve certain cells, and need compare their +// solved result to the last frame). Whenever a cell is locked in, it is +// compared against the last frame. If it is different, the 'rvec' bitmap is updated with a 1. 'pool' is typically nc->pool, but can // be whatever's backing fb. static int -paint(notcurses* nc, ncplane* p, cell* lastframe, struct crender* rvec, cell* fb, egcpool* pool){ +paint(ncplane* p, cell* lastframe, struct crender* rvec, + cell* fb, egcpool* pool, int dstleny, int dstlenx, + int dstabsy, int dstabsx, int lfdimx){ int y, x, dimy, dimx, offy, offx; - // don't use ncplane_dim_yx()/ncplane_yx() here, lest we deadlock - dimy = p->leny; - dimx = p->lenx; - offy = p->absy - nc->stdscr->absy; - offx = p->absx - nc->stdscr->absx; -//fprintf(stderr, "PLANE %p %d %d %d %d %d %d\n", p, dimy, dimx, offy, offx, nc->stdscr->leny, nc->stdscr->lenx); + ncplane_dim_yx(p, &dimy, &dimx); + offy = p->absy - dstabsy; + offx = p->absx - dstabsx; +//fprintf(stderr, "PLANE %p %d %d %d %d %d %d\n", p, dimy, dimx, offy, offx, dstleny, dstlenx); // skip content above or to the left of the physical screen int starty, startx; if(offy < 0){ @@ -231,19 +233,19 @@ paint(notcurses* nc, ncplane* p, cell* lastframe, struct crender* rvec, cell* fb for(y = starty ; y < dimy ; ++y){ const int absy = y + offy; // once we've passed the physical screen's bottom, we're done - if(absy >= nc->stdscr->leny){ + if(absy >= dstleny){ break; } for(x = startx ; x < dimx ; ++x){ const int absx = x + offx; - if(absx >= nc->stdscr->lenx){ + if(absx >= dstlenx){ break; } - cell* targc = &fb[fbcellidx(absy, nc->stdscr->lenx, absx)]; + cell* targc = &fb[fbcellidx(absy, dstlenx, absx)]; if(cell_locked_p(targc)){ continue; } - struct crender* crender = &rvec[fbcellidx(absy, nc->stdscr->lenx, absx)]; + struct crender* crender = &rvec[fbcellidx(absy, dstlenx, absx)]; const cell* vis = &p->fb[nfbcellidx(p, y, x)]; // if we never loaded any content into the cell (or obliterated it by // writing in a zero), use the plane's base cell. @@ -263,7 +265,7 @@ paint(notcurses* nc, ncplane* p, cell* lastframe, struct crender* rvec, cell* fb // screen, nor if we're bisected by a higher plane. if(cell_double_wide_p(vis)){ // are we on the last column of the real screen? if so, 0x20 us - if(absx >= nc->stdscr->lenx - 1){ + if(absx >= dstlenx - 1){ targc->gcluster = ' '; // is the next cell occupied? if so, 0x20 us }else if(targc[1].gcluster){ @@ -313,13 +315,14 @@ paint(notcurses* nc, ncplane* p, cell* lastframe, struct crender* rvec, cell* fb // which were already locked in were skipped at the top of the loop)? if(cell_locked_p(targc)){ lock_in_highcontrast(targc, crender); - cell* prevcell = &lastframe[fbcellidx(absy, nc->lfdimx, absx)]; + cell* prevcell = &lastframe[fbcellidx(absy, lfdimx, absx)]; /*if(cell_simple_p(targc)){ fprintf(stderr, "WROTE %u [%c] to %d/%d (%d/%d)\n", targc->gcluster, prevcell->gcluster, y, x, absy, absx); }else{ fprintf(stderr, "WROTE %u [%s] to %d/%d (%d/%d)\n", targc->gcluster, extended_gcluster(crender->p, targc), y, x, absy, absx); } -fprintf(stderr, "POOL: %p NC: %p SRC: %p\n", nc->pool.pool, nc, crender->p);*/ +fprintf(stderr, "POOL: %p NC: %p SRC: %p\n", pool->pool, nc, crender->p); +}*/ if(cellcmp_and_dupfar(pool, prevcell, crender->p, targc)){ crender->damaged = true; if(cell_double_wide_p(targc)){ @@ -382,21 +385,35 @@ 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); + cell* tmpfb = malloc(sizeof(*tmpfb) * dimy * dimx); + cell* rendfb = malloc(sizeof(*rendfb) * dimy * dimx); const size_t crenderlen = sizeof(struct crender) * dimy * dimx; struct crender* rvec = malloc(crenderlen); memset(rvec, 0, crenderlen); - init_fb(fb, dimy, dimx); - if(paint(nc, src, dst->fb, rvec, fb, &dst->pool) || paint(nc, dst, dst->fb, rvec, fb, &dst->pool)){ + init_fb(tmpfb, dimy, dimx); + init_fb(rendfb, dimy, dimx); + if(paint(src, rendfb, rvec, tmpfb, &dst->pool, dst->leny, dst->lenx, + dst->absy, dst->absx, dst->lenx)){ + free(rvec); + free(rendfb); + free(tmpfb); + return -1; + } + if(paint(dst, rendfb, rvec, tmpfb, &dst->pool, dst->leny, dst->lenx, + dst->absy, dst->absx, dst->lenx)){ free(rvec); - free(fb); + free(rendfb); + free(tmpfb); return -1; } - postpaint(fb, dst->fb, dimy, dimx, rvec, &dst->pool); + postpaint(tmpfb, rendfb, dimy, dimx, rvec, &dst->pool); free(dst->fb); - dst->fb = fb; + dst->fb = rendfb; free(rvec); return 0; } @@ -417,7 +434,9 @@ notcurses_render_internal(notcurses* nc, struct crender* rvec){ init_fb(fb, dimy, dimx); ncplane* p = nc->top; while(p){ - if(paint(nc, p, nc->lastframe, rvec, fb, &nc->pool)){ + if(paint(p, nc->lastframe, rvec, fb, &nc->pool, + nc->stdscr->leny, nc->stdscr->lenx, + nc->stdscr->absy, nc->stdscr->absx, nc->lfdimx)){ free(fb); return -1; } @@ -787,9 +806,9 @@ update_palette(notcurses* nc, FILE* out){ // * refresh -- write the stream to the emulator // Takes a rendered frame (a flat framebuffer, where each cell has the desired -// EGC, attribute, and channels) and the previously-rendered frame, and spits -// out an optimal sequence of terminal-appropriate escapes and EGCs. There -// should be an rvec entry for each cell; only the 'damaged' field is used. +// EGC, attribute, and channels), which has been written to nc->lastframe, and +// spits out an optimal sequence of terminal-appropriate escapes and EGCs. There +// should be an rvec entry for each cell, but only the 'damaged' field is used. static int notcurses_rasterize(notcurses* nc, const struct crender* rvec){ FILE* out = nc->rstate.mstreamfp; diff --git a/src/tetris/main.cpp b/src/tetris/main.cpp new file mode 100644 index 000000000..8a3ef153b --- /dev/null +++ b/src/tetris/main.cpp @@ -0,0 +1,330 @@ +#include +#include +#include +#include +#include +#include +#include + +const std::string BackgroundFile = "../data/tetris-background.jpeg"; + +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(const std::string& s) throw() + : std::runtime_error(s) { + } + 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, std::atomic_bool& gameover) : + nc_(nc), + score_(0), + msdelay_(100ms), + curpiece_(nullptr), + board_(nullptr), + backg_(nullptr), + stdplane_(nc_.get_stdplane()), + gameover_(gameover) + { + DrawBoard(); + curpiece_ = NewPiece(); + } + + // 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); + const std::lock_guard lock(mtx_); + if(curpiece_){ + int y, x; + curpiece_->get_yx(&y, &x); + if(PieceStuck()){ + if(y <= board_top_y_ - 2){ + gameover_ = true; + return; + } + curpiece_->mergedown(*board_); + curpiece_ = NewPiece(); + }else{ + ++y; + if(!curpiece_->move(y, x) || !nc_.render()){ + throw TetrisNotcursesErr("move() or render()"); + } + } + } + }while(!gameover_); + } + + void MoveLeft() { + const std::lock_guard lock(mtx_); + int y, x; + if(!PrepForMove(&y, &x)){ + return; + } + // For each line of the current piece, find the leftmost populated column. + // Check the game area to the immediate left. If something's there, we + // can't make this move. + ncpp::Cell c; + for(int ly = 0 ; ly < curpiece_->get_dim_y() ; ++ly){ + int lx = 0; + while(lx < curpiece_->get_dim_x()){ + if(curpiece_->get_at(ly, lx, &c)){ + if(c.get().gcluster && c.get().gcluster != ' '){ + break; + } + } + ++lx; + } + if(lx < curpiece_->get_dim_x()){ // otherwise, nothing on this row + ncpp::Cell b; + int cmpy = ly, cmpx = lx - 1; + curpiece_->translate(*board_, &cmpy, &cmpx); + if(board_->get_at(cmpy, cmpx, &b)){ + if(b.get().gcluster && b.get().gcluster != ' '){ + return; // move is blocked + } + } + } + } + --x; + if(!curpiece_->move(y, x) || !nc_.render()){ // FIXME needs y? + throw TetrisNotcursesErr("move() or render()"); + } + } + + void MoveRight() { + const std::lock_guard lock(mtx_); + int y, x; + if(!PrepForMove(&y, &x)){ + return; + } + // For each line of the current piece, find the rightmost populated column. + // Check the game area to the immediate right. If something's there, we + // can't make this move. + ncpp::Cell c; + for(int ly = 0 ; ly < curpiece_->get_dim_y() ; ++ly){ + int lx = curpiece_->get_dim_x() - 1; + while(lx >= 0){ + if(curpiece_->get_at(ly, lx, &c)){ + if(c.get().gcluster && c.get().gcluster != ' '){ + break; + } + } + --lx; + } + if(lx >= 0){ // otherwise, nothing on this row + ncpp::Cell b; + int cmpy = ly, cmpx = lx + 1; + curpiece_->translate(*board_, &cmpy, &cmpx); + if(board_->get_at(cmpy, cmpx, &b)){ + if(b.get().gcluster && b.get().gcluster != ' '){ + return; // move is blocked + } + } + } + } + ++x; + if(!curpiece_->move(y, x) || !nc_.render()){ // FIXME needs y? + throw TetrisNotcursesErr("move() or render()"); + } + } + + void RotateCcw() { + const std::lock_guard lock(mtx_); + int y, x; + if(!PrepForMove(&y, &x)){ + return; + } + // FIXME rotate that fucker ccw + } + + void RotateCw() { + const std::lock_guard lock(mtx_); + int y, x; + if(!PrepForMove(&y, &x)){ + return; + } + // FIXME rotate that fucker cw + } + +private: + ncpp::NotCurses& nc_; + uint64_t score_; + std::mutex mtx_; + std::chrono::milliseconds msdelay_; + std::unique_ptr curpiece_; + std::unique_ptr board_; + std::unique_ptr backg_; + ncpp::Plane* stdplane_; + std::atomic_bool& gameover_; + int board_top_y_; + + // Returns true if there's a current piece which can be moved + bool PrepForMove(int* y, int* x) { + if(!curpiece_){ + return false; + } + curpiece_->get_yx(y, x); + return true; + } + + // background is drawn to the standard plane, at the bottom. + void DrawBackground(const std::string& s) { + int averr; + try{ + backg_ = std::make_unique(s.c_str(), &averr, 0, 0, ncpp::NCScale::Stretch); + }catch(std::exception& e){ + throw TetrisNotcursesErr("visual(): " + s + ": " + e.what()); + } + if(!backg_->decode(&averr)){ + throw TetrisNotcursesErr("decode(): " + s); + } + if(!backg_->render(0, 0, 0, 0)){ + throw TetrisNotcursesErr("render(): " + s); + } + } + + // draw the background on the standard plane, then create a new plane for + // the play area. + void DrawBoard() { + DrawBackground(BackgroundFile); + int y, x; + stdplane_->get_dim(&y, &x); + board_top_y_ = y - (BOARD_HEIGHT + 2); + board_ = std::make_unique(BOARD_HEIGHT, BOARD_WIDTH * 2, + board_top_y_, x / 2 - (BOARD_WIDTH + 1)); + uint64_t channels = 0; + channels_set_fg(&channels, 0x00b040); + channels_set_bg_alpha(&channels, CELL_ALPHA_TRANSPARENT); + if(!board_->rounded_box(0, channels, BOARD_HEIGHT - 1, BOARD_WIDTH * 2 - 1, NCBOXMASK_TOP)){ + throw TetrisNotcursesErr("rounded_box()"); + } + channels_set_fg_alpha(&channels, CELL_ALPHA_TRANSPARENT); + board_->set_base(channels, 0, ""); + 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 game area coordinates via translation + curpiece_->translate(*board_, &cmpy, &cmpx); + ncpp::Cell c; + if(board_->get_at(cmpy, cmpx, &c) < 0){ + throw TetrisNotcursesErr("get_at()"); + } + if(c.get().gcluster && c.get().gcluster != ' '){ + 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 - 1); + std::unique_ptr n = std::make_unique(2, cols, board_top_y_ - 2, 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_bg_alpha(CELL_ALPHA_TRANSPARENT); + 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; + } + std::atomic_bool gameover = false; + notcurses_options ncopts{}; + ncpp::NotCurses nc(ncopts); + Tetris t{nc, gameover}; + std::thread tid(&Tetris::Ticker, &t); + ncpp::Plane* stdplane = nc.get_stdplane(); + char32_t input = 0; + ncinput ni; + while(!gameover && (input = nc.getc(true, &ni)) != (char32_t)-1){ + if(input == 'q'){ + break; + } + switch(input){ + case NCKEY_LEFT: t.MoveLeft(); break; + case NCKEY_RIGHT: t.MoveRight(); break; + case 'z': t.RotateCcw(); break; + case 'x': t.RotateCw(); break; + default: + stdplane->cursor_move(0, 0); + stdplane->printf("Got unknown input U+%06x", input); + nc.render(); + break; + } + } + if(gameover || input == 'q'){ + gameover = true; + tid.join(); + }else{ + return EXIT_FAILURE; + } + return nc.stop() ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/tests/fills.cpp b/tests/fills.cpp index 945829be8..32c099394 100644 --- a/tests/fills.cpp +++ b/tests/fills.cpp @@ -17,6 +17,14 @@ TEST_CASE("Fills") { struct ncplane* n_ = notcurses_stdplane(nc_); REQUIRE(n_); + // can't polyfill with a null glyph + SUBCASE("PolyfillNullGlyph") { + int dimx, dimy; + ncplane_dim_yx(n_, &dimy, &dimx); + cell c = CELL_TRIVIAL_INITIALIZER; + CHECK(0 > ncplane_polyfill_yx(n_, dimy, dimx, &c)); + } + // trying to polyfill an invalid cell ought be an error SUBCASE("PolyfillOffplane") { int dimx, dimy; @@ -40,11 +48,17 @@ TEST_CASE("Fills") { CHECK(0 == ncplane_destroy(pfn)); } + SUBCASE("PolyfillStandardPlane") { + cell c = CELL_SIMPLE_INITIALIZER('-'); + CHECK(0 < ncplane_polyfill_yx(n_, 0, 0, &c)); + CHECK(0 == notcurses_render(nc_)); + } + SUBCASE("PolyfillEmptyPlane") { cell c = CELL_SIMPLE_INITIALIZER('+'); - struct ncplane* pfn = ncplane_new(nc_, 4, 4, 0, 0, nullptr); + struct ncplane* pfn = ncplane_new(nc_, 20, 20, 0, 0, nullptr); REQUIRE(nullptr != pfn); - CHECK(16 == ncplane_polyfill_yx(pfn, 0, 0, &c)); + CHECK(400 == ncplane_polyfill_yx(pfn, 0, 0, &c)); CHECK(0 == notcurses_render(nc_)); CHECK(0 == ncplane_destroy(pfn)); } @@ -311,7 +325,7 @@ TEST_CASE("Fills") { CHECK(0 == notcurses_render(nc_)); } - SUBCASE("MergeDown") { + SUBCASE("MergeDownASCII") { auto p1 = ncplane_new(nc_, 1, 10, 0, 0, nullptr); REQUIRE(p1); // make sure glyphs replace nulls @@ -345,6 +359,83 @@ TEST_CASE("Fills") { ncplane_destroy(p1); } + SUBCASE("MergeDownUni") { + auto p1 = ncplane_new(nc_, 1, 10, 0, 0, nullptr); + REQUIRE(p1); + // make sure glyphs replace nulls + CHECK(0 < ncplane_putstr(p1, "█▀▄▌▐🞵🞶🞷🞸🞹")); + CHECK(0 == ncplane_mergedown(p1, n_)); + cell cbase = CELL_TRIVIAL_INITIALIZER; + cell cp = CELL_TRIVIAL_INITIALIZER; + for(int i = 0 ; i < 10 ; ++i){ + CHECK(0 < ncplane_at_yx(n_, 0, i, &cbase)); + CHECK(0 < ncplane_at_yx(p1, 0, i, &cp)); + CHECK(0 == cellcmp(n_, &cbase, p1, &cp)); + } + ncplane_destroy(p1); + CHECK(0 == notcurses_render(nc_)); + auto p3 = ncplane_new(nc_, 1, 10, 0, 0, nullptr); + CHECK(0 == ncplane_cursor_move_yx(p3, 0, 0)); + // make sure glyphs replace glyps + CHECK(0 < ncplane_putstr(p3, "🞵🞶🞷🞸🞹█▀▄▌▐")); + CHECK(0 == ncplane_mergedown(p3, NULL)); + cell c3 = CELL_TRIVIAL_INITIALIZER; + for(int i = 0 ; i < 10 ; ++i){ + CHECK(0 < ncplane_at_yx(n_, 0, i, &cbase)); + CHECK(0 < ncplane_at_yx(p3, 0, i, &c3)); + CHECK(0 == cellcmp(n_, &cbase, p3, &c3)); + } + CHECK(0 == notcurses_render(nc_)); + // make sure nulls do not replace glyphs + auto p2 = ncplane_new(nc_, 1, 10, 0, 0, nullptr); + CHECK(0 == ncplane_mergedown(p2, NULL)); + ncplane_destroy(p2); + for(int i = 0 ; i < 10 ; ++i){ + CHECK(0 < ncplane_at_yx(n_, 0, i, &cbase)); + CHECK(0 < ncplane_at_yx(p3, 0, i, &c3)); + CHECK(0 == cellcmp(n_, &cbase, p3, &c3)); + } + ncplane_destroy(p3); + CHECK(0 == notcurses_render(nc_)); + } + + // test merging down one plane to another plane which is smaller than the + // standard plane + SUBCASE("MergeDownSmallPlane") { + constexpr int DIMX = 10; + constexpr int DIMY = 10; + auto p1 = ncplane_new(nc_, DIMY, DIMX, 2, 2, nullptr); + REQUIRE(p1); + cell c1 = CELL_TRIVIAL_INITIALIZER; + CHECK(0 < cell_load(p1, &c1, "█")); + CHECK(0 == cell_set_bg(&c1, 0x00ff00)); + CHECK(0 == cell_set_fg(&c1, 0x0000ff)); + ncplane_polyfill_yx(p1, 0, 0, &c1); + CHECK(0 == notcurses_render(nc_)); + auto p2 = ncplane_new(nc_, DIMY / 2, DIMX / 2, 3, 3, nullptr); + REQUIRE(p2); + cell c2 = CELL_TRIVIAL_INITIALIZER; + CHECK(0 < cell_load(p2, &c2, "🞶")); + CHECK(0 == cell_set_bg(&c2, 0x00ffff)); + CHECK(0 == cell_set_fg(&c2, 0xff00ff)); + ncplane_polyfill_yx(p2, 0, 0, &c2); + CHECK(0 == ncplane_mergedown(p2, p1)); + CHECK(0 == notcurses_render(nc_)); + for(int y = 0 ; y < DIMY ; ++y){ + for(int x = 0 ; x < DIMX ; ++x){ + CHECK(0 < ncplane_at_yx(p1, y, x, &c1)); + if(y < 1 || y > 5 || x < 1 || x > 5){ + CHECK(0 == strcmp(extended_gcluster(p1, &c1), "█")); + }else{ + CHECK(0 < ncplane_at_yx(p2, y - 1, x - 1, &c2)); + CHECK(0 == cellcmp(p1, &c1, p2, &c2)); + } + } + } + ncplane_destroy(p1); + ncplane_destroy(p2); + } + CHECK(0 == notcurses_stop(nc_)); CHECK(0 == fclose(outfp_));