Merge branch 'master' of github.com:dankamongmen/notcurses

This commit is contained in:
nick black 2020-03-24 01:17:28 -04:00
commit 4e7d918732
No known key found for this signature in database
GPG Key ID: 5F43400C21CBFACC
15 changed files with 593 additions and 55 deletions

View File

@ -448,6 +448,27 @@ target_compile_definitions(notcurses-ncreel
FORTIFY_SOURCE=2 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 # notcurses-view
file(GLOB VIEWSRCS CONFIGURE_DEPENDS src/view/*.cpp) file(GLOB VIEWSRCS CONFIGURE_DEPENDS src/view/*.cpp)
if(${USE_FFMPEG}) if(${USE_FFMPEG})
@ -682,6 +703,7 @@ install(TARGETS notcurses-demo DESTINATION bin)
install(TARGETS notcurses-input DESTINATION bin) install(TARGETS notcurses-input DESTINATION bin)
install(TARGETS notcurses-ncreel DESTINATION bin) install(TARGETS notcurses-ncreel DESTINATION bin)
install(TARGETS notcurses-tester DESTINATION bin) install(TARGETS notcurses-tester DESTINATION bin)
install(TARGETS notcurses-tetris DESTINATION bin)
if(${USE_FFMPEG}) if(${USE_FFMPEG})
install(TARGETS notcurses-view DESTINATION bin) install(TARGETS notcurses-view DESTINATION bin)
endif() endif()

BIN
data/tetris-background.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View File

@ -829,7 +829,7 @@ WARN_LOGFILE =
# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING # spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
# Note: If this tag is empty the current directory is searched. # 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 # 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 # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses

View File

@ -22,6 +22,7 @@
<a href="notcurses-ncreel.1.html">notcurses-ncreel</a>—experiments with ncreels<br/> <a href="notcurses-ncreel.1.html">notcurses-ncreel</a>—experiments with ncreels<br/>
<a href="notcurses-pydemo.1.html">notcurses-pydemo</a>—validates the Python wrappers<br/> <a href="notcurses-pydemo.1.html">notcurses-pydemo</a>—validates the Python wrappers<br/>
<a href="notcurses-tester.1.html">notcurses-tester</a>—unit test driver<br/> <a href="notcurses-tester.1.html">notcurses-tester</a>—unit test driver<br/>
<a href="notcurses-tetris.1.html">notcurses-tetris</a>—Tetris in the terminal<br/>
<a href="notcurses-view.1.html">notcurses-view</a>—renders images and video to the terminal<br/> <a href="notcurses-view.1.html">notcurses-view</a>—renders images and video to the terminal<br/>
<h2>C library (section 3)</h2> <h2>C library (section 3)</h2>
<a href="notcurses_cell.3.html">notcurses_cell</a>—operations on <tt>cell</tt> objects<br/> <a href="notcurses_cell.3.html">notcurses_cell</a>—operations on <tt>cell</tt> objects<br/>

View File

@ -0,0 +1,32 @@
% notcurses-tetris(1)
% nick black <nickblack@linux.com>
% 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)**

View File

@ -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 as rendering is not taking place, however, multiple threads may safely output
to multiple ncplanes. 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 **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 **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** of the rendering region. Only those cells where **src** intersects with **dst**

View File

@ -121,7 +121,7 @@ namespace ncpp
return cell_simple_p (&_cell); return cell_simple_p (&_cell);
} }
uint32_t get_edc_idx () const noexcept uint32_t get_egc_idx () const noexcept
{ {
return cell_egc_idx (&_cell); return cell_egc_idx (&_cell);
} }

View File

@ -118,6 +118,14 @@ namespace ncpp
return ncplane_pulse (plane, ts, fader, curry) != -1; 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 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; return ncplane_gradient (plane, egc, attrword, ul, ur, ll, lr, ystop, xstop) != -1;
@ -295,6 +303,20 @@ namespace ncpp
return move_above (*above); 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 bool cursor_move (int y, int x) const noexcept
{ {
return ncplane_cursor_move_yx (plane, y, x) != -1; 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 void translate (const Plane *dst, int *y = nullptr, int *x = nullptr) const
{ {
if (dst == nullptr) ncplane_translate(*this, dst ? dst->plane: nullptr, y, x);
throw invalid_argument ("'dst' must be a valid pointer");
translate (*this, *dst, y, x);
} }
void translate (const Plane &dst, int *y = nullptr, int *x = nullptr) noexcept void translate (const Plane &dst, int *y = nullptr, int *x = nullptr) noexcept
@ -875,10 +895,7 @@ namespace ncpp
if (src == nullptr) if (src == nullptr)
throw invalid_argument ("'src' must be a valid pointer"); throw invalid_argument ("'src' must be a valid pointer");
if (dst == nullptr) ncplane_translate(*src, dst ? dst->plane : nullptr, y, x);
throw invalid_argument ("'dst' must be a valid pointer");
translate (*src, *dst, y, x);
} }
static void translate (const Plane &src, const Plane &dst, int *y = nullptr, int *x = nullptr) noexcept static void translate (const Plane &src, const Plane &dst, int *y = nullptr, int *x = nullptr) noexcept

View File

@ -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 // 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' // 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, API void ncplane_translate(const struct ncplane* src, const struct ncplane* dst,
int* RESTRICT y, int* RESTRICT x); 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 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); API struct ncplane* ncplane_below(struct ncplane* n);
// Rotate the plane pi/2 radians clockwise or counterclockwise. Note that // 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' // 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 // 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 // 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); API int ncplane_mergedown(struct ncplane* RESTRICT src, struct ncplane* RESTRICT dst);
// Erase every cell in the ncplane, resetting all attributes to normal, all // 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, // 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 // 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. // its return value handled as outlined for stream cb. If streamer() returns
// If streamer() returns non-zero, the stream is aborted, and that value is // non-zero, the stream is aborted, and that value is returned. By convention,
// returned. By convention, return a positive number to indicate intentional // return a positive number to indicate intentional abort from within
// abort from within streamer(). 'timescale' allows the frame duration time to // streamer(). 'timescale' allows the frame duration time to be scaled. For a
// be scaled. For a visual naturally running at 30FPS, a 'timescale' of 0.1 // visual naturally running at 30FPS, a 'timescale' of 0.1 will result in
// will result in 300FPS, and a 'timescale' of 10 will result in 3FPS. It is an // 300FPS, and a 'timescale' of 10 will result in 3FPS. It is an error to
// error to supply 'timescale' less than or equal to 0. // supply 'timescale' less than or equal to 0.
API int ncvisual_stream(struct notcurses* nc, struct ncvisual* ncv, API int ncvisual_stream(struct notcurses* nc, struct ncvisual* ncv,
int* averr, float timescale, streamcb streamer, int* averr, float timescale, streamcb streamer,
void* curry); void* curry);

View File

@ -57,11 +57,13 @@ 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. // 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 ncplane_polyfill_yx(ncplane* n, int y, int x, const cell* c){
int ret = -1; int ret = -1;
if(c->gcluster){ // can't polyfill with a null EGC
if(y < n->leny && x < n->lenx){ if(y < n->leny && x < n->lenx){
if(y >= 0 && x >= 0){ if(y >= 0 && x >= 0){
ret = ncplane_polyfill_recurse(n, y, x, c); ret = ncplane_polyfill_recurse(n, y, x, c);
} }
} }
}
return ret; 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. // Ideally, rotation through 360 degrees will restore the original 2x1 squre.
// Unfortunately, the case where a half block occupies a cell having the same // 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 // addition, lower blocks eventually become upper blocks with their channels
// reversed. In general: // reversed. In general:
// //

View File

@ -26,7 +26,6 @@
#include <signal.h> #include <signal.h>
#include <wctype.h> #include <wctype.h>
#include <stdbool.h> #include <stdbool.h>
#include <pthread.h>
#include "notcurses/notcurses.h" #include "notcurses/notcurses.h"
#include "egcpool.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 // True if the cell does not generate background pixels. Only the FULL BLOCK
// glyph has this property, AFAIK. // glyph has this property, AFAIK.
// FIXME set a bit, doing this at load time // 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; return !!c->gcluster;
} }
size_t ulen = strlen(extended_gcluster(splane, c)); 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); int eoffset = egcpool_stash(tpool, extended_gcluster(splane, c), ulen);
if(eoffset < 0){ if(eoffset < 0){
return -1; return -1;

View File

@ -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, void ncplane_translate(const ncplane* src, const ncplane* dst,
int* restrict y, int* restrict x){ int* restrict y, int* restrict x){
if(dst == NULL){
dst = ncplane_stdplane_const(src);
}
if(y){ if(y){
*y = src->absy - dst->absy + *y; *y = src->absy - dst->absy + *y;
} }

View File

@ -203,19 +203,21 @@ lock_in_highcontrast(cell* targc, struct crender* crender){
} }
} }
// Paints a single ncplane into the provided framebuffer 'fb'. Whenever a cell // Paints a single ncplane into the provided scratch framebuffer 'fb', and
// is locked in, it is compared against the last frame. If it is different, the // ultimately 'lastframe' (we can't always write directly into 'lastframe',
// 'damagevec' bitmap is updated with a 1. 'pool' is typically nc->pool, but can // 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. // be whatever's backing fb.
static int 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; int y, x, dimy, dimx, offy, offx;
// don't use ncplane_dim_yx()/ncplane_yx() here, lest we deadlock ncplane_dim_yx(p, &dimy, &dimx);
dimy = p->leny; offy = p->absy - dstabsy;
dimx = p->lenx; offx = p->absx - dstabsx;
offy = p->absy - nc->stdscr->absy; //fprintf(stderr, "PLANE %p %d %d %d %d %d %d\n", p, dimy, dimx, offy, offx, dstleny, dstlenx);
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);
// skip content above or to the left of the physical screen // skip content above or to the left of the physical screen
int starty, startx; int starty, startx;
if(offy < 0){ 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){ for(y = starty ; y < dimy ; ++y){
const int absy = y + offy; const int absy = y + offy;
// once we've passed the physical screen's bottom, we're done // once we've passed the physical screen's bottom, we're done
if(absy >= nc->stdscr->leny){ if(absy >= dstleny){
break; break;
} }
for(x = startx ; x < dimx ; ++x){ for(x = startx ; x < dimx ; ++x){
const int absx = x + offx; const int absx = x + offx;
if(absx >= nc->stdscr->lenx){ if(absx >= dstlenx){
break; break;
} }
cell* targc = &fb[fbcellidx(absy, nc->stdscr->lenx, absx)]; cell* targc = &fb[fbcellidx(absy, dstlenx, absx)];
if(cell_locked_p(targc)){ if(cell_locked_p(targc)){
continue; 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)]; const cell* vis = &p->fb[nfbcellidx(p, y, x)];
// if we never loaded any content into the cell (or obliterated it by // if we never loaded any content into the cell (or obliterated it by
// writing in a zero), use the plane's base cell. // 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. // screen, nor if we're bisected by a higher plane.
if(cell_double_wide_p(vis)){ if(cell_double_wide_p(vis)){
// are we on the last column of the real screen? if so, 0x20 us // 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 = ' '; targc->gcluster = ' ';
// is the next cell occupied? if so, 0x20 us // is the next cell occupied? if so, 0x20 us
}else if(targc[1].gcluster){ }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)? // which were already locked in were skipped at the top of the loop)?
if(cell_locked_p(targc)){ if(cell_locked_p(targc)){
lock_in_highcontrast(targc, crender); 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)){ /*if(cell_simple_p(targc)){
fprintf(stderr, "WROTE %u [%c] to %d/%d (%d/%d)\n", targc->gcluster, prevcell->gcluster, y, x, absy, absx); fprintf(stderr, "WROTE %u [%c] to %d/%d (%d/%d)\n", targc->gcluster, prevcell->gcluster, y, x, absy, absx);
}else{ }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, "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)){ if(cellcmp_and_dupfar(pool, prevcell, crender->p, targc)){
crender->damaged = true; crender->damaged = true;
if(cell_double_wide_p(targc)){ 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. // paint within the real viewport currently.
int ncplane_mergedown(ncplane* restrict src, ncplane* restrict dst){ int ncplane_mergedown(ncplane* restrict src, ncplane* restrict dst){
notcurses* nc = src->nc; notcurses* nc = src->nc;
if(dst == NULL){
dst = nc->stdscr;
}
int dimy, dimx; int dimy, dimx;
ncplane_dim_yx(dst, &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; const size_t crenderlen = sizeof(struct crender) * dimy * dimx;
struct crender* rvec = malloc(crenderlen); struct crender* rvec = malloc(crenderlen);
memset(rvec, 0, crenderlen); memset(rvec, 0, crenderlen);
init_fb(fb, dimy, dimx); init_fb(tmpfb, dimy, dimx);
if(paint(nc, src, dst->fb, rvec, fb, &dst->pool) || paint(nc, dst, dst->fb, rvec, fb, &dst->pool)){ 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(rvec);
free(fb); free(rendfb);
free(tmpfb);
return -1; return -1;
} }
postpaint(fb, dst->fb, dimy, dimx, rvec, &dst->pool); if(paint(dst, rendfb, rvec, tmpfb, &dst->pool, dst->leny, dst->lenx,
dst->absy, dst->absx, dst->lenx)){
free(rvec);
free(rendfb);
free(tmpfb);
return -1;
}
postpaint(tmpfb, rendfb, dimy, dimx, rvec, &dst->pool);
free(dst->fb); free(dst->fb);
dst->fb = fb; dst->fb = rendfb;
free(rvec); free(rvec);
return 0; return 0;
} }
@ -417,7 +434,9 @@ notcurses_render_internal(notcurses* nc, struct crender* rvec){
init_fb(fb, dimy, dimx); init_fb(fb, dimy, dimx);
ncplane* p = nc->top; ncplane* p = nc->top;
while(p){ 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); free(fb);
return -1; return -1;
} }
@ -787,9 +806,9 @@ update_palette(notcurses* nc, FILE* out){
// * refresh -- write the stream to the emulator // * refresh -- write the stream to the emulator
// Takes a rendered frame (a flat framebuffer, where each cell has the desired // Takes a rendered frame (a flat framebuffer, where each cell has the desired
// EGC, attribute, and channels) and the previously-rendered frame, and spits // EGC, attribute, and channels), which has been written to nc->lastframe, and
// out an optimal sequence of terminal-appropriate escapes and EGCs. There // 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. // should be an rvec entry for each cell, but only the 'damaged' field is used.
static int static int
notcurses_rasterize(notcurses* nc, const struct crender* rvec){ notcurses_rasterize(notcurses* nc, const struct crender* rvec){
FILE* out = nc->rstate.mstreamfp; FILE* out = nc->rstate.mstreamfp;

330
src/tetris/main.cpp Normal file
View File

@ -0,0 +1,330 @@
#include <mutex>
#include <atomic>
#include <thread>
#include <chrono>
#include <cstdlib>
#include <clocale>
#include <ncpp/NotCurses.hh>
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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> lock(mtx_);
int y, x;
if(!PrepForMove(&y, &x)){
return;
}
// FIXME rotate that fucker ccw
}
void RotateCw() {
const std::lock_guard<std::mutex> 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<ncpp::Plane> curpiece_;
std::unique_ptr<ncpp::Plane> board_;
std::unique_ptr<ncpp::Visual> 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<ncpp::Visual>(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<ncpp::Plane>(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<ncpp::Plane> 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<ncpp::Plane> n = std::make_unique<ncpp::Plane>(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;
}

View File

@ -17,6 +17,14 @@ TEST_CASE("Fills") {
struct ncplane* n_ = notcurses_stdplane(nc_); struct ncplane* n_ = notcurses_stdplane(nc_);
REQUIRE(n_); 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 // trying to polyfill an invalid cell ought be an error
SUBCASE("PolyfillOffplane") { SUBCASE("PolyfillOffplane") {
int dimx, dimy; int dimx, dimy;
@ -40,11 +48,17 @@ TEST_CASE("Fills") {
CHECK(0 == ncplane_destroy(pfn)); 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") { SUBCASE("PolyfillEmptyPlane") {
cell c = CELL_SIMPLE_INITIALIZER('+'); 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); 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 == notcurses_render(nc_));
CHECK(0 == ncplane_destroy(pfn)); CHECK(0 == ncplane_destroy(pfn));
} }
@ -311,7 +325,7 @@ TEST_CASE("Fills") {
CHECK(0 == notcurses_render(nc_)); CHECK(0 == notcurses_render(nc_));
} }
SUBCASE("MergeDown") { SUBCASE("MergeDownASCII") {
auto p1 = ncplane_new(nc_, 1, 10, 0, 0, nullptr); auto p1 = ncplane_new(nc_, 1, 10, 0, 0, nullptr);
REQUIRE(p1); REQUIRE(p1);
// make sure glyphs replace nulls // make sure glyphs replace nulls
@ -345,6 +359,83 @@ TEST_CASE("Fills") {
ncplane_destroy(p1); 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 == notcurses_stop(nc_));
CHECK(0 == fclose(outfp_)); CHECK(0 == fclose(outfp_));