start rewrite

This commit is contained in:
Andrea Blankenstijn 2022-01-04 13:56:03 +01:00
parent 7c930df84c
commit ee51631905
25 changed files with 423 additions and 1027 deletions

View File

@ -1,223 +0,0 @@
#include <array>
#include <cmath>
#include <SDL2/SDL_assert.h>
#include <SDL2/SDL_events.h>
#include <basic_widgets/core/draw.hpp>
#include <basic_widgets/core/renderer.hpp>
#include <basic_widgets/core/texture.hpp>
#include "board_widget.hpp"
#include "game.hpp"
using namespace bwidgets;
using namespace game;
using namespace game::data;
ui::BoardWidget::BoardWidget(Widget* p) : Widget(p)
{
_click_area = &_widget_area;
click_handler = [this](const SDL_MouseButtonEvent& p) -> void {
if (board != nullptr) {
auto* coord = new SDL_Point(this->board_coord_from_input({p.x, p.y}));
SDL_Event play_ev;
SDL_zero(play_ev);
play_ev.type = this->event_type;
play_ev.user.code = (Sint32)BoardWidget::event_code::PLAY;
play_ev.user.data1 = this;
play_ev.user.data2 = coord;
SDL_PushEvent(&play_ev);
}
};
}
ui::BoardWidget::~BoardWidget()
{
discard(_texture_p1, _texture_p2);
}
auto ui::BoardWidget::board_coord_from_input(const SDL_Point& p) const -> SDL_Point
{
if (board == nullptr) return {-1, -1};
return {(p.x - _viewport.x - _widget_area.x) / (_widget_area.w / board->width),
(p.y - _viewport.y - _widget_area.y) / (_widget_area.h / board->height)};
}
auto ui::BoardWidget::size() const noexcept -> Size
{
Size min_board_size;
// NOLINTNEXTLINE(readability-magic-numbers)
if (board != nullptr) min_board_size = {board->width * 64, board->height * 64};
// NOLINTNEXTLINE(readability-magic-numbers)
else min_board_size = {64, 64};
auto shortest {min_board_size.w < min_board_size.h ? min_board_size.w
: min_board_size.h};
int border {shortest / _border_ratio};
return {min_board_size.w + 2 * border, min_board_size.h + 2 * border};
}
void ui::BoardWidget::_draw_atoms(const SDL_Rect& area, int atoms, Texture* texture)
{
const auto radius = area.w < area.h ? area.w / 8 : area.h / 8;
const auto diameter = radius * 2;
const SDL_Point center {area.w / 2 - radius, area.h / 2 - radius};
const SDL_Point diagonal_radius {
(int)std::sqrt(radius * radius / 2),
(int)std::sqrt(radius * radius / 2),
};
const int disabled = -1;
// clang-format off
std::array<SDL_Point, 4> render_origins;
render_origins[0] = {
atoms == 1 || atoms == 3
? center.x
: atoms == 2 || atoms == 4
? center.x + diagonal_radius.x
: disabled,
atoms == 1 || atoms == 3
? center.y
: atoms == 2 || atoms == 4
? center.y + diagonal_radius.y
: disabled
};
render_origins[1] = {
atoms >= 2 && atoms <= 4
? center.x - diagonal_radius.x
: disabled,
atoms >= 2 && atoms <= 4
? center.y - diagonal_radius.y
: disabled,
};
render_origins[2] = {
atoms == 3
? center.x + diagonal_radius.x
: atoms == 4
? center.x + diagonal_radius.x
: disabled,
atoms == 3
? center.y + diagonal_radius.y
: atoms == 4
? center.y - diagonal_radius.y
: disabled
};
render_origins[3] = {
atoms == 4
? center.x - diagonal_radius.x
: disabled,
atoms == 4
? center.y + diagonal_radius.y
: disabled
};
// clang-format on
for (auto o : render_origins) {
const int size {o.x == disabled ? 0 : diameter};
_renderer->copy(texture, nullptr, {area.x + o.x, area.y + o.y, size, size});
}
}
void ui::BoardWidget::_handle_geometry_change(const SDL_Rect& vp) noexcept
{
_widget_area = {0, 0, vp.w, vp.h};
auto smallest_size {_widget_area.w < _widget_area.h ? _widget_area.w
: _widget_area.h};
_border_area = {_widget_area.x, _widget_area.y, smallest_size / _border_ratio,
smallest_size / _border_ratio};
_widget_area = {_border_area.x + _border_area.w, _border_area.y + _border_area.w,
_widget_area.w - 2 * (_border_area.w),
_widget_area.h - 2 * (_border_area.w)};
discard(_texture_p1, _texture_p2);
}
void ui::BoardWidget::_handle_renderer_change(Renderer* /*unused*/)
{
discard(_texture_p1, _texture_p2);
}
void ui::BoardWidget::_handle_rendering()
{
if ((_texture_p1 == nullptr) || (_texture_p2 == nullptr)) _handle_texture_update();
_renderer->draw_color(color_bg)->fill_rect(_widget_area);
_render_empty_board();
_render_square_content();
}
void ui::BoardWidget::_handle_texture_update()
{
SDL_assert_release((_texture_p1 == nullptr) && (_texture_p2 == nullptr)); // NOLINT
if (board == nullptr) return;
auto circle_size = _widget_area.w > _widget_area.h
? _widget_area.h / board->height / 4
: _widget_area.w / board->width / 4;
// NOLINTNEXTLINE(readability-magic-numbers)
auto aa = circle_size > 20 ? 3 : circle_size > 10 ? 2 : circle_size > 4 ? 1 : 0;
// NOLINTNEXTLINE(readability-magic-numbers)
_texture_p1 = filled_circle({75, 125, 255, 255}, circle_size, _renderer, aa);
// NOLINTNEXTLINE(readability-magic-numbers)
_texture_p2 = filled_circle({255, 100, 100, 255}, circle_size, _renderer, aa);
}
void ui::BoardWidget::_render_empty_board()
{
_renderer->draw_color(color_outline);
if (board != nullptr) {
for (int col = 0; col <= board->width; col++) {
int x = col * (_widget_area.w - 1) / board->width;
_renderer->draw_line({_widget_area.x + x, _widget_area.y},
{_widget_area.x + x, _widget_area.y + _widget_area.h});
}
for (int row = 0; row <= board->height; row++) {
int y = row * (_widget_area.h - 1) / board->height;
_renderer->draw_line({_widget_area.x, _widget_area.y + y},
{_widget_area.x + _widget_area.w, _widget_area.y + y});
}
}
for (int xy = 0; xy < _border_area.w; xy++) {
const float color_factor = 1 - (float)xy / (float)_border_area.w / 3;
_renderer
->draw_color({(uint8_t)((float)color_outline.r * color_factor),
(uint8_t)((float)color_outline.g * color_factor),
(uint8_t)((float)color_outline.b * color_factor),
color_outline.a})
->draw_rect({_border_area.x + xy, _border_area.y + xy,
_widget_area.w + 2 * (_widget_area.x - _border_area.x - xy) + 1,
_widget_area.h + 2 * (_widget_area.y - _border_area.y - xy) + 1});
}
}
void ui::BoardWidget::_render_square_content()
{
if (board == nullptr) return;
for (int y = 0; y < board->height; y++) {
for (int x = 0; x < board->width; x++) {
const SDL_Rect sqr_area {
_widget_area.x + x * _widget_area.w / board->width + 1,
_widget_area.y + y * _widget_area.h / board->height + 1,
_widget_area.w / board->width - 2, _widget_area.h / board->height - 2};
const auto& sqr = Board::square(*board, {x, y});
const auto capacity = logic::square_capacity(*board, {x, y});
if (sqr.value > capacity) {
_renderer->draw_color(color_hl1)->fill_rect(sqr_area);
}
else if (sqr.value == capacity) {
_renderer->draw_color(color_hl2)->fill_rect(sqr_area);
}
_draw_atoms(sqr_area, sqr.value,
sqr.owner == game::data::Player::P1 ? _texture_p1 : _texture_p2);
}
}
}

