BlinkenArea - GitList
Repositories
Blog
Wiki
Blinker
Code
Commits
Branches
Tags
Search
Tree:
8c6f3b8
Branches
Tags
master
Blinker
src
common
Pong.cpp
pong: configurable speed
Stefan Schuermans
commited
8c6f3b8
at 2019-06-16 11:28:22
Pong.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 "OpConn.h" #include "OpConnIf.h" #include "OpReqIf.h" #include "OutStreamFile.h" #include "Pong.h" #include "Time.h" #include "TimeCallee.h" #include "UIntFile.h" namespace Blinker { /// descriptor for delay value Pong::ValueDescr const Pong::c_delayDescr = { 200, 100, 500 }; /// operator connection name suffix for left player's connection std::string const Pong::c_opConnSuffixLeft = "/left"; /// operator connection name suffix for right player's connection std::string const Pong::c_opConnSuffixRight = "/right"; /** * @brief constructor * @param[in] name module name * @param[in] mgrs managers * @param[in] dirBase base directory */ Pong::Pong(const std::string &name, Mgrs &mgrs, const Directory &dirBase): Game(name, mgrs, dirBase), m_fileBallColor(dirBase.getFile("ballColor")), m_fileLineColor(dirBase.getFile("lineColor")), m_filePadColor(dirBase.getFile("padColor")), m_fileComputerColor(dirBase.getFile("computerColor")), m_fileScoreColor(dirBase.getFile("scoreColor")), m_fileGoalColor(dirBase.getFile("goalColor")), m_fileDelay(dirBase.getFile("delay")), m_ballColor(), m_lineColor(), m_padColor(), m_computerColor(), m_scoreColor(), m_goalColor(), m_delay(c_delayDescr.default_), m_pConnLeft(NULL), m_pConnRight(NULL), m_ballPosX(-1), m_ballPosY(-1), m_ballDirX(0), m_ballDirY(0), m_padSize(0), m_leftPosY(0), m_rightPosY(0), m_leftDelay(0), m_rightDelay(0), m_goalDelay(0), m_bounceCnt(0), m_scoreLeft(0), m_scoreRight(0) { // open operator connection interfaces for left and right player m_mgrs.m_opMgr.open(m_name + c_opConnSuffixLeft, this); m_mgrs.m_opMgr.open(m_name + c_opConnSuffixRight, this); } /// virtual destructor Pong::~Pong() { // close operator connection interfaces m_mgrs.m_opMgr.close(m_name + c_opConnSuffixLeft); m_mgrs.m_opMgr.close(m_name + c_opConnSuffixRight); // close open operator connections if (m_pConnLeft) { m_pConnLeft->close(); m_pConnLeft = NULL; } if (m_pConnRight) { m_pConnRight->close(); m_pConnRight = NULL; } // cancel time callback request m_mgrs.m_callMgr.cancelTimeCall(this); } /// check for update of configuration (derived game), return true on update bool Pong::updateConfigGame() { bool ret = false; // color file was modified -> convert color, return true for update if (colorUpdate(m_fileBallColor, m_ballColor) || colorUpdate(m_fileLineColor, m_lineColor) || colorUpdate(m_filePadColor, m_padColor) || colorUpdate(m_fileComputerColor, m_computerColor) || colorUpdate(m_fileScoreColor, m_scoreColor) || colorUpdate(m_fileGoalColor, m_goalColor) || valueUpdate(m_fileDelay, c_delayDescr, m_delay)) { ret = true; } return ret; } /** * @brief check if accepting new operator connction is possible * @param[in] name operator interface name * @return if accepting new connection is possible */ bool Pong::acceptNewOpConn(const std::string &name) { // left player can join if none there yet if (name == m_name + c_opConnSuffixLeft && ! m_pConnLeft) { return true; } // right player can join if none there yet if (name == m_name + c_opConnSuffixRight && ! m_pConnRight) { return true; } // default: reject connection return false; } /** * @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 Pong::newOpConn(const std::string &name, OpConn *pConn) { // left player joins if none there yet if (name == m_name + c_opConnSuffixLeft && ! m_pConnLeft) { m_pConnLeft = pConn; } // right player joins if none there yet else if (name == m_name + c_opConnSuffixRight && ! m_pConnRight) { m_pConnRight = pConn; } // nothing happens else { return; } // already active if (isActive()) { redraw(); // player color is different for phone / computer } // first player joined else { activate(); } } /** * @brief key command received on operator connection * @param[in] pConn operator connection object * @param[in] key key that was pressed */ void Pong::opConnRecvKey(OpConn *pConn, char key) { // left player if (pConn == m_pConnLeft) { processKey(key, m_leftPosY); } // right player else if (pConn == m_pConnRight) { processKey(key, m_rightPosY); } } /** * @brief play command received on operator connection * @param[in] pConn operator connection object * @param[in] sound name of sound to play */ void Pong::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 Pong::opConnClose(OpConn *pConn) { // left player leaves if (pConn == m_pConnLeft) { m_pConnLeft = NULL; } // right player leaves else if (pConn == m_pConnRight) { m_pConnRight = NULL; } // nothing happens else { return; } // still a phone player there if (m_pConnLeft || m_pConnRight) { redraw(); // player color is different for phone / computer } // no phone players left else { deactivate(); } } /// re-initialize game (e.g. due to config change) void Pong::reinitialize() { // compute parameters m_padSize = (m_height + 1) / 3; m_leftPosY = (m_height - m_padSize) / 2; m_rightPosY = (m_height - m_padSize) / 2; m_leftDelay = 0; m_rightDelay = 0; // convert colors color2data(m_fileBallColor, m_ballColor); color2data(m_fileLineColor, m_lineColor); color2data(m_filePadColor, m_padColor); color2data(m_fileComputerColor, m_computerColor); color2data(m_fileScoreColor, m_scoreColor); color2data(m_fileGoalColor, m_goalColor); // get values valueFromFile(m_fileDelay, c_delayDescr, m_delay); // reset scores m_scoreLeft = 0; m_scoreRight = 0; // start first ball startBall(); } /// redraw current game image, expected to call sendFrame() at end void Pong::redraw() { int y, x; // draw background rectFill(0, m_height, 0, m_width, m_backgroundColor); // draw middle line: single line on odd width, two dashed lines at even width for (y = 0; y < m_height; ++y) { x = (m_width - (y & 1)) / 2; pixel(y, x, m_lineColor); } // draw score ColorData const &scoreColor = m_goalDelay <= 0 ? m_scoreColor : m_goalColor; y = (m_height - 1) / 2; x = m_width / 4; number3x5(y, x, 0, 0, m_scoreLeft, scoreColor); number3x5(y, m_width - 1 - x, 0, 0, m_scoreRight, scoreColor); // draw pads lineVert(m_leftPosY, m_leftPosY + m_padSize - 1, 0, m_pConnLeft ? m_padColor : m_computerColor); lineVert(m_rightPosY, m_rightPosY + m_padSize - 1, m_width - 1, m_pConnRight ? m_padColor : m_computerColor); // draw ball (blinking if goal delay is active) if (m_goalDelay <= 0 || (m_goalDelay & 4) == 0) { pixel(m_ballPosY, m_ballPosX, m_ballColor); } // send updated image buffer as frame sendFrame(); } /// callback when requested time reached void Pong::timeCall() { // game is running if (m_goalDelay <= 0) { // computer player: move pads if (! m_pConnLeft) { computerLeft(); } if (! m_pConnRight) { computerRight(); } // bounce ball bounceBall(); // move ball m_ballPosX += m_ballDirX; m_ballPosY += m_ballDirY; // detect goal detectGoal(); } // goal delay (blinking ball) else if (m_goalDelay > 0) { --m_goalDelay; // new ball when delay is over if (m_goalDelay <= 0) { startBall(); return; // startBall calls redraw() and planTimeCall() } } // draw and send frame redraw(); // request next call if needed planTimeCall(); } /** * @brief process key received from phone player * @param[in] key received key from player * @param[in,out] padPosY y position of player's pad */ void Pong::processKey(char key, int &padPosY) { // do not move pad if goal delay is active if (m_goalDelay > 0) { return; } // move pad (2 = up, 8 = down), do not move pad out of field if (key == '2' && padPosY > 0) { --padPosY; redraw(); } else if (key == '8' && padPosY < m_height - m_padSize) { ++padPosY; redraw(); } } /** * @brief delay processing for computer players * @param[in,out] delay delay variable of computer player * @return whether computer player is allowed to move */ bool Pong::computerDelay(int &delay) const { // zero delay: generate new delay if (delay <= 0) { int avg_steps = (m_height - m_padSize) / 2; int delay_range = avg_steps > 1 ? m_width / avg_steps: m_width; delay = rand() % delay_range + delay_range; } // count down delay --delay; // moving allowd if delay expired return delay <= 0; } /** * @brief computation of ideal pad position for computer players * @param[in] padBallX x coordinate of position of ball when hitting the pad * @param[in] padY current y coordinate of pad * @param[out] padYmin minimum ideal y position of pad * @param[out] padYmax maximum ideal y position of pad */ void Pong::computerComputePadPos(int padBallX, int padY, int &padYmin, int &padYmax) const { // ball not moving towards pad if ((padBallX - m_ballPosX) * m_ballDirX <= 0) { // do not move if ball is still close to pad if (abs(padBallX - m_ballPosX) <= 2) { padYmin = padY; padYmax = padY; return; } // move pad to middle (might be 2 pixels wide) padYmin = (m_height - m_padSize) / 2; padYmax = (m_height - m_padSize + 1) / 2; return; } // compute expected ball position at pad int ballPosX = m_ballPosX; // simulate were ball is going int ballPosY = m_ballPosY; int ballDirY = m_ballDirY; while ((padBallX - ballPosX) * m_ballDirX > 0) { // while moving to pad int deltaX = padBallX - ballPosX; int deltaY = deltaX * m_ballDirX * ballDirY; if (deltaY < -ballPosY) { deltaY = -ballPosY; ballPosX += deltaY * m_ballDirX * ballDirY; ballPosY = 0; ballDirY = 1; } else if (deltaY > m_height - 1 - ballPosY) { deltaY = m_height - 1 - ballPosY; ballPosX += deltaY * m_ballDirX * ballDirY; ballPosY = m_height - 1; ballDirY = -1; } else { ballPosX += deltaX; ballPosY += deltaY; } } // compute pad position to hit ball with center of pad padYmin = ballPosY - m_padSize / 2; padYmax = ballPosY - (m_padSize - 1) / 2; } /** * @brief move pad for computer players * @param[in] padYmin minimum desired y position of pad * @param[in] padYmax maximum desired y position of pad * @param[in,out] padPosY y position of pad */ void Pong::computerMovePad(int padYmin, int padYmax, int &padPosY) const { // do not move pad if goal delay is active if (m_goalDelay > 0) { return; } // move pad, do not move pad out of field if (padPosY > padYmax && padPosY > 0) { --padPosY; } else if (padPosY < padYmin && padPosY < m_height - m_padSize) { ++padPosY; } } /// computer player for left pad void Pong::computerLeft() { if (computerDelay(m_leftDelay)) { int padYmin, padYmax; computerComputePadPos(1, m_leftPosY, padYmin, padYmax); computerMovePad(padYmin, padYmax, m_leftPosY); } } /// computer player for right pad void Pong::computerRight() { if (computerDelay(m_rightDelay)) { int padYmin, padYmax; computerComputePadPos(m_width - 2, m_rightPosY, padYmin, padYmax); computerMovePad(padYmin, padYmax, m_rightPosY); } } /// bounce ball void Pong::bounceBall() { bounceBallSide(); // must be done before player bounce to be safe bounceBallLeft(); bounceBallRight(); bounceBallSide(); // must also be done after player bounce to be safe } /// bounce ball at sides void Pong::bounceBallSide() { if (m_ballPosY <= 0 && m_ballDirY < 0) { m_ballDirY = 1; } if (m_ballPosY >= m_height - 1 && m_ballDirY > 0) { m_ballDirY = -1; } if (m_ballPosX <= 0 && m_ballDirX < 0) { m_ballDirX = 1; } if (m_ballPosX >= m_width - 1 && m_ballDirX > 0) { m_ballDirX = -1; } } /// bounce ball at left pad void Pong::bounceBallLeft() { if (m_ballPosX != 1 || m_ballDirX >= 0) { return; } // top corner if (m_ballPosY == m_leftPosY - 1 && m_ballDirY > 0) { m_ballDirX = 1; m_ballDirY = -1; ++m_bounceCnt; } // bottom corner else if (m_ballPosY == m_leftPosY + m_padSize && m_ballDirY < 0) { m_ballDirX = 1; m_ballDirY = 1; ++m_bounceCnt; } // pad edge else if (m_ballPosY >= m_leftPosY && m_ballPosY < m_leftPosY + m_padSize) { m_ballDirX = 1; ++m_bounceCnt; } } /// bounce ball at right pad void Pong::bounceBallRight() { if (m_ballPosX != m_width - 2 || m_ballDirX <= 0) { return; } // top corner if (m_ballPosY == m_rightPosY - 1 && m_ballDirY > 0) { m_ballDirX = -1; m_ballDirY = -1; ++m_bounceCnt; } // bottom corner else if (m_ballPosY == m_rightPosY + m_padSize && m_ballDirY < 0) { m_ballDirX = -1; m_ballDirY = 1; ++m_bounceCnt; } // pad edge else if (m_ballPosY >= m_rightPosY && m_ballPosY < m_rightPosY + m_padSize) { m_ballDirX = -1; ++m_bounceCnt; } } /// detect goal void Pong::detectGoal() { static int goalBlinkCnt = 3; static int goalDelay = goalBlinkCnt * 8 + 3; // ball at far left - goal for right player if (m_ballPosX == 0) { m_goalDelay = goalDelay; ++m_scoreRight; } // ball at far right - goal for left player else if (m_ballPosX == m_width - 1) { m_goalDelay = goalDelay; ++m_scoreLeft; } } /// request next time call - or cancel request if not needed void Pong::planTimeCall() { // no time call needed if not active if (! isActive()) { m_mgrs.m_callMgr.cancelTimeCall(this); return; } // compute interval based on score and bounce count int speedup = 10 * (m_scoreLeft + m_scoreRight) + m_bounceCnt; float scale = 0.3f + 0.7f * expf(-0.03f * speedup); float interval = 1e-3f * m_delay * scale; // request next time call Time stepTime; stepTime.fromFloatSec(interval); m_mgrs.m_callMgr.requestTimeCall(this, Time::now() + stepTime); } /// start ball void Pong::startBall() { // ball starts horizontally at middle of field, vertically random m_ballPosX = (m_width - (rand() & 1)) / 2; m_ballPosY = rand() % m_height; // random diagonal direction m_ballDirX = (rand() & 1) * 2 - 1; m_ballDirY = (rand() & 1) * 2 - 1; // no delays, ball has not bounced at pad m_leftDelay = 0; m_rightDelay = 0; m_goalDelay = 0; m_bounceCnt = 0; // draw and send frame redraw(); // request first time call if needed planTimeCall(); } } // namespace Blinker