More tetris work for book #421 (#423)

* tetris: use NES gravities
* tetris: use NES grav multiplier of 50ms
* tetris: implement move down #421
* README: mention notcurses-tetris #421
* tetris: use double box for boundary #421
* tetris: extract background.h
* tetris: break up into chunks suitable for book
* tetris: do the rotations
pull/424/head
Nick Black 4 years ago committed by GitHub
parent ae1421db15
commit de9158bd7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,13 @@
This document attempts to list user-visible changes and any major internal
rearrangements of Notcurse.
* 1.2.5 (not yet released)
** `ncvisual_render()` now returns the number of cells emitted on success, as
opposed to 0. Failure still sees -1 returned.
** `ncvisual_render()` now interprets length parameters of -1 to mean "to the
end along this axis", and no longer interprets 0 to mean this. 0 now means
"a length of 0", resulting in a zero-area rendering.
* 1.2.4 2020-03-24
** Add ncmultiselector
** Add `ncdirect_cursor_enable()` and `ncdirect_cursor_disable()`.

@ -2398,12 +2398,13 @@ channels_set_bg_default(uint64_t* channels){
## Included tools
Five binaries are built as part of notcurses:
Six binaries are installed as part of notcurses:
* `notcurses-demo`: some demonstration code
* `notcurses-view`: renders visual media (images/videos)
* `notcurses-input`: decode and print keypresses
* `notcurses-planereels`: play around with ncreels
* `notcurses-tester`: unit testing
* `notcurses-tetris`: a tetris clone
To run `notcurses-demo` from a checkout, provide the `tests/` directory via
the `-p` argument. Demos requiring data files will otherwise abort. The base

@ -653,6 +653,8 @@ ncplane_putegc(struct ncplane* n, const char* gclust, int* sbytes){
// of the plane will not be changed.
API int ncplane_putegc_stainable(struct ncplane* n, const char* gclust, int* sbytes);
// 0x0--0x10ffff can be UTF-8-encoded with only 4 bytes...but we aren't
// yet actively guarding against higher values getting into wcstombs FIXME
#define WCHAR_MAX_UTF8BYTES 6
// ncplane_putegc(), but following a conversion from wchar_t to UTF-8 multibyte.

@ -0,0 +1,36 @@
// 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, -1, -1) <= 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_->double_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()");
}
}

@ -0,0 +1,17 @@
// the number of milliseconds before a drop is forced at the given level,
// using the NES fps counter of 50ms
static constexpr int Gravity(int level) {
constexpr int MS_PER_GRAV = 30; // 10MHz*63/88/455/525 (~29.97fps) in NTSC
// The number of frames before a drop is forced, per level
constexpr std::array<int, 30> Gravities = {
48, 43, 38, 33, 28, 23, 18, 13, 8, 6, 5, 5, 5,
4, 4, 4, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1
};
if(level < 0){
throw std::out_of_range("Illegal level");
}
if(static_cast<unsigned long>(level) < Gravities.size()){
return Gravities[level] * MS_PER_GRAV;
}
return MS_PER_GRAV; // all levels 29+ are a single grav
}

@ -10,18 +10,6 @@ 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()
@ -40,154 +28,41 @@ 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)
gameover_(gameover),
level_(0),
msdelay_(Gravity(level_))
{
DrawBoard();
curpiece_ = NewPiece();
}
// 0.5 cell aspect: One board height == one row. One board width == two columns.
// 0.5 cell aspect: 1 board height == one row. 1 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
}
#include "gravity.h"
#include "ticker.h"
#include "movedown.h"
#include "moveleft.h"
#include "moveright.h"
#include "rotate.h"
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_;
int level_;
std::chrono::milliseconds msdelay_;
// Returns true if there's a current piece which can be moved
bool PrepForMove(int* y, int* x) {
@ -198,97 +73,9 @@ private:
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, -1, -1)){
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;
}
#include "background.h"
#include "stuck.h"
#include "newpiece.h"
};
@ -296,6 +83,7 @@ int main(void) {
if(setlocale(LC_ALL, "") == nullptr){
return EXIT_FAILURE;
}
srand(time(NULL));
std::atomic_bool gameover = false;
notcurses_options ncopts{};
ncpp::NotCurses nc(ncopts);
@ -309,8 +97,9 @@ int main(void) {
break;
}
switch(input){
case NCKEY_LEFT: t.MoveLeft(); break;
case NCKEY_RIGHT: t.MoveRight(); break;
case NCKEY_LEFT: case 'h': t.MoveLeft(); break;
case NCKEY_RIGHT: case 'l': t.MoveRight(); break;
case NCKEY_DOWN: case 'j': t.MoveDown(); break;
case 'z': t.RotateCcw(); break;
case 'x': t.RotateCw(); break;
default:
@ -320,7 +109,7 @@ int main(void) {
break;
}
}
if(gameover || input == 'q'){
if(gameover || input == 'q'){ // FIXME signal it on 'q'
gameover = true;
tid.join();
}else{

@ -0,0 +1,20 @@
// returns true if the game has ended as a result of this move down
bool MoveDown() {
const std::lock_guard<std::mutex> lock(mtx_);
int y, x;
if(PrepForMove(&y, &x)){
if(PieceStuck()){
if(y <= board_top_y_ - 2){
return true;
}
curpiece_->mergedown(*board_);
curpiece_ = NewPiece();
}else{
++y;
if(!curpiece_->move(y, x) || !nc_.render()){
throw TetrisNotcursesErr("move() or render()");
}
}
}
return false;
}

@ -0,0 +1,36 @@
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()");
}
}

@ -0,0 +1,36 @@
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()");
}
}

@ -0,0 +1,42 @@
// 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() {
// "North-facing" tetrimino forms (form in which they are released from the
// top) are expressed in terms of two rows having between 2 and 4 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, "** **"} };
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_ - 1, 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;
x = 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;
}

@ -0,0 +1,21 @@
void RotateCcw() {
const std::lock_guard<std::mutex> lock(mtx_);
int y, x;
if(!PrepForMove(&y, &x)){
return;
}
if(!curpiece_->rotate_ccw() || !nc_.render()){
throw TetrisNotcursesErr("rotate_ccw() or render()");
}
}
void RotateCw() {
const std::lock_guard<std::mutex> lock(mtx_);
int y, x;
if(!PrepForMove(&y, &x)){
return;
}
if(!curpiece_->rotate_cw() || !nc_.render()){
throw TetrisNotcursesErr("rotate_cw() or render()");
}
}

@ -0,0 +1,21 @@
bool PieceStuck() {
if(curpiece_){
// 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);
while(x--){
int cmpy = y, 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;
}

@ -0,0 +1,13 @@
void Ticker() { // FIXME ideally this would be called from constructor :/
std::chrono::milliseconds ms;
mtx_.lock();
do{
ms = msdelay_;
mtx_.unlock();
std::this_thread::sleep_for(ms);
if(MoveDown()){
gameover_ = true;
return;
}
}while(!gameover_);
}
Loading…
Cancel
Save