View File

@ -1,72 +0,0 @@
#ifndef BOARD_WIDGET_HPP
#define BOARD_WIDGET_HPP
#include <SDL2/SDL_rect.h>
#include <SDL2/SDL_stdinc.h>
#include <basic_widgets/w/base/widget.hpp>
#include <basic_widgets/w/feat/mouse_handler.hpp>
#include <basic_widgets/w/feat/texture_handler.hpp>
extern "C" {
auto SDL_RegisterEvents(int) -> Uint32;
}
namespace game::data
{
struct Board;
}
namespace game::ui
{
class BoardWidget : public bwidgets::Widget,
public bwidgets::MouseHandler,
public bwidgets::TextureHandler
{
protected:
static const int _border_ratio {30};
SDL_Rect _border_area;
bwidgets::Texture* _texture_p1 {nullptr};
bwidgets::Texture* _texture_p2 {nullptr};
virtual void _draw_atoms(const SDL_Rect&, int, bwidgets::Texture*);
virtual void _render_empty_board();
virtual void _render_square_content();
void _handle_focus_change(bool) override {}
void _handle_geometry_change(const SDL_Rect&) noexcept override;
void _handle_renderer_change(bwidgets::Renderer*) override;
void _handle_rendering() override;
void _handle_texture_update() override;
public:
enum struct event_code : Sint32
{
PLAY
};
const Uint32 event_type {SDL_RegisterEvents(1)};
const data::Board* board {nullptr};
// NOLINTNEXTLINE(readability-magic-numbers)
SDL_Color color_bg {50, 50, 50, SDL_ALPHA_OPAQUE};
// NOLINTNEXTLINE(readability-magic-numbers)
SDL_Color color_hl1 {255, 0, 0, SDL_ALPHA_OPAQUE / 16};
// NOLINTNEXTLINE(readability-magic-numbers)
SDL_Color color_hl2 {255, 255, 0, SDL_ALPHA_OPAQUE / 16};
// NOLINTNEXTLINE(readability-magic-numbers)
SDL_Color color_outline {175, 175, 175, SDL_ALPHA_OPAQUE};
// NOLINTNEXTLINE(readability-magic-numbers)
SDL_Color color_p1 {255, 0, 0, SDL_ALPHA_OPAQUE};
// NOLINTNEXTLINE(readability-magic-numbers)
SDL_Color color_p2 {0, 0, 255, SDL_ALPHA_OPAQUE};
BoardWidget(Widget* parent = nullptr);
~BoardWidget() override;
[[nodiscard]] auto board_coord_from_input(const SDL_Point&) const -> SDL_Point;
[[nodiscard]] auto size() const noexcept -> bwidgets::Size override;
};
}
#endif

