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
This commit is contained in:
Nick Black 2020-03-22 17:33:08 -04:00 committed by GitHub
parent fbd222b04a
commit 52bdbc6724
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 271 additions and 11 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()

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;
@ -860,9 +868,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 +881,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);

View File

@ -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. // 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"

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

@ -382,6 +382,9 @@ 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* fb = malloc(sizeof(*fb) * dimy * dimx);

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

@ -0,0 +1,189 @@
#include <iostream>
#include <mutex>
#include <thread>
#include <chrono>
#include <cstdlib>
#include <clocale>
#include <ncpp/NotCurses.hh>
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<ncpp::Plane> 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<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 - 3);
const int yoff = y - (BOARD_HEIGHT + 4);
std::unique_ptr<ncpp::Plane> n = std::make_unique<ncpp::Plane>(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;
}