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;
+}