View File

@ -1,10 +0,0 @@
#include <utility>
#include "data.hpp"
using namespace game::data;
Board::Board(int w, int h) : width(w), height(h)
{
squares.resize((std::vector<Square>::size_type)w * h, {});
}

View File

@ -1,64 +0,0 @@
#ifndef DATA_HPP
#define DATA_HPP
#include <vector>
namespace game::data
{
enum struct Player
{
NONE,
P1,
P2
};
struct Square
{
Player owner = Player::NONE;
int value = 0;
static inline void reset(Square& s)
{
s.owner = Player::NONE;
s.value = 0;
}
};
struct Coordinate
{
int x;
int y;
};
struct Board
{
std::vector<Square> squares;
int width;
int height;
Board(int w, int h);
[[nodiscard]] inline auto is_corner(const Coordinate& c) const -> bool
{
return (c.x == 0 && c.y == 0) || (c.x == 0 && c.y == height - 1)
|| (c.x == width - 1 && c.y == 0)
|| (c.x == width - 1 && c.y == height - 1);
}
[[nodiscard]] inline auto is_side(const Coordinate& c) const -> bool
{
return c.x == 0 || c.x == width - 1 || c.y == 0 || c.y == height - 1;
}
static inline auto square(const Board& b, const Coordinate& c) -> const Square&
{
return square(const_cast<Board&>(b), c);
}
static inline auto square(Board& b, const Coordinate c) -> Square&
{
return b.squares.at(c.y * b.width + c.x);
}
};
using Boards = std::vector<Board>;
}
#endif

View File

@ -1,27 +0,0 @@
#include <basic_widgets/core/texture.hpp>
#include "fading_caption.hpp"
using namespace game;
auto ui::FadingCaption::show() -> ui::FadingCaption*
{
_start_time = SDL_GetTicks();
_shown = true;
return this;
}
void ui::FadingCaption::_handle_rendering()
{
if (!_shown) return;
if (_text_texture == nullptr) _handle_texture_update();
if (SDL_GetTicks() - _start_time < duration) {
uint8_t alpha = SDL_ALPHA_OPAQUE * visibility();
_text_texture->blend_mode(SDL_BLENDMODE_BLEND);
_text_texture->alpha_mode(alpha);
Caption::_handle_rendering();
}
else _shown = false;
}

View File

@ -1,31 +0,0 @@
#ifndef FADING_CAPTION_HPP
#define FADING_CAPTION_HPP
#include <algorithm>
#include <basic_widgets/w/caption.hpp>
namespace game::ui
{
class FadingCaption final : public bwidgets::Caption
{
bool _shown {};
uint32_t _start_time;
void _handle_rendering() override;
public:
uint32_t duration = 2500U; // NOLINT(readability-magic-numbers)
auto show() -> FadingCaption*;
[[nodiscard]] inline auto visibility() const noexcept -> float
{
if (!_shown) return 0;
auto progress = (float)std::clamp(SDL_GetTicks() - _start_time, 0U, duration)
/ (float)duration;
return 1 - progress * progress * progress * progress;
}
};
}
#endif

View File

@ -1,77 +0,0 @@
#include "game.hpp"
using namespace game;
using namespace game::data;
auto game::logic::play(const Board& b, const Coordinate& c, const Player p) -> Boards
{
auto new_state(b);
auto& square = Board::square(new_state, c);
Boards states;
if (square.owner != Player::NONE && p != square.owner) throw error::invalid_move();
square.owner = p;
square.value++;
states = spread(new_state);
return states;
}
auto game::logic::spread(const data::Board& b) noexcept -> Boards
{
Boards states;
Board current = b;
int overflows;
do {
overflows = 0;
auto new_state = current;
for (auto y = 0; y < current.height; y++) {
for (auto x = 0; x < current.width; x++) {
auto sqr = Board::square(current, {x, y});
if (sqr.value > square_capacity(current, {x, y})) {
overflows++;
Square::reset(Board::square(new_state, {x, y}));
states.push_back(new_state);
// NOLINTNEXTLINE(modernize-avoid-c-arrays)
for (data::Coordinate neighboors[4] {
{ x, y + 1},
{x + 1, y},
{ x, y - 1},
{x - 1, y}
};
const data::Coordinate& c : neighboors)
{
if (c.x >= 0 && c.y >= 0 && c.x < current.width
&& c.y < current.height) {
auto& adj_sqr = Board::square(new_state, c);
adj_sqr.value++;
adj_sqr.owner = sqr.owner;
}
}
}
}
}
states.push_back(new_state);
current = new_state;
} while (overflows > 0 && winner(current) == Player::NONE);
return states;
}
auto game::logic::winner(const Board& b) noexcept -> Player
{
auto winner {Player::NONE};
for (const auto& s : b.squares) {
if (winner == Player::NONE) {
if (s.owner != Player::NONE) {
winner = s.owner;
}
}
else if (s.owner != Player::NONE && winner != s.owner) {
return Player::NONE;
}
}
return winner;
}

