|
|
|
@ -1,11 +1,13 @@
|
|
|
|
|
#include <iostream>
|
|
|
|
|
#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
|
|
|
|
@ -22,26 +24,31 @@ static const struct tetrimino {
|
|
|
|
|
|
|
|
|
|
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(){
|
|
|
|
|
virtual char const* what() const throw() {
|
|
|
|
|
return exception::what();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class Tetris {
|
|
|
|
|
public:
|
|
|
|
|
Tetris(ncpp::NotCurses& nc) :
|
|
|
|
|
Tetris(ncpp::NotCurses& nc, std::atomic_bool& gameover) :
|
|
|
|
|
nc_(nc),
|
|
|
|
|
score_(0),
|
|
|
|
|
msdelay_(10ms),
|
|
|
|
|
msdelay_(100ms),
|
|
|
|
|
curpiece_(nullptr),
|
|
|
|
|
stdplane_(nc_.get_stdplane())
|
|
|
|
|
board_(nullptr),
|
|
|
|
|
backg_(nullptr),
|
|
|
|
|
stdplane_(nc_.get_stdplane()),
|
|
|
|
|
gameover_(gameover)
|
|
|
|
|
{
|
|
|
|
|
curpiece_ = NewPiece();
|
|
|
|
|
DrawBoard();
|
|
|
|
|
curpiece_ = NewPiece();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 0.5 cell aspect: One board height == one row. One board width == two columns.
|
|
|
|
@ -49,7 +56,7 @@ public:
|
|
|
|
|
static constexpr auto BOARD_HEIGHT = 20;
|
|
|
|
|
|
|
|
|
|
// FIXME ideally this would be called from constructor :/
|
|
|
|
|
void Ticker(){
|
|
|
|
|
void Ticker() {
|
|
|
|
|
std::chrono::milliseconds ms;
|
|
|
|
|
mtx_.lock();
|
|
|
|
|
do{
|
|
|
|
@ -57,26 +64,117 @@ public:
|
|
|
|
|
// FIXME loop and verify we didn't get a spurious wakeup
|
|
|
|
|
mtx_.unlock();
|
|
|
|
|
std::this_thread::sleep_for(ms);
|
|
|
|
|
mtx_.lock();
|
|
|
|
|
const std::lock_guard<std::mutex> lock(mtx_);
|
|
|
|
|
if(curpiece_){
|
|
|
|
|
int y, x;
|
|
|
|
|
curpiece_->get_yx(&y, &x);
|
|
|
|
|
++y;
|
|
|
|
|
if(PieceStuck()){
|
|
|
|
|
// FIXME lock it into place, get next piece
|
|
|
|
|
if(y <= board_top_y_ - 2){
|
|
|
|
|
gameover_ = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
curpiece_->mergedown(*board_);
|
|
|
|
|
curpiece_ = NewPiece();
|
|
|
|
|
}else{
|
|
|
|
|
++y;
|
|
|
|
|
if(!curpiece_->move(y, x) || !nc_.render()){
|
|
|
|
|
// FIXME
|
|
|
|
|
throw TetrisNotcursesErr("move() or render()");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}while(ms != std::chrono::milliseconds::zero());
|
|
|
|
|
}while(!gameover_);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Stop(){
|
|
|
|
|
mtx_.lock();
|
|
|
|
|
msdelay_ = std::chrono::milliseconds::zero(); // FIXME wake it up?
|
|
|
|
|
mtx_.unlock();
|
|
|
|
|
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:
|
|
|
|
@ -85,25 +183,60 @@ private:
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void DrawBoard(){
|
|
|
|
|
// 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);
|
|
|
|
|
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)){
|
|
|
|
|
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(){
|
|
|
|
|
bool PieceStuck() {
|
|
|
|
|
if(!curpiece_){
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
@ -114,14 +247,13 @@ private:
|
|
|
|
|
curpiece_->get_dim(&y, &x);
|
|
|
|
|
--y;
|
|
|
|
|
while(x--){
|
|
|
|
|
int cmpy = y + 1, cmpx = x; // need absolute coordinates via translation
|
|
|
|
|
curpiece_->translate(nullptr, &cmpy, &cmpx);
|
|
|
|
|
int cmpy = y + 1, cmpx = x; // need game area coordinates via translation
|
|
|
|
|
curpiece_->translate(*board_, &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(board_->get_at(cmpy, cmpx, &c) < 0){
|
|
|
|
|
throw TetrisNotcursesErr("get_at()");
|
|
|
|
|
}
|
|
|
|
|
if(*egc && *egc != ' '){
|
|
|
|
|
if(c.get().gcluster && c.get().gcluster != ' '){
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -130,20 +262,20 @@ private:
|
|
|
|
|
|
|
|
|
|
// 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(){
|
|
|
|
|
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);
|
|
|
|
|
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){
|
|
|
|
@ -160,27 +292,36 @@ private:
|
|
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
int main(void){
|
|
|
|
|
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};
|
|
|
|
|
Tetris t{nc, gameover};
|
|
|
|
|
std::thread tid(&Tetris::Ticker, &t);
|
|
|
|
|
ncpp::Plane* stdplane = nc.get_stdplane();
|
|
|
|
|
char32_t input;
|
|
|
|
|
ncinput ni;
|
|
|
|
|
while((input = nc.getc(true, &ni)) != (char32_t)-1){
|
|
|
|
|
while(!gameover && (input = nc.getc(true, &ni)) != (char32_t)-1){
|
|
|
|
|
if(input == 'q'){
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
switch(input){
|
|
|
|
|
case NCKEY_LEFT: break;
|
|
|
|
|
case NCKEY_RIGHT: break;
|
|
|
|
|
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(input == 'q'){
|
|
|
|
|
t.Stop();
|
|
|
|
|
if(gameover || input == 'q'){
|
|
|
|
|
gameover = true;
|
|
|
|
|
tid.join();
|
|
|
|
|
}else{
|
|
|
|
|
return EXIT_FAILURE;
|
|
|
|
|