write some App class for SDL UI

This commit is contained in:
Andrea Blankenstijn 2022-02-11 11:15:14 +01:00
parent c78e4749f8
commit 8bbe43a14d
12 changed files with 262 additions and 100 deletions

View File

@ -1,13 +1,17 @@
# ChainReact
Modern C++ remake of an old game. WIP
Modern C++ remake of an old game. Work in progress.
![Start screen](media/start.png)
![Ongoing game](media/ongoinggame.png)
## Build and run
```shell-session
$ meson build
$ meson compile -C build
$ ./build/chainreact
$ ./build/chainreact-sdl
```
## Dependency

BIN
media/ongoinggame.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
media/start.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@ -20,6 +20,7 @@ elif compiler == 'g++'
endif
executable('chainreact-sdl',
'src/core/ai.cpp',
'src/core/board2d.cpp',
'src/sdlui/board_widget_impl.cpp',
'src/sdlui/game_screen_impl.cpp',

45
src/core/ai.cpp Normal file
View File

@ -0,0 +1,45 @@
#include <algorithm>
#include "ai.hpp"
using namespace core;
auto negamax_recurs(const Board2D& board, Player player, int depth, int color) -> float
{
if (depth == 0 || board.winner()) {
return color * board.count(player);
}
auto value {-1.F};
for (const auto& coord : board.valid_moves(player)) {
auto next_board {board};
for (auto mover = next_board.move(coord, player); mover; mover())
;
auto next_player {player == Player::P1 ? Player::P2 : Player::P1};
value =
std::max(value, -negamax_recurs(next_board, next_player, depth - 1, -color));
}
return value;
}
auto core::negamax(const Board2D& board, Player player, int depth) -> Board2D::Coord
{
Board2D::Coord best_move;
auto score {-1.F};
for (const auto& coord : board.valid_moves(player)) {
auto next_board {board};
for (auto mover = next_board.move(coord, player); mover; mover())
;
auto next_player {player == Player::P1 ? Player::P2 : Player::P1};
if (const auto next_score =
-negamax_recurs(next_board, next_player, depth - 1, -1);
next_score > score)
{
best_move = coord;
score = next_score;
}
}
return best_move;
}

12
src/core/ai.hpp Normal file
View File

@ -0,0 +1,12 @@
#ifndef CORE_AI
#define CORE_AI
#include "board2d.hpp"
#include "player.hpp"
namespace core
{
auto negamax(const Board2D&, Player, int) -> Board2D::Coord;
}
#endif

View File

@ -29,6 +29,19 @@ auto Board2D::begin() const noexcept -> const_iterator
return spaces.cbegin();
}
auto Board2D::count(Player player) const noexcept -> float
{
const float total = [this]() {
auto count {0};
for (const auto [_, u] : units) {
count += u;
}
return count;
}();
return units.at(player) / total;
}
void Board2D::capture(const Coord c, const Player p) noexcept
{
auto& s = at(c);
@ -84,6 +97,12 @@ auto Board2D::is_overloaded(Coord c) const noexcept -> bool
return s.value > s.capacity;
}
auto Board2D::is_valid_move(Coord coord, Player player) const noexcept -> bool
{
const auto& s = at(coord);
return s.owner == Player::NONE || s.owner == player;
}
auto Board2D::move(const Coord c, const Player p)
-> Generator<pair<Space, optional<Player>>>
{
@ -179,6 +198,16 @@ auto Board2D::spread(const Coord src, const vector<Coord>& targets, const Player
}
}
auto Board2D::valid_moves(Player player) const noexcept -> vector<Coord>
{
vector<Coord> moves;
for (const auto& s : *this) {
if (is_valid_move(s.coord, player)) moves.push_back(s.coord);
}
return moves;
}
auto Board2D::winner() const noexcept -> optional<Player>
{
Player winner {Player::NONE};

View File

@ -55,10 +55,12 @@ namespace core
Board2D(size_type width, size_type height);
[[nodiscard]] auto begin() const noexcept -> const_iterator;
[[nodiscard]] auto count(Player) const noexcept -> float;
[[nodiscard]] auto end() const noexcept -> const_iterator;
// Initialise the board with given size.
void init(size_type, size_type);
[[nodiscard]] auto is_overloaded(Coord) const noexcept -> bool;
[[nodiscard]] auto is_valid_move(Coord, Player) const noexcept -> bool;
// Make a move step by step.
[[nodiscard]] auto move(Coord, Player)
-> Generator<std::pair<Space, std::optional<Player>>>;
@ -68,6 +70,7 @@ namespace core
[[nodiscard]] auto operator()(Coord) const noexcept -> const Space&;
// Get board size as a <width, height> pair.
[[nodiscard]] auto size() const noexcept -> std::pair<size_type, size_type>;
[[nodiscard]] auto valid_moves(Player) const noexcept -> std::vector<Coord>;
// Get the winner, if any.
[[nodiscard]] auto winner() const noexcept -> std::optional<Player>;

View File

@ -194,5 +194,5 @@ void BoardWidgetImpl::_handle_rendering()
}
}
const Color BoardWidgetImpl::_default_color_p1 {0, 0, 255, SDL_ALPHA_OPAQUE};
const Color BoardWidgetImpl::_default_color_p2 {255, 0, 0, SDL_ALPHA_OPAQUE};
const Color BoardWidgetImpl::_default_color_p1 {52, 101, 164, SDL_ALPHA_OPAQUE};
const Color BoardWidgetImpl::_default_color_p2 {204, 0, 0, SDL_ALPHA_OPAQUE};