View File

@ -1,29 +0,0 @@
#ifndef GAME_HPP
#define GAME_HPP
#include <exception>
#include "data.hpp"
namespace game::error
{
struct invalid_move : std::exception
{};
}
namespace game::logic
{
auto play(const data::Board&, const data::Coordinate&, data::Player) -> data::Boards;
auto spread(const data::Board&) noexcept -> data::Boards;
auto winner(const data::Board&) noexcept -> data::Player;
static inline auto square_capacity(const data::Board& b,
const data::Coordinate& c) noexcept -> int
{
if (b.is_corner(c)) return 1;
if (b.is_side(c)) return 2;
return 3;
}
}
#endif

View File

@ -1,133 +0,0 @@
#include <SDL2/SDL_render.h>
#include <basic_widgets/core/math.hpp>
#include "game_screen.hpp"
using namespace bwidgets;
using namespace game;
using namespace game::data;
// TODO: Why don't I use a ptr to current caption?
static inline auto cur_caption(ui::GameScreen* scr) -> auto&
{
switch (scr->cur_player) {
case Player::P1:
return scr->caption_p1;
case Player::P2:
return scr->caption_p2;
default:
return scr->caption_none;
}
}
ui::GameScreen::GameScreen()
: _menu(this), board(this), caption_none(this), caption_p1(this), caption_p2(this)
{
caption_p1.text("Player 1")->font_color_bg(board.color_outline);
caption_p2.text("Player 2")->font_color_bg(board.color_outline);
caption_notification.render_mode(Font::RenderMode::BLENDED)->alignment =
Caption::Alignment::CENTER;
}
ui::GameScreen::~GameScreen()
{
discard(_font_big, _font_medium);
}
auto ui::GameScreen::font(const std::string& fname) -> ui::GameScreen*
{
_font_file = Font::find(fname);
return this;
}
auto ui::GameScreen::handle_event(const SDL_Event& e) -> ui::GameScreen*
{
if (e.type == SDL_KEYUP && e.key.keysym.sym == SDLK_ESCAPE)
_menu.shown = !_menu.shown;
if (!_menu.shown) board.handle_event(e);
else _menu.handle_event(e);
return this;
}
auto ui::GameScreen::size() const noexcept -> Size
{
return {board.size().w,
board.size().h + cur_caption(const_cast<GameScreen*>(this)).size().h};
}
void ui::GameScreen::_handle_geometry_change(const SDL_Rect& vp) noexcept
{
SDL_assert_release(!_font_file.empty()); // NOLINT
_widget_area = {0, 0, vp.w, vp.h};
auto old_vp_smallest = _viewport.w < _viewport.h ? _viewport.w : _viewport.h;
auto new_vp_smallest = vp.w < vp.h ? vp.w : vp.h;
// NOLINTNEXTLINE(readability-magic-numbers)
if (old_vp_smallest / (12) != new_vp_smallest / (12))
// NOLINTNEXTLINE(readability-magic-numbers)
_load_font(new_vp_smallest / 24);
int heighest_caption = 0;
for (auto step = 0; step < 2; step++) {
for (auto* c : {&caption_none, &caption_p1, &caption_p2}) {
auto size = c->size();
if (step == 0) {
if (size.h > heighest_caption) heighest_caption = size.h;
}
else {
c->viewport(
{vp.x + (vp.w - size.w) / 2, vp.y, size.w, heighest_caption});
}
}
}
board.viewport({vp.x, vp.y + heighest_caption, vp.w, vp.h - heighest_caption});
_menu.viewport(vp);
caption_notification.viewport(
{vp.x, vp.y + center_line(vp.h, caption_notification.size().h), vp.w,
caption_notification.size().h});
}
void ui::GameScreen::_handle_renderer_change(Renderer* r)
{
for (auto* c :
{(Widget*)&board, (Widget*)&caption_none, (Widget*)&caption_p1,
(Widget*)&caption_p2, (Widget*)&caption_notification, (Widget*)&_menu})
c->renderer(r);
}
void ui::GameScreen::_handle_rendering()
{
_renderer->draw_color(board.color_outline)->fill_rect(_widget_area);
cur_caption(this).render();
board.render();
_renderer->viewport(nullptr);
auto notif_visibility = caption_notification.visibility();
if (notif_visibility > 0) {
// NOLINTNEXTLINE(readability-magic-numbers)
_renderer
// NOLINTNEXTLINE(readability-magic-numbers)
->draw_color({200, 200, 200, (uint8_t)(200 * notif_visibility)})
// NOLINTNEXTLINE(readability-magic-numbers)
->fill_rect({_viewport.x, caption_notification.viewport().y - _viewport.h / 16,
_viewport.w,
// NOLINTNEXTLINE(readability-magic-numbers)
caption_notification.viewport().h + _viewport.h / 8});
caption_notification.render();
}
_menu.render();
}
void ui::GameScreen::_load_font(int size)
{
discard(_font_big, _font_medium);
_font_big = new Font(_font_file, size * 2);
_font_medium = new Font(_font_file, size);
for (auto* c : {&caption_none, &caption_p1, &caption_p2}) c->font(_font_medium);
_menu.font(_font_medium);
caption_notification.font(_font_big);
}

View File

