BlinkenArea - GitList
Repositories
Blog
Wiki
Blinker
Code
Commits
Branches
Tags
Search
Tree:
20c513c
Branches
Tags
master
Blinker
src
common
Tetris.cpp
tetris: one time step at game over
Stefan Schuermans
commited
20c513c
at 2019-07-15 19:08:34
Tetris.cpp
Blame
History
Raw
/* Blinker Copyright 2011-2019 Stefan Schuermans <stefan@blinkenarea.org> Copyleft GNU public license - http://www.gnu.org/copyleft/gpl.html a blinkenarea.org project */ #include <cmath> #include <stdlib.h> #include <string> #include <vector> #include <BlinkenLib/BlinkenFrame.h> #include "File.h" #include "Format.h" #include "FormatFile.h" #include "Game.h" #include "Mgrs.h" #include "Module.h" #include "NameFile.h" #include "OpConn.h" #include "OpConnIf.h" #include "OpReqIf.h" #include "OutStreamFile.h" #include "Tetris.h" #include "Time.h" #include "TimeCallee.h" #include "UIntFile.h" namespace Blinker { /** * @brief constructor * @param[in] name module name * @param[in] mgrs managers * @param[in] dirBase base directory */ Tetris::Tetris(const std::string &name, Mgrs &mgrs, const Directory &dirBase): Game(name, mgrs, dirBase), m_fileStoneColor(dirBase.getFile("stoneColor")), m_fileDelay(dirBase.getFile("delay")), m_fileDropDelay(dirBase.getFile("dropDelay")), m_fileBlinkDelay(dirBase.getFile("blinkDelay")), m_fileGameOverDelay(dirBase.getFile("gameOverDelay")), m_fileStartSound(dirBase.getFile("startSound")), m_fileRowCompleteSound(dirBase.getFile("rowCompleteSound")), m_fileGameOverSound(dirBase.getFile("gameOverSound")), m_stoneColor(), m_delay(c_delayDescr.default_), m_dropDelay(c_dropDelayDescr.default_), m_blinkDelay(c_blinkDelayDescr.default_), m_gameOverDelay(c_gameOverDelayDescr.default_), m_pConn(NULL), m_stone(-1), m_rot(-1), m_posX(-1), m_posY(-1), m_dropping(false), m_blinking(0), m_completed(0), m_gameOver(false), m_field(), m_rowsBlink() { // open operator connection interfaces for player m_mgrs.m_opMgr.open(m_name, this); } /// virtual destructor Tetris::~Tetris() { // close operator connection interface m_mgrs.m_opMgr.close(m_name); // close open operator connection if (m_pConn) { m_pConn->close(); m_pConn = NULL; } } /// check for update of configuration (derived game), return true on update bool Tetris::updateConfigGame() { bool ret = false; // color file was modified -> convert color, return true for update // cfg value file was updated -> read new cfg value, return true for update if (colorUpdate(m_fileStoneColor, m_stoneColor)) { ret = true; } if (valueUpdate(m_fileDelay, c_delayDescr, m_delay)) { ret = true; } if (valueUpdate(m_fileDropDelay, c_dropDelayDescr, m_dropDelay)) { ret = true; } if (valueUpdate(m_fileBlinkDelay, c_blinkDelayDescr, m_blinkDelay)) { ret = true; } if (valueUpdate(m_fileGameOverDelay, c_gameOverDelayDescr, m_gameOverDelay)) { ret = true; } // sound name file was modified -> re-read sound name, no other update needed soundUpdate(m_fileStartSound); soundUpdate(m_fileRowCompleteSound); soundUpdate(m_fileGameOverSound); return ret; } /** * @brief check if accepting new operator connection is possible * @param[in] name operator interface name * @return if accepting new connection is possible */ bool Tetris::acceptNewOpConn(const std::string &name) { (void)name; // accept player if no one there yet return ! m_pConn; } /** * @brief new operator connection * @param[in] name operator interface name * @param[in] pConn operator connection object * * The new connection may not yet be used for sending inside this callback. */ void Tetris::newOpConn(const std::string &name, OpConn *pConn) { (void)name; // player arrives and starts game if (! m_pConn) { m_pConn = pConn; requestOpConnSound(m_pConn, m_fileStartSound); activate(); } // close imcoming connection as soon as possible, nothing else happens else { requestOpConnClose(pConn); return; } } /** * @brief key command received on operator connection * @param[in] pConn operator connection object * @param[in] key key that was pressed */ void Tetris::opConnRecvKey(OpConn *pConn, char key) { // hash -> hang up if (key == '#') { opConnClose(pConn); pConn->close(); return; } // star -> inform player about game if (key == '*') { playOpConnSound(pConn, m_fileStartSound); return; } /** normal keys for controlling game, deactivated if dropping stone, rows blinking or end of game */ if (m_dropping || m_blinking > 0 || m_gameOver) { return; } // move left if (key == '4') { if (checkStoneFit(m_stone, m_rot, m_posY, m_posX - 1)) { wipeStone(m_stone, m_rot, m_posY, m_posX); m_posX -= 1; drawStone(m_stone, m_rot, m_posY, m_posX); // TODO: active color sendFrame(); } return; } // move right if (key == '6') { if (checkStoneFit(m_stone, m_rot, m_posY, m_posX + 1)) { wipeStone(m_stone, m_rot, m_posY, m_posX); m_posX += 1; drawStone(m_stone, m_rot, m_posY, m_posX); // TODO: active color sendFrame(); } return; } // rotate left if (key == '1') { int new_rot = m_rot - 1; if (new_rot < 0) { new_rot = c_rotCnt - 1; } if (checkStoneFit(m_stone, new_rot, m_posY, m_posX)) { wipeStone(m_stone, m_rot, m_posY, m_posX); m_rot = new_rot; drawStone(m_stone, m_rot, m_posY, m_posX); // TODO: active color sendFrame(); } return; } // rotate right if (key == '2' || key == '3') { int new_rot = m_rot + 1; if (new_rot >= c_rotCnt) { new_rot = 0; } if (checkStoneFit(m_stone, new_rot, m_posY, m_posX)) { wipeStone(m_stone, m_rot, m_posY, m_posX); m_rot = new_rot; drawStone(m_stone, m_rot, m_posY, m_posX); // TODO: active color sendFrame(); } return; } // drop stone if (key == '8') { m_dropping = true; planTimeStep(); // stone falls fater now -> update time callback return; } } /** * @brief play command received on operator connection * @param[in] pConn operator connection object * @param[in] sound name of sound to play */ void Tetris::opConnRecvPlay(OpConn *pConn, const std::string &sound) { (void)pConn; (void)sound; } /** * @brief operator connection is closed * @param[in] pConn operator connection object * * The connection may not be used for sending any more in this callback. */ void Tetris::opConnClose(OpConn *pConn) { // remove coperator connection from requests (if it was in) forgetOpConn(pConn); // player leaves -> deactivate game if (pConn == m_pConn) { m_pConn = NULL; deactivate(); } } /// re-initialize game (e.g. due to config change) void Tetris::reinitialize() { // convert colors color2data(m_fileStoneColor, m_stoneColor); // get values valueFromFile(m_fileDelay, c_delayDescr, m_delay); valueFromFile(m_fileDropDelay, c_dropDelayDescr, m_dropDelay); valueFromFile(m_fileBlinkDelay, c_blinkDelayDescr, m_blinkDelay); valueFromFile(m_fileGameOverDelay, c_gameOverDelayDescr, m_gameOverDelay); // initialize field: empty m_field.clear(); m_field.resize(m_height * m_width, -1); // initialize blinking rows: no row blinking m_rowsBlink.clear(); m_rowsBlink.resize(m_height, false); // no rows blinking or completed, game not over yet m_blinking = 0; m_completed = 0; m_gameOver = false; // start with new stone newStone(); // redraw image and send frame redraw(); // request first time step if needed planTimeStep(); } /// redraw current game image, expected to call sendFrame() at end void Tetris::redraw() { // draw background rectFill(0, m_height, 0, m_width, m_backgroundColor); // draw fixed pixels, respect blinking rows for (int y = 0, i = 0; y < m_height; ++y) { if (! (m_blinking & 1) || ! m_rowsBlink.at(y)) { for (int x = 0; x < m_width; ++x, ++i) { if (m_field.at(i) >= 0) { pixel(y, x, m_stoneColor); } } } } // draw current stone drawStone(m_stone, m_rot, m_posY, m_posX); // send updated image buffer as frame sendFrame(); } /// process next time step of game void Tetris::timeStep() { // count time at end of game if (m_gameOver) { timeGameOver(); // blinking of completed rows } else if (m_blinking > 0) { timeBlinkRows(); // falling stone } else { timeStone(); } // request next time step planTimeStep(); } /// count time at end of game void Tetris::timeGameOver() { // close operator connection if (m_pConn) { forgetOpConn(m_pConn); // remove from requests (if it was in) m_pConn->close(); m_pConn = NULL; } // deactivate game deactivate(); } /// blink completed rows void Tetris::timeBlinkRows() { // blink rows ++m_blinking; // end of blinking -> remove blinking rows, new stone if (m_blinking >= 8) { // remove blinking rows for (int b = 0; b < m_height; ++b) { if (m_rowsBlink.at(b)) { // move rows 0..b-1 one row down, i.e., to rows 1..b for (int y = b, i = b * m_width + m_width - 1; y > 0; --y) { for (int x = m_width - 1; x >= 0; --x, --i) { m_field.at(i) = m_field.at(i - m_width); } } // clear first row for (int x = 0; x < m_width; ++x) { m_field.at(x) = -1; } // row not blinking any more m_rowsBlink.at(b) = false; } } // blinking done m_blinking = 0; // new stone newStone(); } // redraw image and send frame redraw(); } /// falling stone void Tetris::timeStone() { // stone can move down by one pixel if (checkStoneFit(m_stone, m_rot, m_posY + 1, m_posX)) { // move stone down by one pixel wipeStone(m_stone, m_rot, m_posY, m_posX); m_posY += 1; drawStone(m_stone, m_rot, m_posY, m_posX); // TODO: active color } // stone cannot move down by one pixel else { // add stone permanently to field at current position freezeStone(m_stone, m_rot, m_posY, m_posX); drawStone(m_stone, m_rot, m_posY, m_posX); // TODO: frozen color // overflow of game field -> game over if (checkStoneOverflow(m_stone, m_rot, m_posY, m_posX)) { // unset stone m_stone = -1; // game over m_gameOver = true; playOpConnSound(m_pConn, m_fileGameOverSound); } // no overflow else { // no current stone any more m_stone = -1; // check for completed rows, (afterwards: new stone) checkComplete(); } } // send updated image buffer as frame sendFrame(); } /// check for completed rows to disappear (new stone afterwards) void Tetris::checkComplete() { // collect y coordinated of completed rows -> m_rowsBlink bool blink = false; for (int y = 0, i = 0; y < m_height; ++y) { bool complete = true; for (int x = 0; x < m_width; ++x, ++i) { if (m_field.at(i) < 0) { complete = false; } } m_rowsBlink.at(y) = complete; if (complete) { blink = true; } } // no completed rows -> new stone if (! blink) { newStone(); } // start blinking (start with rows visible, last bit == 0) else { m_blinking = 2; playOpConnSound(m_pConn, m_fileRowCompleteSound); } } /// set up a new stone void Tetris::newStone() { // random stone, random rotation m_stone = rand() % c_stoneCnt; m_rot = rand() % c_rotCnt; // postion: two pixels above top middle m_posX = (m_width - 1) / 2; m_posY = -2; // stone is not being dropped yet m_dropping = false; } /// set time for next time step of game - or unset if not needed void Tetris::planTimeStep() { // no time call needed if not active if (! isActive()) { unsetTimeStep(); return; } // compute interval based on game state int interval_ms = m_delay; int speedup = m_completed; if (m_gameOver) { interval_ms = m_gameOverDelay; speedup = 0; } else if (m_blinking > 0) { interval_ms = m_blinkDelay; } else if (m_dropping) { interval_ms = m_dropDelay; } float scale = 0.3f + 0.7f * expf(-0.3f * speedup); float interval = 1e-3f * interval_ms * scale; // request next time call Time stepTime; stepTime.fromFloatSec(interval); setTimeStep(Time::now() + stepTime); } /// get rotatation of stone from stone/rotation index (or NULL in invalid) Tetris::RotStone const * Tetris::getRotStone(int stone, int rot) { if (! checkLimitInt(stone, 0, c_stoneCnt -1) || ! checkLimitInt(rot, 0, c_rotCnt - 1)) { return NULL; // invalid stone or rotation } return &c_stones[stone].rot[rot]; } /// check if stone fits at position bool Tetris::checkStoneFit(int stone, int rot, int y, int x) const { // get rotation of stone RotStone const *rotStone = getRotStone(stone, rot); if (! rotStone) { return false; // invalid stone or rotation -> does not fit } // check pixels for (int p = 0; p < c_pixelCnt; ++p) { int py = y + rotStone->pixels[p].y; int px = x + rotStone->pixels[p].x; if (py > m_height - 1 || ! checkLimitInt(px, 0, m_width - 1)) { return false; // outside field (except at top) -> does not fit } if (py >= 0) { // do not check above top int pi = py * m_width + px; if (m_field.at(pi) >= 0) { return false; // occupixed pixel -> does not fit } } } // all checks passed -> stone fits return true; } /// check if stone overflow game field bool Tetris::checkStoneOverflow(int stone, int rot, int y, int x) const { // get rotation of stone RotStone const *rotStone = getRotStone(stone, rot); if (! rotStone) { return true; // invalid stone or rotation -> overflow } // check pixels for overflow for (int p = 0; p < c_pixelCnt; ++p) { int py = y + rotStone->pixels[p].y; int px = x + rotStone->pixels[p].x; if (! checkLimitInt(py, 0, m_height -1) || ! checkLimitInt(px, 0, m_width - 1)) { return true; // outside field (including top) -> overflow } } // all checks passed -> no overflow return false; } /// freeze stone to field at position void Tetris::freezeStone(int stone, int rot, int y, int x) { // get rotation of stone RotStone const *rotStone = getRotStone(stone, rot); if (! rotStone) { return; // invalid stone or rotation -> nothing to do } // add pixels to field for (int p = 0; p < c_pixelCnt; ++p) { int py = y + rotStone->pixels[p].y; int px = x + rotStone->pixels[p].x; if (checkLimitInt(py, 0, m_height - 1) && checkLimitInt(px, 0, m_width - 1)) { int pi = py * m_width + px; m_field.at(pi) = stone; // mark pixel in field with stone index } } } /// draw a stone to image buffer void Tetris::drawStone(int stone, int rot, int y, int x) { colorStone(stone, rot, y, x, m_stoneColor); } /// wipe a stone from image buffer (i.e. replace it with background color) void Tetris::wipeStone(int stone, int rot, int y, int x) { colorStone(stone, rot, y, x, m_backgroundColor); } /// set shape of stone to color in image buffer void Tetris::colorStone(int stone, int rot, int y, int x, ColorData const &color) { // get rotation of stone RotStone const *rotStone = getRotStone(stone, rot); if (! rotStone) { return; // invalid stone or rotation -> nothing to do } // color pixels for (int p = 0; p < c_pixelCnt; ++p) { pixel(y + rotStone->pixels[p].y, x + rotStone->pixels[p].x, color); } } /// stone data Tetris::Stone const Tetris::c_stones[7] = { // the I { { { { { -2, 0 }, { -1, 0 }, { 0, 0 }, { 1, 0 } } }, { { { 0, -2 }, { 0, -1 }, { 0, 0 }, { 0, 1 } } }, { { { -2, 0 }, { -1, 0 }, { 0, 0 }, { 1, 0 } } }, { { { 0, -2 }, { 0, -1 }, { 0, 0 }, { 0, 1 } } }, } }, // the L { { { { { 1, -1 }, { -1, 0 }, { 0, 0 }, { 1, 0 } } }, { { { 0, -1 }, { 0, 0 }, { 0, 1 }, { 1, 1 } } }, { { { -1, 0 }, { 0, 0 }, { 1, 0 }, { -1, 1 } } }, { { { -1, -1 }, { 0, -1 }, { 0, 0 }, { 0, 1 } } }, } }, // the J { { { { { -1, -1 }, { -1, 0 }, { 0, 0 }, { 1, 0 } } }, { { { 0, -1 }, { 1, -1 }, { 0, 0 }, { 0, 1 } } }, { { { -1, 0 }, { 0, 0 }, { 1, 0 }, { 1, 1 } } }, { { { 0, -1 }, { 0, 0 }, { -1, 1 }, { 0, 1 } } }, } }, // the T { { { { { 0, -1 }, { -1, 0 }, { 0, 0 }, { 1, 0 } } }, { { { 0, -1 }, { 0, 0 }, { 1, 0 }, { 0, 1 } } }, { { { -1, 0 }, { 0, 0 }, { 1, 0 }, { 0, 1 } } }, { { { 0, -1 }, { -1, 0 }, { 0, 0 }, { 0, 1 } } }, } }, // the O { { { { { 0, -1 }, { 1, -1 }, { 0, 0 }, { 1, 0 } } }, { { { 0, -1 }, { 1, -1 }, { 0, 0 }, { 1, 0 } } }, { { { 0, -1 }, { 1, -1 }, { 0, 0 }, { 1, 0 } } }, { { { 0, -1 }, { 1, -1 }, { 0, 0 }, { 1, 0 } } }, } }, // the Z { { { { { -1, -1 }, { 0, -1 }, { 0, 0 }, { 1, 0 } } }, { { { 0, -1 }, { -1, 0 }, { 0, 0 }, { -1, 1 } } }, { { { -1, -1 }, { 0, -1 }, { 0, 0 }, { 1, 0 } } }, { { { 0, -1 }, { -1, 0 }, { 0, 0 }, { -1, 1 } } }, } }, // the S { { { { { 0, -1 }, { 1, -1 }, { -1, 0 }, { 0, 0 } } }, { { { -1, -1 }, { -1, 0 }, { 0, 0 }, { 0, 1 } } }, { { { 0, -1 }, { 1, -1 }, { -1, 0 }, { 0, 0 } } }, { { { -1, -1 }, { -1, 0 }, { 0, 0 }, { 0, 1 } } }, } }, }; /// number of stones int const Tetris::c_stoneCnt = sizeof(Tetris::c_stones) / sizeof(Tetris::c_stones[0]); /// number of rotations per stone int const Tetris::c_rotCnt = sizeof(Tetris::Stone::rot) / sizeof(Tetris::Stone::rot[0]); /// number of pixels per stone int const Tetris::c_pixelCnt = sizeof(Tetris::RotStone::pixels) / sizeof(Tetris::RotStone::pixels[0]); /// descriptor for delay value Tetris::ValueDescr const Tetris::c_delayDescr = { 400, 200, 1000 }; /// descriptor for delay value during dropping a stone Tetris::ValueDescr const Tetris::c_dropDelayDescr = { 100, 50, 250 }; /// descriptor for delay value during blinking of disappearing rows Tetris::ValueDescr const Tetris::c_blinkDelayDescr = { 50, 50, 250 }; /// descriptor for delay value at end of game Tetris::ValueDescr const Tetris::c_gameOverDelayDescr = { 2000, 100, 5000 }; } // namespace Blinker