View File

@ -76,6 +76,7 @@ auto GameScreenImpl::board() const -> const shared_ptr<Board2D>&
void GameScreenImpl::board(std::shared_ptr<Board2D> board)
{
_board_widget->board(board);
header_text("Player 1");
_board = board;
}

View File

@ -1,123 +1,146 @@
#include <chrono>
#include <cstdlib>
#include <exception>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <thread>
#include <utility>
#include <basic_widgets/core/type/deleter.hpp>
#include <basic_widgets/w/default_theme.hpp>
#include <basic_widgets/w/widget_factory.hpp>
#include "game_screen_impl.hpp"
#include "sdlui.hpp"
using namespace bwidgets;
using namespace core;
using namespace std;
using namespace sdlui;
int main()
ChainReactSDL::ChainReactSDL()
: _accept_input {true},
_current_player {Player::P1},
_header_event {SDL_RegisterEvents(1)},
_move_thread {thread()}
{
SDL_Init(SDL_INIT_VIDEO);
TTF_Init();
std::atexit([]() {
SDL_Quit();
TTF_Quit();
});
auto win = unique_ptr<SDL_Window, bwidgets::Deleter>(SDL_CreateWindow(
"chainreact-cpp", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 800, 600,
SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_UTILITY));
auto renderer =
make_shared<bwidgets::Renderer>(win.get(), -1, SDL_RENDERER_ACCELERATED);
renderer->blend_mode(SDL_BLENDMODE_BLEND);
auto theme = make_shared<bwidgets::DefaultTheme>();
auto game_screen = make_shared<sdlui::GameScreenImpl>();
game_screen->renderer(renderer);
game_screen->theme(theme);
game_screen->header_text("Player 1");
_window = unique_ptr<SDL_Window, Deleter>(
SDL_CreateWindow("ChainReact", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 640,
480, SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE));
_game_screen = make_unique<GameScreenImpl>();
_renderer = make_shared<Renderer>(_window.get(), -1, SDL_RENDERER_ACCELERATED);
_theme = make_shared<bwidgets::DefaultTheme>();
const uint32_t header_caption_text_event = SDL_RegisterEvents(1);
_renderer->blend_mode(SDL_BLENDMODE_BLEND);
_game_screen->renderer(_renderer);
_game_screen->theme(_theme);
}
auto player {core::Player::P1};
bool quit {false};
bool accept_input {true};
thread move_thread {};
while (!quit) {
SDL_Event ev;
while (SDL_PollEvent(&ev) != 0) {
switch (ev.type) {
case SDL_QUIT:
quit = true;
break;
case SDL_WINDOWEVENT:
if (ev.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
const auto [w, h] = renderer->output_size();
game_screen->viewport({0, 0, w, h});
}
break;
}
if (ev.type == game_screen->board_event_type()) {
switch (static_cast<sdlui::BoardWidget::EventCode>(ev.user.code)) {
case sdlui::BoardWidget::EventCode::MOVE:
if (accept_input) {
if (move_thread.joinable()) move_thread.join();
const auto* next_move =
static_cast<core::Board2D::Coord*>(ev.user.data1);
const auto move_task =
[&accept_input, &player, &game_screen,
&header_caption_text_event](core::Board2D::Coord c) {
accept_input = false;
try {
for (auto mover =
game_screen->board()->move(c, player);
mover;) {
const auto [_, winner] = mover();
if (winner) {
if (winner.value() == core::Player::P1)
game_screen->flash("Player 1 wins!");
else game_screen->flash("Player 2 wins!");
return;
}
this_thread::sleep_for(750ms);
}
SDL_Event header_update;
SDL_zero(header_update);
header_update.type = header_caption_text_event;
if (player == core::Player::P1) {
header_update.user.data1 = (void*)"Player 2";
player = core::Player::P2;
game_screen->flash("Player 2 turn…");
}
else {
header_update.user.data1 = (void*)"Player 1";
player = core::Player::P1;
game_screen->flash("Player 1 turn…");
}
accept_input = true;
SDL_PushEvent(&header_update);
} catch (const core::Board2D::InvalidMove&) {
}
};
move_thread = thread {move_task, *next_move};
delete next_move;
ChainReactSDL::~ChainReactSDL()
{
_game_screen.reset();
_theme.reset();
_renderer.reset();
_window.reset();
TTF_Quit();
SDL_Quit();
}
void ChainReactSDL::handle_board_event(const SDL_UserEvent& event)
{
switch (static_cast<BoardWidget::EventCode>(event.code)) {
case BoardWidget::EventCode::MOVE: {
if (!_accept_input) break;
if (_move_thread.joinable()) _move_thread.join();
_accept_input = false;
const auto* coord = static_cast<const Board2D::Coord*>(event.data1);
const auto move_task = [this](Board2D::Coord coord) {
try {
for (auto mover =
_game_screen->board()->move(coord, _current_player);
mover;) {
const auto [_, winner] = mover();
if (winner) {
if (winner.value() == core::Player::P1)
_game_screen->flash("Player 1 wins!");
else _game_screen->flash("Player 2 wins!");
return;
}
break;
case sdlui::BoardWidget::EventCode::BOARD_CHANGED:
player = core::Player::P1;
if (!game_screen->board()->winner()) accept_input = true;
this_thread::sleep_for(750ms);
}
SDL_Event header_update;
SDL_zero(header_update);
header_update.type = _header_event;
if (_current_player == core::Player::P1) {
header_update.user.data1 = (void*)"Player 2";
_current_player = core::Player::P2;
_game_screen->flash("Player 2 turn…");
}
else {
header_update.user.data1 = (void*)"Player 1";
_current_player = core::Player::P1;
_game_screen->flash("Player 1 turn…");
}
SDL_PushEvent(&header_update);
_accept_input = true;
} catch (const core::Board2D::InvalidMove&) {
_game_screen->flash("Invalid move!");
_accept_input = true;
}
};
_move_thread = thread(move_task, *coord);
delete coord;
break;
}
case BoardWidget::EventCode::BOARD_CHANGED:
if (!_game_screen->board()->winner()) {
_current_player = Player::P1;
_accept_input = true;
}
else if (ev.type == header_caption_text_event) {
game_screen->header_text(static_cast<char*>(ev.user.data1));
break;
}
}
void ChainReactSDL::handle_event(const SDL_Event& event)
{
switch (event.type) {
case SDL_QUIT:
_quit = true;
break;
case SDL_WINDOWEVENT:
if (event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
const auto [w, h] = _renderer->output_size();
_game_screen->viewport({0, 0, w, h});
}
game_screen->handle_event(ev);
break;
}
if (event.type == _game_screen->board_event_type()) {
handle_board_event(event.user);
}
else if (event.type == _header_event) {
_game_screen->header_text(static_cast<const char*>(event.user.data1));
}
_game_screen->handle_event(event);
}
void ChainReactSDL::run()
{
_quit = false;
auto player {Player::P1};
while (!_quit) {
SDL_Event event;
while (SDL_PollEvent(&event) != 0) {
handle_event(event);
}
renderer->draw_color({50, 60, 70, SDL_ALPHA_OPAQUE});
renderer->clear();
game_screen->render();
renderer->present();
_game_screen->render();
_renderer->present();
}
return EXIT_SUCCESS;
}
int main()
{
ChainReactSDL app;
app.run();
}

View File

@ -1,4 +1,48 @@
#ifndef GUI_SDLUI
#define GUI_SDLUI
#include <memory>
#include <thread>
#include <basic_widgets/core/type/deleter.hpp>
#include "game_screen.hpp"
extern "C" {
union SDL_Event;
struct SDL_UserEvent;
struct SDL_Window;
}
namespace bwidgets
{
class Renderer;
}
namespace sdlui
{
class ChainReactSDL final
{
public:
ChainReactSDL();
~ChainReactSDL();
void run();
private:
void handle_board_event(const SDL_UserEvent&);
void handle_event(const SDL_Event&);
bool _accept_input;
core::Player _current_player;
const uint32_t _header_event;
std::thread _move_thread;
bool _quit;
std::unique_ptr<GameScreen> _game_screen;
std::shared_ptr<bwidgets::Renderer> _renderer;
std::shared_ptr<bwidgets::Theme> _theme;
std::unique_ptr<SDL_Window, bwidgets::Deleter> _window;
};
}
#endif