@ -1,51 +0,0 @@
#ifndef GAME_SCREEN_HPP
#define GAME_SCREEN_HPP
#include "board_widget.hpp"
#include "data.hpp"
#include "fading_caption.hpp"
#include "menu.hpp"
#include <unordered_map>
struct SDL_Renderer;
namespace game::ui
{
class GameScreen final : public bwidgets::Widget
{
bwidgets::Font* _font_big {nullptr};
std::string _font_file;
bwidgets::Font* _font_medium {nullptr};
Menu _menu;
void _load_font(int);
void _handle_geometry_change(const SDL_Rect&) noexcept override;
void _handle_renderer_change(bwidgets::Renderer*) override;
void _handle_rendering() override;
public:
BoardWidget board;
bwidgets::Caption caption_none;
FadingCaption caption_notification;
bwidgets::Caption caption_p1;
bwidgets::Caption caption_p2;
data::Player cur_player {data::Player::P1};
GameScreen();
~GameScreen() override;
auto font(const std::string&) -> GameScreen*;
auto handle_event(const SDL_Event&) -> GameScreen* override;
[[nodiscard]] auto size() const noexcept -> bwidgets::Size override;
[[nodiscard]] inline auto settings_event() const
{
return _menu.settings_event;
}
};
}
#endif

142
main.cpp
View File

@ -1,142 +0,0 @@
#include <iostream>
#include <SDL2/SDL_ttf.h>
#include <basic_widgets/core/font.hpp>
#include "game.hpp"
#include "game_screen.hpp"
#include "settings.hpp"
using namespace bwidgets;
using namespace game;
auto main() -> int
{
const int width = 854;
const int height = 480;
SDL_Init(SDL_INIT_VIDEO);
SDL_Window* win =
SDL_CreateWindow("ChainReact", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
width, height, SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE);
auto* rend = new Renderer(win, -1, SDL_RENDERER_ACCELERATED);
rend->blend_mode(SDL_BLENDMODE_BLEND);
SDL_Rect viewport {0, 0, width, height};
TTF_Init();
data::Board board(4, 3);
data::Board transition_board(board);
auto* game_screen = new ui::GameScreen();
game_screen->board.board = &board;
game_screen->font("Monospace");
game_screen->renderer(rend);
game_screen->viewport(viewport);
bool quit = false;
auto player = data::Player::P1;
auto round = 1;
game::data::Boards transitions;
auto transitions_timer = SDL_GetTicks();
size_t transitions_iterator = 0;
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) {
auto s = rend->output_size();
viewport.w = s.w;
viewport.h = s.h;
game_screen->viewport(viewport);
}
break;
}
}
if (ev.type == game_screen->board.event_type) {
switch ((ui::BoardWidget::event_code)ev.user.code) {
case ui::BoardWidget::event_code::PLAY: {
auto* const coord = (SDL_Point*)ev.user.data2;
const auto can_play =
game_screen->caption_notification.visibility() == 0
&& (round <= 2
|| (transitions.empty()
&& game::logic::winner(board)
== game::data::Player::NONE));
if (!can_play) {
delete coord;
break;
}
try {
transitions =
game::logic::play(board, {coord->x, coord->y}, player);
game_screen->cur_player = player = player == data::Player::P1
? data::Player::P2
: data::Player::P1;
round++;
} catch (const game::error::invalid_move&) {
game_screen->caption_notification.show()->text(
"Invalid move!");
}
delete coord;
break;
}
}
}
else if (ev.type == game_screen->settings_event()) {
auto* s = (Settings*)ev.user.data1;
board = data::Board(s->board_size.w, s->board_size.h);
game_screen->board.board = &board;
round = 0;
delete s;
}
game_screen->handle_event(ev);
}
rend->draw_color({0, 0, 0, SDL_ALPHA_OPAQUE})->clear();
if (transitions_iterator < transitions.size()) {
// NOLINTNEXTLINE(readability-magic-numbers)
if (auto t = SDL_GetTicks(); t - transitions_timer > 400) {
game_screen->board.board = &transitions[transitions_iterator];
transitions_iterator++;
transitions_timer = t;
}
}
else if (transitions_iterator > 0 && transitions_iterator == transitions.size())
{
board = transitions.back();
game_screen->board.board = &board;
transitions_iterator = 0;
transitions.clear();
if (auto winner = game::logic::winner(board);
round > 2 && winner != game::data::Player::NONE)
{
if (winner == game::data::Player::P1)
game_screen->caption_notification.show()->text("Player 1 has won!");
else game_screen->caption_notification.show()->text("Player 2 has won!");
}
}
game_screen->render();
rend->present();
}
delete game_screen;
delete rend;
TTF_Quit();
SDL_DestroyWindow(win);
SDL_Quit();
return 0;
}

101
menu.cpp
View File

