diff --git a/Makefile b/Makefile index a7354d5..562ac6d 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ tracy_build: meson compile -j 10 -C builddir test: build - ./builddir/runtests "[animation]" + ./builddir/runtests run: build test powershell "cp ./builddir/zedcaster.exe ." diff --git a/assets/config.json b/assets/config.json index 573cc22..79613ff 100644 --- a/assets/config.json +++ b/assets/config.json @@ -53,7 +53,8 @@ "tunnel_with_rocks": "assets/tunnel_with_rocks.png", "tunnel_with_rocks_stage": "assets/tunnel_with_rocks_stage.png", "ritual_crafting_area": "assets/ritual_crafting_area.png", - "the_ritual_circle": "assets/the_ritual_circle.png" + "the_ritual_circle": "assets/the_ritual_circle.png", + "paper_ui_background": "assets/paper_ui_background.png" }, "worldgen": { "enemy_probability": 50, diff --git a/assets/paper_ui_background.png b/assets/paper_ui_background.png new file mode 100644 index 0000000..13013f7 Binary files /dev/null and b/assets/paper_ui_background.png differ diff --git a/assets/tiles.json b/assets/tiles.json index 969b072..a1528a8 100644 --- a/assets/tiles.json +++ b/assets/tiles.json @@ -4,20 +4,20 @@ "foreground": [40, 15, 125], "background": [200, 15, 75], "collision": false, - "display":"\u289e" + "display":"." }, "WALL_PLAIN": { "texture": "assets/wall_texture_test-256.png", "foreground": [230, 20, 30], "background": [230, 20, 120], "collision": true, - "display": "\ua5b8" + "display": "#" }, "WALL_VINES": { "texture": "assets/wall_with_vines-256.png", "foreground": [230, 20, 30], "background": [230, 20, 120], "collision": false, - "display":"\u0799" + "display":"|" } } diff --git a/constants.hpp b/constants.hpp index b7331dd..ae80208 100644 --- a/constants.hpp +++ b/constants.hpp @@ -26,6 +26,7 @@ constexpr const int GUECS_PADDING = 3; constexpr const int GUECS_BORDER_PX = 1; constexpr const int GUECS_FONT_SIZE = 30; const sf::Color GUECS_FILL_COLOR = ColorValue::DARK_MID; +const sf::Color GUECS_TEXT_COLOR = ColorValue::LIGHT_LIGHT; const sf::Color GUECS_BG_COLOR = ColorValue::MID; const sf::Color GUECS_BORDER_COLOR = ColorValue::MID; diff --git a/guecs.cpp b/guecs.cpp index a1360d0..5d9b1b3 100644 --- a/guecs.cpp +++ b/guecs.cpp @@ -64,9 +64,18 @@ namespace guecs { text.init(cell, $font); }); + $world.query([this](auto, auto& cell, auto& text) { + text.init(cell, $font); + }); + + $world.query([this](auto, auto& cell, auto& text) { + text.init(cell, $font); + }); + $world.query([&](auto, auto &cell, auto &sprite) { sprite.init(cell); }); + } void UI::render(sf::RenderWindow& window) { @@ -94,6 +103,14 @@ namespace guecs { window.draw(*text.text); }); + $world.query([&](auto, auto& text) { + window.draw(*text.text); + }); + + $world.query([&](auto, auto& text) { + window.draw(*text.text); + }); + $world.query([&](auto, auto& text) { window.draw(*text.text); }); diff --git a/guecs.hpp b/guecs.hpp index 52bdb45..a077b24 100644 --- a/guecs.hpp +++ b/guecs.hpp @@ -18,12 +18,34 @@ namespace guecs { struct Label { std::string label; unsigned int size = GUECS_FONT_SIZE; + sf::Color color = GUECS_TEXT_COLOR; shared_ptr font = nullptr; shared_ptr text = nullptr; void init(lel::Cell &cell, shared_ptr font_ptr) { + dbc::check(font_ptr != nullptr, "you failed to initialize this Label"); if(font == nullptr) font = font_ptr; if(text == nullptr) text = make_shared(*font, label, size); + text->setFillColor(color); + auto bounds = text->getLocalBounds(); + auto text_cell = lel::center(bounds.size.x, bounds.size.y, cell); + // this stupid / 2 is because SFML renders from baseline rather than from the claimed bounding box + text->setPosition({float(text_cell.x), float(text_cell.y) - text_cell.h / 2}); + } + }; + + struct WideLabel { + std::wstring label; + unsigned int size = GUECS_FONT_SIZE; + sf::Color color = GUECS_TEXT_COLOR; + shared_ptr font = nullptr; + shared_ptr text = nullptr; + + void init(lel::Cell &cell, shared_ptr font_ptr) { + dbc::check(font_ptr != nullptr, "you failed to initialize this WideLabel"); + if(font == nullptr) font = font_ptr; + if(text == nullptr) text = make_shared(*font, label, size); + text->setFillColor(color); auto bounds = text->getLocalBounds(); auto text_cell = lel::center(bounds.size.x, bounds.size.y, cell); // this stupid / 2 is because SFML renders from baseline rather than from the claimed bounding box @@ -34,13 +56,17 @@ namespace guecs { struct Textual { std::string content; unsigned int size = GUECS_FONT_SIZE; + sf::Color color = GUECS_TEXT_COLOR; + int padding = GUECS_PADDING; shared_ptr font = nullptr; shared_ptr text = nullptr; void init(lel::Cell &cell, shared_ptr font_ptr) { + dbc::check(font_ptr != nullptr, "you failed to initialize this Text"); if(font == nullptr) font = font_ptr; if(text == nullptr) text = make_shared(*font, content, size); - text->setPosition({float(cell.x + GUECS_PADDING * 2), float(cell.y + GUECS_PADDING * 2)}); + text->setFillColor(color); + text->setPosition({float(cell.x + padding * 2), float(cell.y + padding * 2)}); text->setCharacterSize(size); } @@ -50,6 +76,29 @@ namespace guecs { } }; + struct WideText { + std::wstring content; + unsigned int size = GUECS_FONT_SIZE; + sf::Color color = GUECS_TEXT_COLOR; + int padding = GUECS_PADDING; + shared_ptr font = nullptr; + shared_ptr text = nullptr; + + void init(lel::Cell &cell, shared_ptr font_ptr) { + dbc::check(font_ptr != nullptr, "you failed to initialize this WideText"); + if(font == nullptr) font = font_ptr; + if(text == nullptr) text = make_shared(*font, content, size); + text->setFillColor(color); + text->setPosition({float(cell.x + padding * 2), float(cell.y + padding * 2)}); + text->setCharacterSize(size); + } + + void update(std::wstring& new_content) { + content = new_content; + text->setString(content); + } + }; + struct Clickable { /* This is actually called by UI::mouse and passed the entity ID of the * button pressed so you can interact with it in the event handler. @@ -59,6 +108,7 @@ namespace guecs { struct Sprite { std::string name; + int padding = GUECS_PADDING; std::shared_ptr sprite = nullptr; std::shared_ptr texture = nullptr; @@ -67,26 +117,30 @@ namespace guecs { texture = sprite_texture.texture; sprite = make_shared(*texture); sprite->setPosition({ - float(cell.x + GUECS_PADDING), - float(cell.y + GUECS_PADDING)}); + float(cell.x + padding), + float(cell.y + padding)}); auto size = texture->getSize(); sprite->setScale({ - float(cell.w - GUECS_PADDING * 2) / size.x, - float(cell.h - GUECS_PADDING * 2) / size.y}); + float(cell.w - padding * 2) / size.x, + float(cell.h - padding * 2) / size.y}); } }; struct Rectangle { + int padding = GUECS_PADDING; + sf::Color color = GUECS_FILL_COLOR; + sf::Color border_color = GUECS_BORDER_COLOR; + int border_px = GUECS_BORDER_PX; shared_ptr shape = nullptr; void init(lel::Cell& cell) { - sf::Vector2f size{float(cell.w) - GUECS_PADDING * 2, float(cell.h) - GUECS_PADDING * 2}; + sf::Vector2f size{float(cell.w) - padding * 2, float(cell.h) - padding * 2}; if(shape == nullptr) shape = make_shared(size); - shape->setPosition({float(cell.x + GUECS_PADDING), float(cell.y + GUECS_PADDING)}); - shape->setFillColor(GUECS_FILL_COLOR); - shape->setOutlineColor(GUECS_BORDER_COLOR); - shape->setOutlineThickness(GUECS_BORDER_PX); + shape->setPosition({float(cell.x + padding), float(cell.y + padding)}); + shape->setFillColor(color); + shape->setOutlineColor(border_color); + shape->setOutlineThickness(border_px); } }; @@ -112,6 +166,7 @@ namespace guecs { float y = 0.0f; float w = 0.0f; float h = 0.0f; + sf::Color color = GUECS_BG_COLOR; shared_ptr shape = nullptr; @@ -128,7 +183,7 @@ namespace guecs { sf::Vector2f size{float(w), float(h)}; if(shape == nullptr) shape = make_shared(size); shape->setPosition({float(x), float(y)}); - shape->setFillColor(GUECS_BG_COLOR); + shape->setFillColor(color); } }; diff --git a/gui_fsm.cpp b/gui_fsm.cpp index b3a70f4..599a2ae 100644 --- a/gui_fsm.cpp +++ b/gui_fsm.cpp @@ -16,7 +16,6 @@ namespace gui { $window(sf::VideoMode({SCREEN_WIDTH, SCREEN_HEIGHT}), "Zed's Raycaster Thing"), $main_ui($window), $level($levels.current()), - $map_ui($level), $combat_ui($level), $status_ui($level), $font{FONT_FILE_NAME} @@ -28,7 +27,6 @@ namespace gui { FSM_STATE(State, START, ev); FSM_STATE(State, MOVING, ev); FSM_STATE(State, ATTACKING, ev); - FSM_STATE(State, MAPPING, ev); FSM_STATE(State, ROTATING, ev); FSM_STATE(State, IDLE, ev); FSM_STATE(State, IN_COMBAT, ev); @@ -41,10 +39,7 @@ namespace gui { void FSM::START(Event ) { $main_ui.update_level($level); $level.world->set_the({}); - $main_ui.init(); - $map_ui.init(); - $combat_ui.init(); $status_ui.init(); $status_ui.log("Welcome to the game!"); @@ -57,23 +52,6 @@ namespace gui { state(State::IDLE); } - void FSM::MAPPING(Event ev) { - using enum Event; - // BUG: can't close window when in mapping - switch(ev) { - case MAP_OPEN: - state(State::IDLE); - break; - case CLOSE: - state(State::IDLE); - break; - case TICK: - break; - default: - dbc::log("invalid event sent to MAPPING"); - } - } - void FSM::MOVING(Event ) { // this should be an optional that returns a point if(auto move_to = $main_ui.play_move()) { @@ -148,7 +126,7 @@ namespace gui { state(State::ROTATING); break; case MAP_OPEN: - state(State::MAPPING); + $status_ui.map_open = !$status_ui.map_open; break; case ATTACK: state(State::ATTACKING); @@ -313,16 +291,15 @@ namespace gui { $boss_fight_ui->render($window); } else { $main_ui.render(); + $status_ui.render($window); + $combat_ui.render($window); } } void FSM::render() { - if(in_state(State::MAPPING)) { - $window.clear(); - $map_ui.render($window); - } else if(in_state(State::NEXT_LEVEL)) { + if(in_state(State::NEXT_LEVEL)) { $window.clear(); $boss_fight_ui->render($window); } else { @@ -416,7 +393,6 @@ namespace gui { $status_ui.update_level($level); $combat_ui.update_level($level); - $map_ui.update_level($level); $main_ui.update_level($level); $boss_fight_ui = $levels.create_bossfight($level.world); $boss_fight_ui->init(); diff --git a/gui_fsm.hpp b/gui_fsm.hpp index acff410..d916cec 100644 --- a/gui_fsm.hpp +++ b/gui_fsm.hpp @@ -3,7 +3,6 @@ #include "stats.hpp" #include "levelmanager.hpp" #include "fsm.hpp" -#include "map_view.hpp" #include "main_ui.hpp" #include "combat_ui.hpp" #include "status_ui.hpp" @@ -16,7 +15,6 @@ namespace gui { IN_COMBAT, COMBAT_ROTATE, ATTACKING, - MAPPING, ROTATING, NEXT_LEVEL, IDLE, @@ -50,7 +48,6 @@ namespace gui { MainUI $main_ui; GameLevel $level; shared_ptr $boss_fight_ui = nullptr; - MapViewUI $map_ui; CombatUI $combat_ui; StatusUI $status_ui; sf::Font $font; diff --git a/main.cpp b/main.cpp index f1f434f..02913ce 100644 --- a/main.cpp +++ b/main.cpp @@ -13,7 +13,6 @@ int main(int argc, char* argv[]) { ai::init("assets/ai.json"); animation::init(); - sound::mute(true); gui::FSM main; main.event(gui::Event::STARTED); @@ -31,7 +30,6 @@ int main(int argc, char* argv[]) { // ZED: need to sort out how to deal with this in the FSM if(main.in_state(gui::State::IDLE) || main.in_state(gui::State::NEXT_LEVEL) - || main.in_state(gui::State::MAPPING) || main.in_state(gui::State::IN_COMBAT)) { if(main.autowalking) { diff --git a/map_view.cpp b/map_view.cpp index 2a2766a..229a305 100644 --- a/map_view.cpp +++ b/map_view.cpp @@ -6,64 +6,63 @@ #include "rand.hpp" #include "animation.hpp" #include "rand.hpp" +#include +#include namespace gui { using namespace components; MapViewUI::MapViewUI(GameLevel &level) : - $level(level) + $level(level), $tiles(level.map->width(), level.map->height()) { - $gui.position(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); - $gui.layout( - "[*%(100,900)left|*%(200,900)map_grid| _]" - "[_ | _ | _]" - "[_ | _ | _]" - "[_ | _ | _]" - "[_ | _ | _]" - "[_ | _ | _]" - "[_ | _ | _]" - "[_ | _ | _]" - "[_ | _ | _]" - "[bottom_status_left | bottom_status_right ]"); - - auto cell = $gui.cell_for($gui.entity("map_grid")); - $grid.position(cell.x, cell.y, cell.w, cell.h); - $grid.layout( - "[cell_11|cell_12|cell_13|cell_14|cell_15|cell_16|cell_17]" - "[cell_21|cell_22|cell_23|cell_24|cell_25|cell_26|cell_27]" - "[cell_31|cell_32|cell_33|cell_34|cell_35|cell_36|cell_37]" - "[cell_41|cell_42|cell_43|cell_44|cell_45|cell_46|cell_47]" - "[cell_51|cell_52|cell_53|cell_54|cell_55|cell_56|cell_57]"); } void MapViewUI::update_level(GameLevel &level) { $level = level; } - void MapViewUI::init() { - $gui.world().set_the({$gui.$parser}); - + void MapViewUI::init(int x, int y, int w, int h) { + $gui.position(x, y, w, h); + $gui.layout( + "[*%(100,900)map_grid]" + "[_ ]" + "[_ ]" + "[_ ]" + "[_ ]" + "[_ ]" + "[_ ]" + "[_ ]" + "[_ ]"); for(auto& [name, cell] : $gui.cells()) { auto box = $gui.entity(name); - if(name != "map_grid") { + if(name == "status") { + $gui.set(box, {"paper_ui_background"}); + } else if(name != "map_grid") { $gui.set(box, {}); $gui.set(box, {name}); } } - $gui.init(); + auto grid = $gui.entity("map_grid"); + $gui.set(grid, {L"Loading...", 25, ColorValue::DARK_LIGHT, 20}); + $gui.set(grid, {"paper_ui_background"}); - for(auto& [name, cell] : $grid.cells()) { - auto box = $grid.entity(name); - $grid.set(box, {}); - $grid.set(box, {name}); - } + $gui.init(); - $grid.init(); } void MapViewUI::render(sf::RenderWindow &window) { + $tiles = $level.map->tiles(); + auto grid = $gui.entity("map_grid"); + auto player_pos = $level.world->get($level.player); + + std::string map_out = $tiles.to_string(player_pos.location.x, player_pos.location.y); + std::wstring_convert> converter; + std::wstring map_wstr = converter.from_bytes(map_out); + + auto& map_text = $gui.get(grid); + map_text.update(map_wstr); + $gui.render(window); - $grid.render(window); } } diff --git a/map_view.hpp b/map_view.hpp index cc6791d..c5c8e4a 100644 --- a/map_view.hpp +++ b/map_view.hpp @@ -2,16 +2,17 @@ #include "levelmanager.hpp" #include "textures.hpp" #include "guecs.hpp" +#include "tilemap.hpp" namespace gui { class MapViewUI { public: guecs::UI $gui; - guecs::UI $grid; GameLevel $level; + TileMap $tiles; MapViewUI(GameLevel &level); - void init(); + void init(int x, int y, int w, int h); void render(sf::RenderWindow &window); void update_level(GameLevel &level); }; diff --git a/meson.build b/meson.build index 86af5aa..36255be 100644 --- a/meson.build +++ b/meson.build @@ -1,3 +1,5 @@ +# clang might need _LIBCPP_ENABLE_CXX26_REMOVED_CODECVT + project('raycaster', 'cpp', version: '0.1.0', default_options: [ diff --git a/raycaster.cpp b/raycaster.cpp index 9f80edd..5130029 100644 --- a/raycaster.cpp +++ b/raycaster.cpp @@ -379,6 +379,7 @@ void Raycaster::update_level(GameLevel level) { $sprites.clear(); $level = level; + // BUG: this is way too complex, please make it easier, the issue is that I need to convert the maps to visible tiles and that involves wstring convert, but this is many steps done probably over and over auto& tiles = $level.map->tiles(); $map = textures::convert_char_to_texture(tiles.$tile_ids); diff --git a/raycaster.hpp b/raycaster.hpp index b511986..e6572b5 100644 --- a/raycaster.hpp +++ b/raycaster.hpp @@ -21,7 +21,7 @@ struct Raycaster { double $dir_y = 0; // the 2d raycaster version of camera plane - double $plane_x = 0; + double $plane_x = 0.0; double $plane_y = 0.66; sf::Texture $view_texture; sf::Sprite $view_sprite; diff --git a/ritual_ui.cpp b/ritual_ui.cpp index cba10dd..a4309f0 100644 --- a/ritual_ui.cpp +++ b/ritual_ui.cpp @@ -124,7 +124,9 @@ namespace gui { } } - /* WARNING: This is really not the greatest way to do this. */ + /* WARNING: This is really not the greatest way to do this. + * look in status_ui.update_level() + * */ void RitualUI::update() { dbc::log("RITUAL UPDATE NOT IMPLEMENTED"); } diff --git a/scripts/gcovr_patched_coverage.py b/scripts/gcovr_patched_coverage.py new file mode 100644 index 0000000..7baecca --- /dev/null +++ b/scripts/gcovr_patched_coverage.py @@ -0,0 +1,1020 @@ +# -*- coding:utf-8 -*- + +# ************************** Copyrights and license *************************** +# +# This file is part of gcovr 8.3, a parsing and reporting tool for gcov. +# https://gcovr.com/en/8.3 +# +# _____________________________________________________________________________ +# +# Copyright (c) 2013-2025 the gcovr authors +# Copyright (c) 2013 Sandia Corporation. +# Under the terms of Contract DE-AC04-94AL85000 with Sandia Corporation, +# the U.S. Government retains certain rights in this software. +# +# This software is distributed under the 3-clause BSD License. +# For more information, see the README.rst file. +# +# **************************************************************************** + +""" +The gcovr coverage data model. + +This module represents the core data structures +and should not have dependencies on any other gcovr module, +also not on the gcovr.utils module. + +The data model should contain the exact same information +as the JSON input/output format. + +The types ending with ``*Coverage`` +contain per-project/-line/-decision/-branch coverage. + +The types ``SummarizedStats``, ``CoverageStat``, and ``DecisionCoverageStat`` +report aggregated metrics/percentages. +""" + +from __future__ import annotations +import logging +import os +import re +from typing import ( + ItemsView, + Iterator, + Iterable, + Optional, + TypeVar, + Union, + Literal, + ValuesView, +) +from dataclasses import dataclass + +from .utils import commonpath, force_unix_separator + +LOGGER = logging.getLogger("gcovr") + +_T = TypeVar("_T") + + +def sort_coverage( + covdata: Union[ + dict[str, FileCoverage], + dict[str, Union[FileCoverage, CoverageContainerDirectory]], + ], + sort_key: Literal["filename", "uncovered-number", "uncovered-percent"], + sort_reverse: bool, + by_metric: Literal["line", "branch", "decision"], + filename_uses_relative_pathname: bool = False, +) -> list[str]: + """Sort a coverage dict. + + covdata (dict): the coverage dictionary + sort_key ("filename", "uncovered-number", "uncovered-percent"): the values to sort by + sort_reverse (bool): reverse order if True + by_metric ("line", "branch", "decision"): select the metric to sort + filename_uses_relative_pathname (bool): for html, we break down a pathname to the + relative path, but not for other formats. + + returns: the sorted keys + """ + + basedir = commonpath(list(covdata.keys())) + + def key_filename(key: str) -> list[Union[int, str]]: + def convert_to_int_if_possible(text: str) -> Union[int, str]: + return int(text) if text.isdigit() else text + + key = ( + force_unix_separator( + os.path.relpath(os.path.realpath(key), os.path.realpath(basedir)) + ) + if filename_uses_relative_pathname + else key + ).casefold() + + return [convert_to_int_if_possible(part) for part in re.split(r"([0-9]+)", key)] + + def coverage_stat(key: str) -> CoverageStat: + cov = covdata[key] + if by_metric == "branch": + return cov.branch_coverage() + if by_metric == "decision": + return cov.decision_coverage().to_coverage_stat + return cov.line_coverage() + + def key_num_uncovered(key: str) -> int: + stat = coverage_stat(key) + uncovered = stat.total - stat.covered + return uncovered + + def key_percent_uncovered(key: str) -> float: + stat = coverage_stat(key) + covered = stat.covered + total = stat.total + + # No branches are always put directly after (or before when reversed) + # files with 100% coverage (by assigning such files 110% coverage) + return covered / total if total > 0 else 1.1 + + if sort_key == "uncovered-number": + # First sort filename alphabetical and then by the requested key + return sorted( + sorted(covdata, key=key_filename), + key=key_num_uncovered, + reverse=sort_reverse, + ) + if sort_key == "uncovered-percent": + # First sort filename alphabetical and then by the requested key + return sorted( + sorted(covdata, key=key_filename), + key=key_percent_uncovered, + reverse=sort_reverse, + ) + + # By default, we sort by filename alphabetically + return sorted(covdata, key=key_filename, reverse=sort_reverse) + + +class BranchCoverage: + r"""Represent coverage information about a branch. + + Args: + source_block_id (int): + The block number. + count (int): + Number of times this branch was followed. + fallthrough (bool, optional): + Whether this is a fallthrough branch. False if unknown. + throw (bool, optional): + Whether this is an exception-handling branch. False if unknown. + destination_block_id (int, optional): + The destination block of the branch. None if unknown. + excluded (bool, optional): + Whether the branch is excluded. + """ + + first_undefined_source_block_id: bool = True + + __slots__ = ( + "source_block_id", + "count", + "fallthrough", + "throw", + "destination_block_id", + "excluded", + ) + + def __init__( + self, + source_block_id: Optional[int], + count: int, + fallthrough: bool = False, + throw: bool = False, + destination_block_id: Optional[int] = None, + excluded: Optional[bool] = None, + ) -> None: + if count < 0: + raise AssertionError("count must not be a negative value.") + + self.source_block_id = source_block_id + self.count = count + self.fallthrough = fallthrough + self.throw = throw + self.destination_block_id = destination_block_id + self.excluded = excluded + + @property + def source_block_id_or_0(self) -> int: + """Get a valid block number (0) if there was no definition in GCOV file.""" + if self.source_block_id is None: + self.source_block_id = 0 + if BranchCoverage.first_undefined_source_block_id: + BranchCoverage.first_undefined_source_block_id = False + LOGGER.info("No block number defined, assuming 0 for all undefined") + + return self.source_block_id + + @property + def is_excluded(self) -> bool: + """Return True if the branch is excluded.""" + return False if self.excluded is None else self.excluded + + @property + def is_reportable(self) -> bool: + """Return True if the branch is reportable.""" + return not self.excluded + + @property + def is_covered(self) -> bool: + """Return True if the branch is covered.""" + return self.is_reportable and self.count > 0 + + +class CallCoverage: + r"""Represent coverage information about a call. + + Args: + callno (int): + The number of the call. + covered (bool): + Whether the call was performed. + excluded (bool, optional): + Whether the call is excluded. + """ + + __slots__ = "callno", "covered", "excluded" + + def __init__( + self, + callno: int, + covered: bool, + excluded: Optional[bool] = False, + ) -> None: + self.callno = callno + self.covered = covered + self.excluded = excluded + + @property + def is_reportable(self) -> bool: + """Return True if the call is reportable.""" + return not self.excluded + + @property + def is_covered(self) -> bool: + """Return True if the call is covered.""" + return self.is_reportable and self.covered + + +class ConditionCoverage: + r"""Represent coverage information about a condition. + + Args: + count (int): + The number of the call. + covered (int): + Whether the call was performed. + not_covered_true list[int]: + The conditions which were not true. + not_covered_false list[int]: + The conditions which were not false. + excluded (bool, optional): + Whether the condition is excluded. + """ + + __slots__ = "count", "covered", "not_covered_true", "not_covered_false", "excluded" + + def __init__( + self, + count: int, + covered: int, + not_covered_true: list[int], + not_covered_false: list[int], + excluded: Optional[bool] = False, + ) -> None: + if count < 0: + raise AssertionError("count must not be a negative value.") + if count < covered: + raise AssertionError("count must not be less than covered.") + self.count = count + self.covered = covered + self.not_covered_true = not_covered_true + self.not_covered_false = not_covered_false + self.excluded = excluded + + +class DecisionCoverageUncheckable: + r"""Represent coverage information about a decision.""" + + __slots__ = () + + def __init__(self) -> None: + pass + + +class DecisionCoverageConditional: + r"""Represent coverage information about a decision. + + Args: + count_true (int): + Number of times this decision was made. + + count_false (int): + Number of times this decision was made. + + """ + + __slots__ = "count_true", "count_false" + + def __init__(self, count_true: int, count_false: int) -> None: + if count_true < 0: + raise AssertionError("count_true must not be a negative value.") + self.count_true = count_true + if count_false < 0: + raise AssertionError("count_true must not be a negative value.") + self.count_false = count_false + + +class DecisionCoverageSwitch: + r"""Represent coverage information about a decision. + + Args: + count (int): + Number of times this decision was made. + """ + + __slots__ = ("count",) + + def __init__(self, count: int) -> None: + if count < 0: + raise AssertionError("count must not be a negative value.") + self.count = count + + +DecisionCoverage = Union[ + DecisionCoverageConditional, + DecisionCoverageSwitch, + DecisionCoverageUncheckable, +] + + +class FunctionCoverage: + r"""Represent coverage information about a function. + + The counter is stored as dictionary with the line as key to be able + to merge function coverage in different ways + + Args: + name (str): + The mangled name of the function, None if not available. + demangled_name (str): + The demangled name (signature) of the functions. + lineno (int): + The line number. + count (int): + How often this function was executed. + blocks (float): + Block coverage of function. + start ((int, int)), optional): + Tuple with function start line and column. + end ((int, int)), optional): + Tuple with function end line and column. + excluded (bool, optional): + Whether this line is excluded by a marker. + """ + + __slots__ = ( + "name", + "demangled_name", + "count", + "blocks", + "start", + "end", + "excluded", + ) + + def __init__( + self, + name: Optional[str], + demangled_name: str, + *, + lineno: int, + count: int, + blocks: float, + start: Optional[tuple[int, int]] = None, + end: Optional[tuple[int, int]] = None, + excluded: bool = False, + ) -> None: + if count < 0: count = 0 + self.name = name + self.demangled_name = demangled_name + self.count = dict[int, int]({lineno: count}) + self.blocks = dict[int, float]({lineno: blocks}) + self.excluded = dict[int, bool]({lineno: excluded}) + self.start: Optional[dict[int, tuple[int, int]]] = ( + None if start is None else {lineno: start} + ) + self.end: Optional[dict[int, tuple[int, int]]] = ( + None if end is None else {lineno: end} + ) + + +class LineCoverage: + r"""Represent coverage information about a line. + + Each line is either *excluded* or *reportable*. + + A *reportable* line is either *covered* or *uncovered*. + + The default state of a line is *coverable*/*reportable*/*uncovered*. + + Args: + lineno (int): + The line number. + count (int): + How often this line was executed at least partially. + function_name (str, optional): + Mangled name of the function the line belongs to. + block_ids (*int, optional): + List of block ids in this line + excluded (bool, optional): + Whether this line is excluded by a marker. + md5 (str, optional): + The md5 checksum of the source code line. + """ + + __slots__ = ( + "lineno", + "count", + "function_name", + "block_ids", + "excluded", + "md5", + "branches", + "conditions", + "decision", + "calls", + ) + + def __init__( + self, + lineno: int, + count: int, + function_name: Optional[str] = None, + block_ids: Optional[list[int]] = None, + md5: Optional[str] = None, + excluded: bool = False, + ) -> None: + if lineno <= 0: + raise AssertionError("Line number must be a positive value.") + if count < 0: + raise AssertionError("count must not be a negative value.") + + self.lineno: int = lineno + self.count: int = count + self.function_name: Optional[str] = function_name + self.block_ids: Optional[list[int]] = block_ids + self.md5: Optional[str] = md5 + self.excluded: bool = excluded + self.branches = dict[int, BranchCoverage]() + self.conditions = dict[int, ConditionCoverage]() + self.decision: Optional[DecisionCoverage] = None + self.calls = dict[int, CallCoverage]() + + @property + def is_excluded(self) -> bool: + """Return True if the line is excluded.""" + return self.excluded + + @property + def is_reportable(self) -> bool: + """Return True if the line is reportable.""" + return not self.excluded + + @property + def is_covered(self) -> bool: + """Return True if the line is covered.""" + return self.is_reportable and self.count > 0 + + @property + def is_uncovered(self) -> bool: + """Return True if the line is uncovered.""" + return self.is_reportable and self.count == 0 + + @property + def has_uncovered_branch(self) -> bool: + """Return True if the line has a uncovered branches.""" + return not all( + branchcov.is_covered or branchcov.is_excluded + for branchcov in self.branches.values() + ) + + @property + def has_uncovered_decision(self) -> bool: + """Return True if the line has a uncovered decision.""" + if self.decision is None: + return False + + if isinstance(self.decision, DecisionCoverageUncheckable): + return False + + if isinstance(self.decision, DecisionCoverageConditional): + return self.decision.count_true == 0 or self.decision.count_false == 0 + + if isinstance(self.decision, DecisionCoverageSwitch): + return self.decision.count == 0 + + raise AssertionError(f"Unknown decision type: {self.decision!r}") + + def exclude(self) -> None: + """Exclude line from coverage statistic.""" + self.excluded = True + self.count = 0 + self.branches.clear() + self.conditions.clear() + self.decision = None + self.calls.clear() + + def branch_coverage(self) -> CoverageStat: + """Return the branch coverage statistic of the line.""" + total = 0 + covered = 0 + for branchcov in self.branches.values(): + if branchcov.is_reportable: + total += 1 + if branchcov.is_covered: + covered += 1 + return CoverageStat(covered=covered, total=total) + + def condition_coverage(self) -> CoverageStat: + """Return the condition coverage statistic of the line.""" + total = 0 + covered = 0 + for condition in self.conditions.values(): + total += condition.count + covered += condition.covered + return CoverageStat(covered=covered, total=total) + + def decision_coverage(self) -> DecisionCoverageStat: + """Return the decision coverage statistic of the line.""" + if self.decision is None: + return DecisionCoverageStat(0, 0, 0) + + if isinstance(self.decision, DecisionCoverageUncheckable): + return DecisionCoverageStat(0, 1, 2) # TODO should it be uncheckable=2? + + if isinstance(self.decision, DecisionCoverageConditional): + covered = 0 + if self.decision.count_true > 0: + covered += 1 + if self.decision.count_false > 0: + covered += 1 + return DecisionCoverageStat(covered, 0, 2) + + if isinstance(self.decision, DecisionCoverageSwitch): + covered = 0 + if self.decision.count > 0: + covered += 1 + return DecisionCoverageStat(covered, 0, 1) + + raise AssertionError(f"Unknown decision type: {self.decision!r}") + + +class FileCoverage: + """Represent coverage information about a file.""" + + __slots__ = "filename", "functions", "lines", "data_sources" + + def __init__( + self, filename: str, data_source: Optional[Union[str, set[str]]] + ) -> None: + self.filename: str = filename + self.functions = dict[str, FunctionCoverage]() + self.lines = dict[int, LineCoverage]() + self.data_sources = ( + set[str]() + if data_source is None + else set[str]( + [data_source] if isinstance(data_source, str) else data_source + ) + ) + + def filter_for_function(self, functioncov: FunctionCoverage) -> FileCoverage: + """Get a file coverage object reduced to a single function""" + if functioncov.name not in self.functions: + raise AssertionError( + f"Function {functioncov.name} must be in filtered file coverage object." + ) + if functioncov.name is None: + raise AssertionError( + "Data for filtering is missing. Need supported GCOV JSON format to get the information." + ) + filecov = FileCoverage(self.filename, self.data_sources) + filecov.functions[functioncov.name] = functioncov + + filecov.lines = { + lineno: linecov + for lineno, linecov in self.lines.items() + if linecov.function_name == functioncov.name + } + + return filecov + + @property + def stats(self) -> SummarizedStats: + """Create a coverage statistic of a file coverage object.""" + return SummarizedStats( + line=self.line_coverage(), + branch=self.branch_coverage(), + condition=self.condition_coverage(), + decision=self.decision_coverage(), + function=self.function_coverage(), + call=self.call_coverage(), + ) + + def function_coverage(self) -> CoverageStat: + """Return the function coverage statistic of the file.""" + total = 0 + covered = 0 + + for functioncov in self.functions.values(): + for lineno, excluded in functioncov.excluded.items(): + if not excluded: + total += 1 + if functioncov.count[lineno] > 0: + covered += 1 + + return CoverageStat(covered, total) + + def line_coverage(self) -> CoverageStat: + """Return the line coverage statistic of the file.""" + total = 0 + covered = 0 + + for linecov in self.lines.values(): + if linecov.is_reportable: + total += 1 + if linecov.is_covered: + covered += 1 + + return CoverageStat(covered, total) + + def branch_coverage(self) -> CoverageStat: + """Return the branch coverage statistic of the file.""" + stat = CoverageStat.new_empty() + + for linecov in self.lines.values(): + if linecov.is_reportable: + stat += linecov.branch_coverage() + + return stat + + def condition_coverage(self) -> CoverageStat: + """Return the condition coverage statistic of the file.""" + stat = CoverageStat.new_empty() + + for linecov in self.lines.values(): + if linecov.is_reportable: + stat += linecov.condition_coverage() + + return stat + + def decision_coverage(self) -> DecisionCoverageStat: + """Return the decision coverage statistic of the file.""" + stat = DecisionCoverageStat.new_empty() + + for linecov in self.lines.values(): + if linecov.is_reportable: + stat += linecov.decision_coverage() + + return stat + + def call_coverage(self) -> CoverageStat: + """Return the call coverage statistic of the file.""" + covered = 0 + total = 0 + + for linecov in self.lines.values(): + if linecov.is_reportable and len(linecov.calls) > 0: + for callcov in linecov.calls.values(): + if callcov.is_reportable: + total += 1 + if callcov.is_covered: + covered += 1 + + return CoverageStat(covered, total) + + +class CoverageContainer: + """Coverage container holding all the coverage data.""" + + def __init__(self) -> None: + self.data = dict[str, FileCoverage]() + self.directories = list[CoverageContainerDirectory]() + + def __getitem__(self, key: str) -> FileCoverage: + return self.data[key] + + def __len__(self) -> int: + return len(self.data) + + def __contains__(self, key: str) -> bool: + return key in self.data + + def __iter__(self) -> Iterator[str]: + return iter(self.data) + + def values(self) -> ValuesView[FileCoverage]: + """Get the file coverage data objects.""" + return self.data.values() + + def items(self) -> ItemsView[str, FileCoverage]: + """Get the file coverage data items.""" + return self.data.items() + + @property + def stats(self) -> SummarizedStats: + """Create a coverage statistic from a coverage data object.""" + stats = SummarizedStats.new_empty() + for filecov in self.values(): + stats += filecov.stats + return stats + + def sort_coverage( + self, + sort_key: Literal["filename", "uncovered-number", "uncovered-percent"], + sort_reverse: bool, + by_metric: Literal["line", "branch", "decision"], + filename_uses_relative_pathname: bool = False, + ) -> list[str]: + """Sort the coverage data""" + return sort_coverage( + self.data, + sort_key, + sort_reverse, + by_metric, + filename_uses_relative_pathname, + ) + + @staticmethod + def _get_dirname(filename: str) -> Optional[str]: + """Get the directory name with a trailing path separator. + + >>> import os + >>> CoverageContainer._get_dirname("bar/foobar.cpp".replace("/", os.sep)).replace(os.sep, "/") + 'bar/' + >>> CoverageContainer._get_dirname("/foo/bar/A/B.cpp".replace("/", os.sep)).replace(os.sep, "/") + '/foo/bar/A/' + >>> CoverageContainer._get_dirname(os.sep) is None + True + """ + if filename == os.sep: + return None + return str(os.path.dirname(filename.rstrip(os.sep))) + os.sep + + def populate_directories( + self, sorted_keys: Iterable[str], root_filter: re.Pattern[str] + ) -> None: + r"""Populate the list of directories and add accumulated stats. + + This function will accumulate statistics such that every directory + above it will know the statistics associated with all files deep within a + directory structure. + + Args: + sorted_keys: The sorted keys for covdata + root_filter: Information about the filter used with the root directory + """ + + # Get the directory coverage + subdirs = dict[str, CoverageContainerDirectory]() + for key in sorted_keys: + filecov = self[key] + dircov: Optional[CoverageContainerDirectory] = None + dirname: Optional[str] = ( + os.path.dirname(filecov.filename) + .replace("\\", os.sep) + .replace("/", os.sep) + .rstrip(os.sep) + ) + os.sep + while dirname is not None and root_filter.search(dirname + os.sep): + if dirname not in subdirs: + subdirs[dirname] = CoverageContainerDirectory(dirname) + if dircov is None: + subdirs[dirname][filecov.filename] = filecov + else: + subdirs[dirname].data[dircov.filename] = dircov + subdirs[dircov.filename].parent_dirname = dirname + subdirs[dirname].stats += filecov.stats + dircov = subdirs[dirname] + dirname = CoverageContainer._get_dirname(dirname) + + # Replace directories where only one sub container is available + # with the content this sub container + LOGGER.debug( + "Replace directories with only one sub element with the content of this." + ) + subdirs_to_remove = set() + for dirname, covdata_dir in subdirs.items(): + # There is exact one element, replace current element with referenced element + if len(covdata_dir) == 1: + # Get the orphan item + orphan_key, orphan_value = next(iter(covdata_dir.items())) + # The only child is a File object + if isinstance(orphan_value, FileCoverage): + # Replace the reference to ourself with our content + if covdata_dir.parent_dirname is not None: + LOGGER.debug( + f"Move {orphan_key} to {covdata_dir.parent_dirname}." + ) + parent_covdata_dir = subdirs[covdata_dir.parent_dirname] + parent_covdata_dir[orphan_key] = orphan_value + del parent_covdata_dir[dirname] + subdirs_to_remove.add(dirname) + else: + LOGGER.debug( + f"Move content of {orphan_value.dirname} to {dirname}." + ) + # Replace the children with the orphan ones + covdata_dir.data = orphan_value.data + # Change the parent key of each new child element + for new_child_value in covdata_dir.values(): + if isinstance(new_child_value, CoverageContainerDirectory): + new_child_value.parent_dirname = dirname + # Mark the key for removal. + subdirs_to_remove.add(orphan_key) + + for dirname in subdirs_to_remove: + del subdirs[dirname] + + self.directories = list(subdirs.values()) + + +class CoverageContainerDirectory: + """Represent coverage information about a directory.""" + + __slots__ = "dirname", "parent_dirname", "data", "stats" + + def __init__(self, dirname: str) -> None: + super().__init__() + self.dirname: str = dirname + self.parent_dirname: Optional[str] = None + self.data = dict[str, Union[FileCoverage, CoverageContainerDirectory]]() + self.stats: SummarizedStats = SummarizedStats.new_empty() + + def __setitem__( + self, key: str, item: Union[FileCoverage, CoverageContainerDirectory] + ) -> None: + self.data[key] = item + + def __getitem__(self, key: str) -> Union[FileCoverage, CoverageContainerDirectory]: + return self.data[key] + + def __delitem__(self, key: str) -> None: + del self.data[key] + + def __len__(self) -> int: + return len(self.data) + + def values(self) -> ValuesView[Union[FileCoverage, CoverageContainerDirectory]]: + """Get the file coverage data objects.""" + return self.data.values() + + def items(self) -> ItemsView[str, Union[FileCoverage, CoverageContainerDirectory]]: + """Get the file coverage data items.""" + return self.data.items() + + @property + def filename(self) -> str: + """Helpful function for when we use this DirectoryCoverage in a union with FileCoverage""" + return self.dirname + + def sort_coverage( + self, + sort_key: Literal["filename", "uncovered-number", "uncovered-percent"], + sort_reverse: bool, + by_metric: Literal["line", "branch", "decision"], + filename_uses_relative_pathname: bool = False, + ) -> list[str]: + """Sort the coverage data""" + return sort_coverage( + self.data, + sort_key, + sort_reverse, + by_metric, + filename_uses_relative_pathname, + ) + + def line_coverage(self) -> CoverageStat: + """A simple wrapper function necessary for sort_coverage().""" + return self.stats.line + + def branch_coverage(self) -> CoverageStat: + """A simple wrapper function necessary for sort_coverage().""" + return self.stats.branch + + def decision_coverage(self) -> DecisionCoverageStat: + """A simple wrapper function necessary for sort_coverage().""" + return self.stats.decision + + +@dataclass +class SummarizedStats: + """Data class for the summarized coverage statistics.""" + + line: CoverageStat + branch: CoverageStat + condition: CoverageStat + decision: DecisionCoverageStat + function: CoverageStat + call: CoverageStat + + @staticmethod + def new_empty() -> SummarizedStats: + """Create a empty coverage statistic.""" + return SummarizedStats( + line=CoverageStat.new_empty(), + branch=CoverageStat.new_empty(), + condition=CoverageStat.new_empty(), + decision=DecisionCoverageStat.new_empty(), + function=CoverageStat.new_empty(), + call=CoverageStat.new_empty(), + ) + + def __iadd__(self, other: SummarizedStats) -> SummarizedStats: + self.line += other.line + self.branch += other.branch + self.condition += other.condition + self.decision += other.decision + self.function += other.function + self.call += other.call + return self + + +@dataclass +class CoverageStat: + """A single coverage metric, e.g. the line coverage percentage of a file.""" + + covered: int + """How many elements were covered.""" + + total: int + """How many elements there were in total.""" + + @staticmethod + def new_empty() -> CoverageStat: + """Create a empty coverage statistic.""" + return CoverageStat(0, 0) + + @property + def percent(self) -> Optional[float]: + """Percentage of covered elements, equivalent to ``self.percent_or(None)``""" + return self.percent_or(None) + + def percent_or(self, default: _T) -> Union[float, _T]: + """Percentage of covered elements. + + Coverage is truncated to one decimal: + >>> CoverageStat(1234, 10000).percent_or("default") + 12.3 + + Coverage is capped at 99.9% unless everything is covered: + >>> CoverageStat(9999, 10000).percent_or("default") + 99.9 + >>> CoverageStat(10000, 10000).percent_or("default") + 100.0 + + If there are no elements, percentage is NaN and the default will be returned: + >>> CoverageStat(0, 0).percent_or("default") + 'default' + """ + if not self.total: + return default + + # Return 100% only if covered == total. + if self.covered == self.total: + return 100.0 + + # There is at least one uncovered item. + # Round to 1 decimal and clamp to max 99.9%. + ratio = self.covered / self.total + return min(99.9, round(ratio * 100.0, 1)) + + def __iadd__(self, other: CoverageStat) -> CoverageStat: + self.covered += other.covered + self.total += other.total + return self + + +@dataclass +class DecisionCoverageStat: + """A CoverageStat for decision coverage (accounts for Uncheckable cases).""" + + covered: int + uncheckable: int + total: int + + @classmethod + def new_empty(cls) -> DecisionCoverageStat: + """Create a empty decision coverage statistic.""" + return cls(0, 0, 0) + + @property + def to_coverage_stat(self) -> CoverageStat: + """Convert a decision coverage statistic to a coverage statistic.""" + return CoverageStat(covered=self.covered, total=self.total) + + @property + def percent(self) -> Optional[float]: + """Return the percent value of the coverage.""" + return self.to_coverage_stat.percent + + def percent_or(self, default: _T) -> Union[float, _T]: + """Return the percent value of the coverage or the given default if no coverage is present.""" + return self.to_coverage_stat.percent_or(default) + + def __iadd__(self, other: DecisionCoverageStat) -> DecisionCoverageStat: + self.covered += other.covered + self.uncheckable += other.uncheckable + self.total += other.total + return self diff --git a/status_ui.cpp b/status_ui.cpp index 8e9a5eb..2ed44b7 100644 --- a/status_ui.cpp +++ b/status_ui.cpp @@ -10,7 +10,8 @@ namespace gui { using std::any, std::any_cast, std::string, std::make_any; StatusUI::StatusUI(GameLevel level) : - $level(level), $ritual_ui(level) + $level(level), $ritual_ui(level), + $map_ui($level) { $gui.position(STATUS_UI_X, STATUS_UI_Y, STATUS_UI_WIDTH, STATUS_UI_HEIGHT); $gui.layout( @@ -37,6 +38,8 @@ namespace gui { $log_to = $gui.entity("log_view"); $gui.set($log_to, {}); $gui.set($log_to, {"Welcome to the Game!", 20}); + + $map_ui.init(cell.x, cell.y, cell.w, cell.h); } else { auto button = $gui.entity(name); $gui.set(button, {}); @@ -134,6 +137,11 @@ namespace gui { void StatusUI::render(sf::RenderWindow &window) { $gui.render(window); + + if(map_open) { + $map_ui.render(window); + } + $ritual_ui.render(window); } @@ -146,6 +154,7 @@ namespace gui { void StatusUI::update_level(GameLevel &level) { $level = level; + $map_ui.update_level($level); init(); } } diff --git a/status_ui.hpp b/status_ui.hpp index 0fccb6f..8f4ecb8 100644 --- a/status_ui.hpp +++ b/status_ui.hpp @@ -5,16 +5,19 @@ #include "textures.hpp" #include "guecs.hpp" #include "ritual_ui.hpp" +#include "map_view.hpp" namespace gui { class StatusUI { public: + bool map_open = false; guecs::UI $gui; DinkyECS::Entity $log_to; std::map $slots; std::deque $messages; GameLevel $level; RitualUI $ritual_ui; + MapViewUI $map_ui; StatusUI(GameLevel level); void select_slot(DinkyECS::Entity ent, std::any data); diff --git a/tilemap.cpp b/tilemap.cpp index ee4f566..1a3f4e8 100644 --- a/tilemap.cpp +++ b/tilemap.cpp @@ -1,6 +1,7 @@ #include "tilemap.hpp" #include "dbc.hpp" #include "constants.hpp" +#include using nlohmann::json; using components::Tile; @@ -14,18 +15,26 @@ TileMap::TileMap(size_t width, size_t height) : { } -void TileMap::dump(int show_x, int show_y) { +std::string TileMap::to_string(int show_x, int show_y) { + std::string result; + for(matrix::each_row it{$tile_ids}; it.next();) { const Tile &cell = $display[it.y][it.x]; if(int(it.x) == show_x && int(it.y) == show_y) { - fmt::print("{}<", cell.display); + result += "@"; } else { - fmt::print("{} ", cell.display); + result += cell.display; } - if(it.row) fmt::print("\n"); + if(it.row) result += "\n"; } + + return result; +} + +void TileMap::dump(int show_x, int show_y) { + std::cout << to_string(show_x, show_y) << std::endl; } void TileMap::set_tile(size_t x, size_t y, string tile_name) { diff --git a/tilemap.hpp b/tilemap.hpp index 099152e..e8312c6 100644 --- a/tilemap.hpp +++ b/tilemap.hpp @@ -32,6 +32,7 @@ public: void set_tile(size_t x, size_t y, std::string tile_name); std::vector tile_names(bool collision); + std::string to_string(int show_x, int show_y); void dump(int show_x=-1, int show_y=-1); bool INVARIANT(); };