write some App class for SDL UI
This commit is contained in:
parent
c78e4749f8
commit
8bbe43a14d
|
@ -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
|
||||
|
|
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
|
||||
|
||||
executable('chainreact-sdl',
|
||||
'src/core/ai.cpp',
|
||||
'src/core/board2d.cpp',
|
||||
'src/sdlui/board_widget_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();
|
||||
}
|
||||
|
||||
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};
|
||||
|
|
|
@ -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>;
|
||||
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
_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();
|
||||
});
|
||||
SDL_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");
|
||||
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();
|
||||
|
||||
const uint32_t header_caption_text_event = SDL_RegisterEvents(1);
|
||||
|
||||
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;
|
||||
_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(c, player);
|
||||
_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!");
|
||||
_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.type = _header_event;
|
||||
if (_current_player == core::Player::P1) {
|
||||
header_update.user.data1 = (void*)"Player 2";
|
||||
player = core::Player::P2;
|
||||
game_screen->flash("Player 2 turn…");
|
||||
_current_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…");
|
||||
_current_player = core::Player::P1;
|
||||
_game_screen->flash("Player 1 turn…");
|
||||
}
|
||||
accept_input = true;
|
||||
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, *next_move};
|
||||
delete next_move;
|
||||
_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;
|
||||
}
|
||||
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) {
|
||||
game_screen->header_text(static_cast<char*>(ev.user.data1));
|
||||
break;
|
||||
}
|
||||
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});
|
||||
renderer->clear();
|
||||
game_screen->render();
|
||||
renderer->present();
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
_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);
|
||||
}
|
||||
|
||||
_game_screen->render();
|
||||
_renderer->present();
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
ChainReactSDL app;
|
||||
app.run();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue