write some App class for SDL UI
This commit is contained in:
parent
c78e4749f8
commit
8bbe43a14d
|
@ -1,13 +1,17 @@
|
||||||
# ChainReact
|
# 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
|
## Build and run
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
$ meson build
|
$ meson build
|
||||||
$ meson compile -C build
|
$ meson compile -C build
|
||||||
$ ./build/chainreact
|
$ ./build/chainreact-sdl
|
||||||
```
|
```
|
||||||
|
|
||||||
## Dependency
|
## Dependency
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
After Width: | Height: | Size: 7.2 KiB |
|
@ -20,6 +20,7 @@ elif compiler == 'g++'
|
||||||
endif
|
endif
|
||||||
|
|
||||||
executable('chainreact-sdl',
|
executable('chainreact-sdl',
|
||||||
|
'src/core/ai.cpp',
|
||||||
'src/core/board2d.cpp',
|
'src/core/board2d.cpp',
|
||||||
'src/sdlui/board_widget_impl.cpp',
|
'src/sdlui/board_widget_impl.cpp',
|
||||||
'src/sdlui/game_screen_impl.cpp',
|
'src/sdlui/game_screen_impl.cpp',
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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
|
|
@ -29,6 +29,19 @@ auto Board2D::begin() const noexcept -> const_iterator
|
||||||
return spaces.cbegin();
|
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
|
void Board2D::capture(const Coord c, const Player p) noexcept
|
||||||
{
|
{
|
||||||
auto& s = at(c);
|
auto& s = at(c);
|
||||||
|
@ -84,6 +97,12 @@ auto Board2D::is_overloaded(Coord c) const noexcept -> bool
|
||||||
return s.value > s.capacity;
|
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)
|
auto Board2D::move(const Coord c, const Player p)
|
||||||
-> Generator<pair<Space, optional<Player>>>
|
-> 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>
|
auto Board2D::winner() const noexcept -> optional<Player>
|
||||||
{
|
{
|
||||||
Player winner {Player::NONE};
|
Player winner {Player::NONE};
|
||||||
|
|
|
@ -55,10 +55,12 @@ namespace core
|
||||||
Board2D(size_type width, size_type height);
|
Board2D(size_type width, size_type height);
|
||||||
|
|
||||||
[[nodiscard]] auto begin() const noexcept -> const_iterator;
|
[[nodiscard]] auto begin() const noexcept -> const_iterator;
|
||||||
|
[[nodiscard]] auto count(Player) const noexcept -> float;
|
||||||
[[nodiscard]] auto end() const noexcept -> const_iterator;
|
[[nodiscard]] auto end() const noexcept -> const_iterator;
|
||||||
// Initialise the board with given size.
|
// Initialise the board with given size.
|
||||||
void init(size_type, size_type);
|
void init(size_type, size_type);
|
||||||
[[nodiscard]] auto is_overloaded(Coord) const noexcept -> bool;
|
[[nodiscard]] auto is_overloaded(Coord) const noexcept -> bool;
|
||||||
|
[[nodiscard]] auto is_valid_move(Coord, Player) const noexcept -> bool;
|
||||||
// Make a move step by step.
|
// Make a move step by step.
|
||||||
[[nodiscard]] auto move(Coord, Player)
|
[[nodiscard]] auto move(Coord, Player)
|
||||||
-> Generator<std::pair<Space, std::optional<Player>>>;
|
-> Generator<std::pair<Space, std::optional<Player>>>;
|
||||||
|
@ -68,6 +70,7 @@ namespace core
|
||||||
[[nodiscard]] auto operator()(Coord) const noexcept -> const Space&;
|
[[nodiscard]] auto operator()(Coord) const noexcept -> const Space&;
|
||||||
// Get board size as a <width, height> pair.
|
// Get board size as a <width, height> pair.
|
||||||
[[nodiscard]] auto size() const noexcept -> std::pair<size_type, size_type>;
|
[[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.
|
// Get the winner, if any.
|
||||||
[[nodiscard]] auto winner() const noexcept -> std::optional<Player>;
|
[[nodiscard]] auto winner() const noexcept -> std::optional<Player>;
|
||||||
|
|
||||||
|
|
|
@ -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_p1 {52, 101, 164, SDL_ALPHA_OPAQUE};
|
||||||
const Color BoardWidgetImpl::_default_color_p2 {255, 0, 0, SDL_ALPHA_OPAQUE};
|
const Color BoardWidgetImpl::_default_color_p2 {204, 0, 0, SDL_ALPHA_OPAQUE};
|
||||||
|
|
|
@ -76,6 +76,7 @@ auto GameScreenImpl::board() const -> const shared_ptr<Board2D>&
|
||||||
void GameScreenImpl::board(std::shared_ptr<Board2D> board)
|
void GameScreenImpl::board(std::shared_ptr<Board2D> board)
|
||||||
{
|
{
|
||||||
_board_widget->board(board);
|
_board_widget->board(board);
|
||||||
|
header_text("Player 1");
|
||||||
_board = board;
|
_board = board;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,123 +1,146 @@
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstdlib>
|
|
||||||
#include <exception>
|
|
||||||
#include <iostream>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
#include <basic_widgets/core/type/deleter.hpp>
|
|
||||||
#include <basic_widgets/w/default_theme.hpp>
|
#include <basic_widgets/w/default_theme.hpp>
|
||||||
#include <basic_widgets/w/widget_factory.hpp>
|
#include <basic_widgets/w/widget_factory.hpp>
|
||||||
|
|
||||||
#include "game_screen_impl.hpp"
|
#include "game_screen_impl.hpp"
|
||||||
#include "sdlui.hpp"
|
#include "sdlui.hpp"
|
||||||
|
|
||||||
|
using namespace bwidgets;
|
||||||
|
using namespace core;
|
||||||
using namespace std;
|
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);
|
SDL_Init(SDL_INIT_VIDEO);
|
||||||
TTF_Init();
|
TTF_Init();
|
||||||
std::atexit([]() {
|
|
||||||
SDL_Quit();
|
_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>();
|
||||||
|
|
||||||
|
_renderer->blend_mode(SDL_BLENDMODE_BLEND);
|
||||||
|
_game_screen->renderer(_renderer);
|
||||||
|
_game_screen->theme(_theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
ChainReactSDL::~ChainReactSDL()
|
||||||
|
{
|
||||||
|
_game_screen.reset();
|
||||||
|
_theme.reset();
|
||||||
|
_renderer.reset();
|
||||||
|
_window.reset();
|
||||||
TTF_Quit();
|
TTF_Quit();
|
||||||
});
|
SDL_Quit();
|
||||||
|
}
|
||||||
|
|
||||||
auto win = unique_ptr<SDL_Window, bwidgets::Deleter>(SDL_CreateWindow(
|
void ChainReactSDL::handle_board_event(const SDL_UserEvent& event)
|
||||||
"chainreact-cpp", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 800, 600,
|
{
|
||||||
SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_UTILITY));
|
switch (static_cast<BoardWidget::EventCode>(event.code)) {
|
||||||
auto renderer =
|
case BoardWidget::EventCode::MOVE: {
|
||||||
make_shared<bwidgets::Renderer>(win.get(), -1, SDL_RENDERER_ACCELERATED);
|
if (!_accept_input) break;
|
||||||
renderer->blend_mode(SDL_BLENDMODE_BLEND);
|
if (_move_thread.joinable()) _move_thread.join();
|
||||||
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");
|
|
||||||
|
|
||||||
const uint32_t header_caption_text_event = SDL_RegisterEvents(1);
|
_accept_input = false;
|
||||||
|
const auto* coord = static_cast<const Board2D::Coord*>(event.data1);
|
||||||
auto player {core::Player::P1};
|
const auto move_task = [this](Board2D::Coord coord) {
|
||||||
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 {
|
try {
|
||||||
for (auto mover =
|
for (auto mover =
|
||||||
game_screen->board()->move(c, player);
|
_game_screen->board()->move(coord, _current_player);
|
||||||
mover;) {
|
mover;) {
|
||||||
const auto [_, winner] = mover();
|
const auto [_, winner] = mover();
|
||||||
if (winner) {
|
if (winner) {
|
||||||
if (winner.value() == core::Player::P1)
|
if (winner.value() == core::Player::P1)
|
||||||
game_screen->flash("Player 1 wins!");
|
_game_screen->flash("Player 1 wins!");
|
||||||
else game_screen->flash("Player 2 wins!");
|
else _game_screen->flash("Player 2 wins!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this_thread::sleep_for(750ms);
|
this_thread::sleep_for(750ms);
|
||||||
}
|
}
|
||||||
SDL_Event header_update;
|
SDL_Event header_update;
|
||||||
SDL_zero(header_update);
|
SDL_zero(header_update);
|
||||||
header_update.type = header_caption_text_event;
|
header_update.type = _header_event;
|
||||||
if (player == core::Player::P1) {
|
if (_current_player == core::Player::P1) {
|
||||||
header_update.user.data1 = (void*)"Player 2";
|
header_update.user.data1 = (void*)"Player 2";
|
||||||
player = core::Player::P2;
|
_current_player = core::Player::P2;
|
||||||
game_screen->flash("Player 2 turn…");
|
_game_screen->flash("Player 2 turn…");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
header_update.user.data1 = (void*)"Player 1";
|
header_update.user.data1 = (void*)"Player 1";
|
||||||
player = core::Player::P1;
|
_current_player = core::Player::P1;
|
||||||
game_screen->flash("Player 1 turn…");
|
_game_screen->flash("Player 1 turn…");
|
||||||
}
|
}
|
||||||
accept_input = true;
|
|
||||||
SDL_PushEvent(&header_update);
|
SDL_PushEvent(&header_update);
|
||||||
|
_accept_input = true;
|
||||||
} catch (const core::Board2D::InvalidMove&) {
|
} catch (const core::Board2D::InvalidMove&) {
|
||||||
|
_game_screen->flash("Invalid move!");
|
||||||
|
_accept_input = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
move_thread = thread {move_task, *next_move};
|
_move_thread = thread(move_task, *coord);
|
||||||
delete next_move;
|
delete coord;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case BoardWidget::EventCode::BOARD_CHANGED:
|
||||||
|
if (!_game_screen->board()->winner()) {
|
||||||
|
_current_player = Player::P1;
|
||||||
|
_accept_input = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case sdlui::BoardWidget::EventCode::BOARD_CHANGED:
|
|
||||||
player = core::Player::P1;
|
|
||||||
if (!game_screen->board()->winner()) accept_input = true;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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});
|
||||||
}
|
}
|
||||||
else if (ev.type == header_caption_text_event) {
|
break;
|
||||||
game_screen->header_text(static_cast<char*>(ev.user.data1));
|
|
||||||
}
|
}
|
||||||
game_screen->handle_event(ev);
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer->draw_color({50, 60, 70, SDL_ALPHA_OPAQUE});
|
_game_screen->handle_event(event);
|
||||||
renderer->clear();
|
}
|
||||||
game_screen->render();
|
|
||||||
renderer->present();
|
void ChainReactSDL::run()
|
||||||
}
|
{
|
||||||
return EXIT_SUCCESS;
|
_quit = false;
|
||||||
|
auto player {Player::P1};
|
||||||
|
while (!_quit) {
|
||||||
|
SDL_Event event;
|
||||||
|
while (SDL_PollEvent(&event) != 0) {
|
||||||
|
handle_event(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
_game_screen->render();
|
||||||
|
_renderer->present();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
ChainReactSDL app;
|
||||||
|
app.run();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,48 @@
|
||||||
#ifndef GUI_SDLUI
|
#ifndef GUI_SDLUI
|
||||||
#define 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
|
#endif
|
||||||
|
|
Loading…
Reference in New Issue