@ -1,101 +0,0 @@
#include <basic_widgets/core/math.hpp>
#include <basic_widgets/w/caption.hpp>
#include <basic_widgets/w/numeric_input.hpp>
#include "menu.hpp"
#include "settings.hpp"
using namespace bwidgets;
using namespace game;
ui::Menu::Menu(Widget* parent)
: Widget(parent), _menu(AlignedLayout::Alignment::VERTICAL)
{
auto* caption = new Caption();
auto* layout = new AlignedLayout(AlignedLayout::Alignment::HORIZONTAL);
auto* input_col {new NumericInput<int>()};
// NOLINTNEXTLINE(readability-magic-numbers)
input_col->value_range(3, 10)->input_min_width = 2;
// NOLINTNEXTLINE(readability-magic-numbers)
caption->font_color_fg({200, 200, 200, 255});
caption->render_mode(Font::RenderMode::BLENDED)->text("Columns: ");
layout->add_widget(caption)->add_widget(input_col);
_menu.add_widget(layout);
caption = new Caption();
auto* input_row = new NumericInput<int>();
layout = new AlignedLayout(AlignedLayout::Alignment::HORIZONTAL);
// NOLINTNEXTLINE(readability-magic-numbers)
input_row->value_range(3, 10)->input_min_width = 2;
// NOLINTNEXTLINE(readability-magic-numbers)
caption->font_color_fg({200, 200, 200, 255});
caption->render_mode(Font::RenderMode::BLENDED)->text("Rows: ");
layout->add_widget(caption)->add_widget(input_row);
_menu.add_widget(layout);
auto* btn_ok = new Button();
btn_ok->text("Ok");
btn_ok->click_handler = [this, input_col, input_row](const SDL_MouseButtonEvent&) {
_push_settings(new Settings({
{input_col->value, input_row->value}
}));
this->shown = false;
};
_menu.add_widget(btn_ok);
}
auto ui::Menu::handle_event(const SDL_Event& ev) -> ui::Menu*
{
_menu.handle_event(ev);
return this;
}
auto ui::Menu::size() const noexcept -> Size
{
return _menu.size();
}
void ui::Menu::_handle_font_change(Font* f)
{
auto apply_font = [f](Widget* w) {
if (auto* handler = dynamic_cast<FontHandler*>(w); handler) handler->font(f);
};
_menu.for_widgets([apply_font](Widget* w) {
if (auto* layout = dynamic_cast<Layout*>(w); layout)
layout->for_widgets(apply_font);
else if (auto* handler = dynamic_cast<FontHandler*>(w); handler) apply_font(w);
});
}
void ui::Menu::_handle_geometry_change(const SDL_Rect& vp) noexcept
{
auto size = _menu.size();
_widget_area = {center_line(vp.w, size.w), center_line(vp.h, size.h), size.w,
size.h};
_menu.viewport(rect_offset(_widget_area, _viewport));
}
void ui::Menu::_handle_renderer_change(Renderer* r)
{
_menu.renderer(r);
}
void ui::Menu::_handle_rendering()
{
if (shown) {
// NOLINTNEXTLINE(readability-magic-numbers)
_renderer->draw_color({0, 0, 0, 185})->fill_rect(nullptr);
_menu.render();
}
}
void ui::Menu::_push_settings(Settings* s) const
{
SDL_Event ev;
SDL_zero(ev);
ev.type = settings_event;
ev.user.data1 = s;
SDL_PushEvent(&ev);
}

View File

@ -1,42 +0,0 @@
#ifndef MENU_HPP
#define MENU_HPP
#include <SDL2/SDL_events.h>
#include <basic_widgets/w/aligned_layout.hpp>
#include <basic_widgets/w/feat/font_handler.hpp>
namespace game
{
struct Settings;
}
namespace game::ui
{
class Menu final : public bwidgets::Widget,
public bwidgets::FontHandler
{
bwidgets::AlignedLayout _menu;
void _push_settings(Settings*) const;
void _handle_font_change(bwidgets::Font*) override;
void _handle_font_color_change(const bwidgets::Color&,
const bwidgets::Color&) override
{}
void _handle_geometry_change(const SDL_Rect&) noexcept override;
void _handle_renderer_change(bwidgets::Renderer*) override;
void _handle_rendering() override;
public:
const uint32_t settings_event {SDL_RegisterEvents(1)};
bool shown {};
Menu(Widget* parent = nullptr);
auto handle_event(const SDL_Event&) -> Menu* override;
[[nodiscard]] auto size() const noexcept -> bwidgets::Size override;
};
}
#endif

View File

@ -12,18 +12,21 @@ project('chainreact', 'cpp',
add_project_arguments('-pedantic', language: 'cpp')
compiler = meson.get_compiler('cpp').get_id()
if compiler == 'clang++'
add_project_arguments('-fcoroutines-ts', language: 'cpp')
elif compiler == 'g++'
add_project_arguments('-fcoroutines', language: 'cpp')
endif
executable('chainreact',
'board_widget.cpp',
'data.cpp',
'fading_caption.cpp',
'game.cpp',
'game_screen.cpp',
'main.cpp',
'menu.cpp',
'src/core/board2d.cpp',
'src/tui/tui.cpp',
'src/tui/main.cpp',
dependencies : [
dependency('basic_widgets',
static : true,
)
),
],
install : true,
win_subsystem : 'windows')

View File

@ -1,16 +0,0 @@
#ifndef SETTINGS_HPP
#define SETTINGS_HPP
#include <SDL2/SDL_events.h>
#include <basic_widgets/core/type/size.hpp>
namespace game
{
struct Settings
{
bwidgets::Size board_size {4, 3};
};
}
#endif

BIN
src/core/a.out Executable file

Binary file not shown.

151
src/core/board2d.cpp Normal file
View File

