GameEngine is now a state machine so I can push its design further and keep it solid.

master
Zed A. Shaw 2 months ago
parent 1c89afaee2
commit 9e6c05eccd
  1. 11
      builder.cpp
  2. 4
      fsm.hpp
  3. 53
      game_engine.cpp
  4. 17
      game_engine.hpp
  5. 23
      tests/game_engine.cpp

@ -76,20 +76,22 @@ void Builder::building(BuildEvent ev) {
int rc = pclose(build_out); int rc = pclose(build_out);
if(rc == 0) { if(rc == 0) {
game.event(GameEvent::BUILD_SUCCESS);
gui.build_works(); gui.build_works();
} else { } else {
// BUG: make it not play two sounds game.event(GameEvent::BUILD_FAILED);
gui.build_failed(true, build_cmd); gui.build_failed(true, build_cmd);
} }
build_out = NULL; build_out = NULL;
game.event(GameEvent::BUILD_DONE);
state(DONE); state(DONE);
} else { } else {
auto m = parse_line(line); auto m = parse_line(line);
if(m.match) { if(m.match) {
gui.output(format("HIT WITH {} @ {}:{}:{} {}", m.type, m.file_name, m.lnumber, m.col, m.message)); gui.output(format("HIT WITH {} @ {}:{}:{} {}", m.type, m.file_name, m.lnumber, m.col, m.message));
game.hit(m.type); game.event(GameEvent::HIT);
} }
state(READING); state(READING);
} }
@ -117,7 +119,7 @@ void Builder::start(BuildEvent ev) {
void Builder::waiting(BuildEvent ev) { void Builder::waiting(BuildEvent ev) {
if(listener->changes) { if(listener->changes) {
game.start_round(); game.event(GameEvent::BUILD_START);
gui.building(); gui.building();
gui.output(format("CHANGES! Running build {}", build_cmd)); gui.output(format("CHANGES! Running build {}", build_cmd));
state(FORKING); state(FORKING);
@ -162,11 +164,8 @@ void Builder::reading(BuildEvent ev) {
} }
void Builder::done(BuildEvent ev) { void Builder::done(BuildEvent ev) {
game.end_round();
if(game.is_dead()) { if(game.is_dead()) {
gui.you_died(); gui.you_died();
game.reset();
} }
listener->reset_state(); listener->reset_state();

@ -5,15 +5,19 @@
#define FSM_EV(S, F) case S: F(); break #define FSM_EV(S, F) case S: F(); break
#define FSM_STATE(S, F, E) case S: F(E); break #define FSM_STATE(S, F, E) case S: F(E); break
#define FSM_STATE_LOG(C, S, F, E) case C::S: fmt::println(">> " #C " " #S ":" #F " event={}, state={}", int(E), int(_state)); F(E); fmt::println("<< " #C " state={}", int(_state)); break
template<typename S, typename E> template<typename S, typename E>
class DeadSimpleFSM { class DeadSimpleFSM {
protected: protected:
// BUG: don't put this in your class because state() won't work
S _state = S::START; S _state = S::START;
public: public:
virtual void event(E event) = 0; virtual void event(E event) = 0;
void state(S next_state) { void state(S next_state) {
// fmt::println("STATE {}->{}", int(_state), int(next_state));
_state = next_state; _state = next_state;
} }

@ -4,6 +4,7 @@
#include <fmt/core.h> #include <fmt/core.h>
#include <fmt/color.h> #include <fmt/color.h>
#include "game_engine.hpp" #include "game_engine.hpp"
#include <cassert>
const auto ERROR = fmt::emphasis::bold | fg(fmt::color::red); const auto ERROR = fmt::emphasis::bold | fg(fmt::color::red);
@ -23,18 +24,6 @@ int GameEngine::determine_damage(string &type) {
} }
} }
void GameEngine::start_round() {
hits_taken = 0;
}
void GameEngine::end_round() {
++rounds;
if(hits_taken == 0) {
++streak;
heal();
}
}
void GameEngine::reset() { void GameEngine::reset() {
rounds = 0; rounds = 0;
streak = 0; streak = 0;
@ -46,8 +35,6 @@ bool GameEngine::hit(string &type) {
int damage = determine_damage(type); int damage = determine_damage(type);
hit_points -= damage; hit_points -= damage;
++hits_taken; ++hits_taken;
streak = 0;
return is_dead(); return is_dead();
} }
@ -62,20 +49,52 @@ bool GameEngine::is_dead() {
void GameEngine::start(GameEvent ev) { void GameEngine::start(GameEvent ev) {
state(GameState::IDLE); state(GameState::IDLE);
idle(ev);
} }
void GameEngine::idle(GameEvent ev) { void GameEngine::idle(GameEvent ev) {
if(ev == GameEvent::BUILD_START) {
hits_taken = 0;
state(GameState::IN_ROUND);
} else {
state(GameState::IDLE); state(GameState::IDLE);
}
} }
void GameEngine::in_round(GameEvent ev) { void GameEngine::in_round(GameEvent ev) {
if(ev == GameEvent::HIT) {
string er_type = "error";
hit(er_type); // FIXME: bring back error type
if(is_dead()) {
state(GameState::DEAD);
} else {
state(GameState::IN_ROUND); state(GameState::IN_ROUND);
}
} else if(ev == GameEvent::BUILD_SUCCESS) {
state(GameState::SUCCESS);
} else if(ev == GameEvent::BUILD_FAILED) {
state(GameState::FAILURE);
} else {
state(GameState::IN_ROUND);
}
} }
void GameEngine::dead(GameEvent ev) { void GameEngine::dead(GameEvent ev) {
state(GameState::DEAD); reset();
state(GameState::IDLE);
}
void GameEngine::success(GameEvent ev) {
assert(ev == GameEvent::BUILD_DONE && "success state expected BUILD_DONE");
++rounds;
++streak;
heal();
state(GameState::IDLE);
} }
void GameEngine::alive(GameEvent ev) { void GameEngine::failure(GameEvent ev) {
state(GameState::ALIVE); assert(ev == GameEvent::BUILD_DONE && "failure state expected BUILD_DONE");
++rounds;
streak = 0;
state(GameState::IDLE);
} }

@ -9,16 +9,17 @@
using std::string; using std::string;
enum class GameState { enum class GameState {
START, IDLE, IN_ROUND, DEAD, ALIVE START, IDLE, IN_ROUND, DEAD, SUCCESS, FAILURE
}; };
enum class GameEvent { enum class GameEvent {
BUILD_START, BUILD_END, BUILD_START, BUILD_SUCCESS,
HIT BUILD_DONE, BUILD_FAILED, HIT
}; };
class GameEngine : DeadSimpleFSM<GameState, GameEvent> { class GameEngine : DeadSimpleFSM<GameState, GameEvent> {
std::map<string, int> damage_types{ std::map<string, int> damage_types{
{"error", 4}, {"error", 4},
{"warning", 1}, {"warning", 1},
@ -31,7 +32,6 @@ class GameEngine : DeadSimpleFSM<GameState, GameEvent> {
int hits_taken = 0; int hits_taken = 0;
int rounds = 0; int rounds = 0;
int streak = 0; int streak = 0;
GameState _state = GameState::START;
GameEngine(int hp); GameEngine(int hp);
@ -47,7 +47,8 @@ class GameEngine : DeadSimpleFSM<GameState, GameEvent> {
FSM_STATE(GameState::IDLE, idle, ev); FSM_STATE(GameState::IDLE, idle, ev);
FSM_STATE(GameState::IN_ROUND, in_round, ev); FSM_STATE(GameState::IN_ROUND, in_round, ev);
FSM_STATE(GameState::DEAD, dead, ev); FSM_STATE(GameState::DEAD, dead, ev);
FSM_STATE(GameState::ALIVE, alive, ev); FSM_STATE(GameState::SUCCESS, success, ev);
FSM_STATE(GameState::FAILURE, failure, ev);
} }
} }
@ -56,12 +57,10 @@ class GameEngine : DeadSimpleFSM<GameState, GameEvent> {
void idle(GameEvent ev); void idle(GameEvent ev);
void in_round(GameEvent ev); void in_round(GameEvent ev);
void dead(GameEvent ev); void dead(GameEvent ev);
void alive(GameEvent ev); void success(GameEvent ev);
void failure(GameEvent ev);
// current API that will die // current API that will die
void start_round();
void end_round();
void heal(); void heal();
bool hit(string &type); bool hit(string &type);
void reset(); void reset();

@ -7,32 +7,11 @@ using namespace fmt;
TEST_CASE("game engine can start and take hit", "[game_engine]") { TEST_CASE("game engine can start and take hit", "[game_engine]") {
// test fails on purpose right now // test fails on purpose right now
GameEngine game{4}; GameEngine game{4};
std::string err{"error"}; REQUIRE(!game.is_dead() == true);
game.start_round();
game.hit(err);
game.end_round();
REQUIRE(game.is_dead() == true);
} }
TEST_CASE("", "[game_engine]") { TEST_CASE("", "[game_engine]") {
// test fails on purpose right now // test fails on purpose right now
GameEngine game{100}; GameEngine game{100};
std::string err{"error"};
game.start_round();
game.hit(err);
game.end_round();
REQUIRE(game.hit_points < 100);
REQUIRE(game.rounds == 1);
REQUIRE(game.streak == 0);
REQUIRE(game.is_dead() == false);
game.start_round();
game.end_round();
REQUIRE(game.hit_points == 100);
REQUIRE(game.rounds == 2);
REQUIRE(game.streak == 1);
REQUIRE(game.is_dead() == false); REQUIRE(game.is_dead() == false);
} }

Loading…
Cancel
Save