@ -0,0 +1,151 @@
#include <cstdio>
#include <iostream>
#include "board2d.hpp"
using namespace core;
Board2D::Board2D(const size_type w, const size_type h)
{
init(w, h);
}
void Board2D::add_unit(const Coord c)
{
std::cerr << "add_unit " << c.x << "," << c.y << std::endl;
auto& space = at(c);
if (!(space.capacity >= space.value)) throw InvalidMove {"Space is overloaded"};
space.value++;
}
void Board2D::capture(const Coord c, const Player p) noexcept
{
std::cerr << static_cast<int>(p) << " captures " << c.x << "," << c.y << std::endl;
at(c).owner = p;
}
void Board2D::init(const size_type w, const size_type h)
{
_width = w;
_height = h;
_spaces.reserve(w * h); // NOLINT
for (size_type y = 0; y < h; y++) {
for (size_type x = 0; x < w; x++) {
_spaces.push_back(Coord(x, y));
if (x == 0 || x == w - 1) {
if (y == 0 || y == h - 1) {
_spaces.back().capacity = 1;
}
else {
_spaces.back().capacity = 2;
}
}
else {
if (y == 0 || y == h - 1) {
_spaces.back().capacity = 2;
}
else {
_spaces.back().capacity = 3;
}
}
}
}
}
auto Board2D::move(const Coord c, const Player p) -> Generator<Board2D>
{
const auto& space = at(c);
if (space.owner == Player::NONE) capture(c, p);
else if (space.owner != p) throw InvalidMove {"Player is not the Space owner"};
add_unit(c);
co_yield *this;
std::vector<Coord> overloaded;
if (is_overloaded(c)) {
overloaded.push_back(c);
do {
const auto src = overloaded.back();
const auto targets = neighbours(src);
overloaded.pop_back();
auto spreader = spread(src, targets, p);
while (spreader) co_yield spreader();
for (const auto t : targets) {
if (is_overloaded(t)) {
std::cerr << "overloaded: " << t.x << "," << t.y << std::endl;
overloaded.push_back(t);
};
}
} while (!overloaded.empty());
}
}
auto Board2D::neighbours(const Coord c) const noexcept -> std::vector<Coord>
{
std::vector<Coord> nghbrs;
if (c.x > 0) nghbrs.push_back({c.x - 1, c.y});
if (c.x < _width - 1) nghbrs.push_back({c.x + 1, c.y});
if (c.y > 0) nghbrs.push_back({c.x, c.y - 1});
if (c.y < _height - 1) nghbrs.push_back({c.x, c.y + 1});
return nghbrs;
}
auto Board2D::is_overloaded(Coord c) const noexcept -> bool
{
const auto& s = at(c);
return s.value > s.capacity;
}
auto Board2D::size() const noexcept -> std::array<size_type, 2>
{
return {_width, _height};
}
auto Board2D::spread(const Coord src, const std::vector<Coord>& targets, Player p)
-> Generator<Board2D>
{
auto& s = at(src);
std::cerr << "spread " << src.x << "," << src.y << " " << s.value << "/"
<< s.capacity << std::endl;
{
const auto count = targets.size();
if (s.value != static_cast<int>(count)) {
throw std::logic_error("cannot spread: unit count and target "
"spaces count differ");
}
}
s.reset();
co_yield *this;
for (const auto& c : targets) {
capture(c, p);
add_unit(c);
co_yield *this;
}
}
auto Board2D::operator()(const Coord c) const noexcept -> const Board2D::Space&
{
return _spaces.at(coord2idx(c));
}
auto Board2D::at(const Coord c) noexcept -> Space&
{
return _spaces.at(coord2idx(c));
}
auto Board2D::at(const Coord c) const noexcept -> const Space&
{
return _spaces.at(coord2idx(c));
}
auto Board2D::coord2idx(const Coord c) const noexcept -> size_type
{
return c.y * _width + c.x;
}

74
src/core/board2d.hpp Normal file
View File

@ -0,0 +1,74 @@
#ifndef CORE_BOARD2D_HPP
#define CORE_BOARD2D_HPP
#include <array>
#include <stdexcept>
#include <vector>
#include "generator.hpp"
#include "player.hpp"
namespace core
{
class Board2D
{
public:
struct Space;
using Spaces = std::vector<Space>;
using size_type = Spaces::size_type;
struct Coord
{
size_type x;
size_type y;
Coord(size_t x_, size_t y_) : x {x_}, y {y_} {}
};
struct Space
{
int capacity {};
Coord coord;
Player owner {Player::NONE};
int value {0};
Space(Coord c) : coord {c} {}
inline void reset() noexcept
{
owner = Player::NONE;
value = 0;
}
};
struct InvalidMove : public std::logic_error
{
using std::logic_error::logic_error;
};
Board2D() = default;
Board2D(size_type, size_type);
void add_unit(Coord);
void capture(Coord, Player) noexcept;
void init(size_type, size_type);
auto move(Coord, Player) -> Generator<Board2D>;
[[nodiscard]] auto neighbours(Coord) const noexcept -> std::vector<Coord>;
[[nodiscard]] auto operator()(Coord) const noexcept -> const Space&;
[[nodiscard]] auto size() const noexcept -> std::array<size_type, 2>;
auto spread(Coord, const std::vector<Coord>&, Player) -> Generator<Board2D>;
[[nodiscard]] auto is_overloaded(Coord) const noexcept -> bool;
private:
size_type _height;
Spaces _spaces;
size_type _width;
[[nodiscard]] auto at(Coord) noexcept -> Space&;
[[nodiscard]] auto at(Coord) const noexcept -> const Space&;
[[nodiscard]] auto coord2idx(Coord) const noexcept -> size_type;
};
}
#endif

66
src/core/generator.hpp Normal file
View File

@ -0,0 +1,66 @@
// https://www.scs.stanford.edu/~dm/blog/c++-coroutines.html#generic-generator-example
#include <exception>
#include <utility>
#ifdef __clang__
#include <experimental/coroutine>
namespace stdco = std::experimental;
#else
#include <coroutine>
namespace stdco = std;
#endif
template<typename T>
struct Generator
{
struct promise_type;
using handle_type = stdco::coroutine_handle<promise_type>;
struct promise_type
{
T value_;
std::exception_ptr exception_;
Generator get_return_object()
{
return Generator(handle_type::from_promise(*this));
}
stdco::suspend_always initial_suspend() { return {}; }
stdco::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { exception_ = std::current_exception(); }
template<std::convertible_to<T> From> // C++20 concept
stdco::suspend_always yield_value(From&& from)
{
value_ = std::forward<From>(from);
return {};
}
void return_void() {}
};
handle_type h_;
Generator(handle_type h) : h_(h) {}
~Generator() { h_.destroy(); }
explicit operator bool()
{
fill();
return !h_.done();
}
T operator()()
{
fill();
full_ = false;
return std::move(h_.promise().value_);
}
private:
bool full_ = false;
void fill()
{
if (!full_) {
h_();
if (h_.promise().exception_) std::rethrow_exception(h_.promise().exception_);
full_ = true;
}
}
};

BIN
src/core/generator.hpp.gch Normal file

Binary file not shown.

14
src/core/player.hpp Normal file
View File

@ -0,0 +1,14 @@
#ifndef CORE_PLAYER_HPP
#define CORE_PLAYER_HPP
namespace core
{
enum struct Player
{
NONE,
P1,
P2
};
}
#endif

30
src/tui/main.cpp Normal file
View File

@ -0,0 +1,30 @@
#include <cstdlib>
#include <iostream>
#include "tui.hpp"
using namespace core;
using namespace std;
using namespace tui;
int main()
{
Board2D b {3, 3};
Player p {Player::P1};
do {
try {
while (true) {
cout << "Player " << static_cast<int>(p) << endl;
const auto opt = move_ask();
const auto coord = opt.value();
play_move(&b, coord, p, [](auto state) { board_print(state); });
p = p == Player::P1 ? Player::P2 : Player::P1;
}
break;
} catch (const Board2D::InvalidMove& e) {
cerr << "Invalid move!" << std::endl;
}
} while (true);
return EXIT_SUCCESS;
}

50
src/tui/tui.cpp Normal file
View File

@ -0,0 +1,50 @@
#include <charconv>
#include <iostream>
#include <optional>
#include <string>
#include "tui.hpp"
#include <system_error>
using namespace core;
using namespace std;
using namespace tui;
void tui::board_print(const Board2D& b)
{
auto [w, h] = b.size();
for (Board2D::size_type y = 0; y < h; y++) {
for (Board2D::size_type x = 0; x < w; x++) {
const auto& space = b({x, y});
cout << "|" << static_cast<int>(space.owner) << ":" << space.value;
}
cout << endl;
}
for (Board2D::size_type x = 0; x < w; x++) cout << "-";
cout << endl;
}
auto tui::move_ask() -> optional<Board2D::Coord>
{
const auto success_code = std::errc();
std::from_chars_result result;
Board2D::size_type x, y;
string x_input, y_input;
cout << "Play move:" << endl << "\tx: ";
cin >> x_input;
result = std::from_chars(x_input.c_str(), x_input.c_str() + x_input.size(), x);
if (result.ec != success_code) goto failure;
cout << "\ty: ";
cin >> y_input;
result = std::from_chars(y_input.c_str(), y_input.c_str() + y_input.size(), y);
if (result.ec != success_code) goto failure;
return make_optional(Board2D::Coord {x, y});
failure:
return nullopt;
}

26
src/tui/tui.hpp Normal file
View File

@ -0,0 +1,26 @@
#include <functional>
#include <optional>
#include "../core/board2d.hpp"
#include "../core/player.hpp"
namespace tui
{
using namespace core;
template<typename Lambda>
concept PlayCB = std::is_convertible_v<Lambda, std::function<void(const Board2D&)>>;
void board_print(const Board2D&);
auto move_ask() -> std::optional<Board2D::Coord>;
template<PlayCB Cb>
void play_move(Board2D* b, Board2D::Coord c, Player p, Cb cb)
{
auto move_stepper = b->move(c, p);
while (move_stepper) {
auto new_state = move_stepper();
cb(new_state);
}
}
}

View File

@ -1,6 +1,6 @@
[wrap-git]
url = https://github.com/soratobukuroneko/sdl2_basic_widgets.git
revision = prototype01
revision = 0.9.0
[provide]
basic_widgets = libbasic_widgets_dep