commit 7d81e22900a4e541720effbf8b28b7104969e3a2 Author: Bensuperpc Date: Mon Jan 29 21:08:29 2024 +0100 Add files Signed-off-by: Bensuperpc diff --git a/.clang-format b/.clang-format new file mode 100755 index 0000000..258d9a2 --- /dev/null +++ b/.clang-format @@ -0,0 +1,259 @@ +--- +Language: Cpp +# BasedOnStyle: Chromium +AccessModifierOffset: -1 +AlignAfterOpenBracket: Align +AlignArrayOfStructures: None +AlignConsecutiveAssignments: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + PadOperators: true +AlignConsecutiveBitFields: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + PadOperators: false +AlignConsecutiveDeclarations: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + PadOperators: false +AlignConsecutiveMacros: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + PadOperators: false +AlignEscapedNewlines: Left +AlignOperands: Align +AlignTrailingComments: + Kind: Always + OverEmptyLines: 0 +AllowAllArgumentsOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false +AllowShortEnumsOnASingleLine: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: All +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes +AttributeMacros: + - __capability +BinPackArguments: true +BinPackParameters: false +BitFieldColonSpacing: Both +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: Never + AfterEnum: false + AfterExternBlock: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakAfterAttributes: Never +BreakAfterJavaFieldAnnotations: false +BreakArrays: true +BreakBeforeBinaryOperators: None +BreakBeforeConceptDeclarations: Always +BreakBeforeBraces: Attach +BreakBeforeInlineASMColon: OnlyMultiline +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: BeforeColon +BreakInheritanceList: BeforeColon +BreakStringLiterals: true +ColumnLimit: 144 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +EmptyLineAfterAccessModifier: Never +EmptyLineBeforeAccessModifier: LogicalBlock +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IfMacros: + - KJ_IF_MAYBE +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^' + Priority: 2 + SortPriority: 0 + CaseSensitive: false + - Regex: '^<.*\.h>' + Priority: 1 + SortPriority: 0 + CaseSensitive: false + - Regex: '^<.*' + Priority: 2 + SortPriority: 0 + CaseSensitive: false + - Regex: '.*' + Priority: 3 + SortPriority: 0 + CaseSensitive: false +IncludeIsMainRegex: '([-_](test|unittest))?$' +IncludeIsMainSourceRegex: '' +IndentAccessModifiers: false +IndentCaseBlocks: false +IndentCaseLabels: true +IndentExternBlock: AfterExternBlock +IndentGotoLabels: true +IndentPPDirectives: None +IndentRequiresClause: true +IndentWidth: 4 +IndentWrappedFunctionNames: false +InsertBraces: false +InsertNewlineAtEOF: false +InsertTrailingCommas: None +IntegerLiteralSeparator: + Binary: 0 + BinaryMinDigits: 0 + Decimal: 0 + DecimalMinDigits: 0 + Hex: 0 + HexMinDigits: 0 +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +LambdaBodyIndentation: Signature +LineEnding: DeriveLF +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Never +ObjCBlockIndentWidth: 2 +ObjCBreakBeforeNestedBlockParam: true +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PackConstructorInitializers: NextLine +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakOpenParenthesis: 0 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyIndentedWhitespace: 0 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Left +PPIndentWidth: -1 +QualifierAlignment: Leave +RawStringFormats: + - Language: Cpp + Delimiters: + - cc + - CC + - cpp + - Cpp + - CPP + - 'c++' + - 'C++' + CanonicalDelimiter: '' + BasedOnStyle: google + - Language: TextProto + Delimiters: + - pb + - PB + - proto + - PROTO + EnclosingFunctions: + - EqualsProto + - EquivToProto + - PARSE_PARTIAL_TEXT_PROTO + - PARSE_TEST_PROTO + - PARSE_TEXT_PROTO + - ParseTextOrDie + - ParseTextProtoOrDie + - ParseTestProto + - ParsePartialTestProto + CanonicalDelimiter: pb + BasedOnStyle: google +ReferenceAlignment: Pointer +ReflowComments: true +RemoveBracesLLVM: false +RemoveSemicolon: false +RequiresClausePosition: OwnLine +RequiresExpressionIndentation: OuterScope +SeparateDefinitionBlocks: Leave +ShortNamespaceLines: 1 +SortIncludes: CaseSensitive +SortJavaStaticImport: Before +SortUsingDeclarations: LexicographicNumeric +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceAroundPointerQualifiers: Default +SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeParensOptions: + AfterControlStatements: true + AfterForeachMacros: true + AfterFunctionDefinitionName: false + AfterFunctionDeclarationName: false + AfterIfMacros: true + AfterOverloadedOperator: false + AfterRequiresInClause: false + AfterRequiresInExpression: false + BeforeNonEmptyParentheses: false +SpaceBeforeRangeBasedForLoopColon: true +SpaceBeforeSquareBrackets: false +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: Never +SpacesInConditionalStatement: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInLineCommentPrefix: + Minimum: 1 + Maximum: -1 +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Auto +StatementAttributeLikeMacros: + - Q_EMIT +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TabWidth: 8 +UseTab: Never +WhitespaceSensitiveMacros: + - BOOST_PP_STRINGIZE + - CF_SWIFT_NAME + - NS_SWIFT_NAME + - PP_STRINGIZE + - STRINGIZE +... + diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..d509f2c --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,155 @@ +--- +# Enable ALL the things! Except not really +# misc-non-private-member-variables-in-classes: the options don't do anything +# modernize-use-nodiscard: too aggressive, attribute is situationally useful +Checks: "*,\ + -google-readability-todo,\ + -altera-*,\ + -fuchsia-*,\ + fuchsia-multiple-inheritance,\ + -llvm-header-guard,\ + -llvm-include-order,\ + -llvmlibc-*,\ + -modernize-use-nodiscard,\ + -misc-non-private-member-variables-in-classes" +WarningsAsErrors: '' +CheckOptions: + - key: 'bugprone-argument-comment.StrictMode' + value: 'true' +# Prefer using enum classes with 2 values for parameters instead of bools + - key: 'bugprone-argument-comment.CommentBoolLiterals' + value: 'true' + - key: 'bugprone-misplaced-widening-cast.CheckImplicitCasts' + value: 'true' + - key: 'bugprone-sizeof-expression.WarnOnSizeOfIntegerExpression' + value: 'true' + - key: 'bugprone-suspicious-string-compare.WarnOnLogicalNotComparison' + value: 'true' + - key: 'readability-simplify-boolean-expr.ChainedConditionalReturn' + value: 'true' + - key: 'readability-simplify-boolean-expr.ChainedConditionalAssignment' + value: 'true' + - key: 'readability-uniqueptr-delete-release.PreferResetCall' + value: 'true' + - key: 'cppcoreguidelines-init-variables.MathHeader' + value: '' + - key: 'cppcoreguidelines-narrowing-conversions.PedanticMode' + value: 'true' + - key: 'readability-else-after-return.WarnOnUnfixable' + value: 'true' + - key: 'readability-else-after-return.WarnOnConditionVariables' + value: 'true' + - key: 'readability-inconsistent-declaration-parameter-name.Strict' + value: 'true' + - key: 'readability-qualified-auto.AddConstToQualified' + value: 'true' + - key: 'readability-redundant-access-specifiers.CheckFirstDeclaration' + value: 'true' +# These seem to be the most common identifier styles + - key: 'readability-identifier-naming.AbstractClassCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ClassCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ClassConstantCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ClassMemberCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ClassMethodCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ConstantCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ConstantMemberCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ConstantParameterCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ConstantPointerParameterCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ConstexprFunctionCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ConstexprMethodCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ConstexprVariableCase' + value: 'lower_case' + - key: 'readability-identifier-naming.EnumCase' + value: 'lower_case' + - key: 'readability-identifier-naming.EnumConstantCase' + value: 'lower_case' + - key: 'readability-identifier-naming.FunctionCase' + value: 'lower_case' + - key: 'readability-identifier-naming.GlobalConstantCase' + value: 'lower_case' + - key: 'readability-identifier-naming.GlobalConstantPointerCase' + value: 'lower_case' + - key: 'readability-identifier-naming.GlobalFunctionCase' + value: 'lower_case' + - key: 'readability-identifier-naming.GlobalPointerCase' + value: 'lower_case' + - key: 'readability-identifier-naming.GlobalVariableCase' + value: 'lower_case' + - key: 'readability-identifier-naming.InlineNamespaceCase' + value: 'lower_case' + - key: 'readability-identifier-naming.LocalConstantCase' + value: 'lower_case' + - key: 'readability-identifier-naming.LocalConstantPointerCase' + value: 'lower_case' + - key: 'readability-identifier-naming.LocalPointerCase' + value: 'lower_case' + - key: 'readability-identifier-naming.LocalVariableCase' + value: 'lower_case' + - key: 'readability-identifier-naming.MacroDefinitionCase' + value: 'UPPER_CASE' + - key: 'readability-identifier-naming.MemberCase' + value: 'lower_case' + - key: 'readability-identifier-naming.MethodCase' + value: 'lower_case' + - key: 'readability-identifier-naming.NamespaceCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ParameterCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ParameterPackCase' + value: 'lower_case' + - key: 'readability-identifier-naming.PointerParameterCase' + value: 'lower_case' + - key: 'readability-identifier-naming.PrivateMemberCase' + value: 'lower_case' + - key: 'readability-identifier-naming.PrivateMemberPrefix' + value: 'm_' + - key: 'readability-identifier-naming.PrivateMethodCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ProtectedMemberCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ProtectedMemberPrefix' + value: 'm_' + - key: 'readability-identifier-naming.ProtectedMethodCase' + value: 'lower_case' + - key: 'readability-identifier-naming.PublicMemberCase' + value: 'lower_case' + - key: 'readability-identifier-naming.PublicMethodCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ScopedEnumConstantCase' + value: 'lower_case' + - key: 'readability-identifier-naming.StaticConstantCase' + value: 'lower_case' + - key: 'readability-identifier-naming.StaticVariableCase' + value: 'lower_case' + - key: 'readability-identifier-naming.StructCase' + value: 'lower_case' + - key: 'readability-identifier-naming.TemplateParameterCase' + value: 'CamelCase' + - key: 'readability-identifier-naming.TemplateTemplateParameterCase' + value: 'CamelCase' + - key: 'readability-identifier-naming.TypeAliasCase' + value: 'lower_case' + - key: 'readability-identifier-naming.TypedefCase' + value: 'lower_case' + - key: 'readability-identifier-naming.TypeTemplateParameterCase' + value: 'CamelCase' + - key: 'readability-identifier-naming.UnionCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ValueTemplateParameterCase' + value: 'CamelCase' + - key: 'readability-identifier-naming.VariableCase' + value: 'lower_case' + - key: 'readability-identifier-naming.VirtualMethodCase' + value: 'lower_case' +... diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 0000000..5bf88c7 --- /dev/null +++ b/.codespellrc @@ -0,0 +1,6 @@ +[codespell] +builtin = clear,rare,en-GB_to_en-US,names,informal,code +check-filenames = +check-hidden = +skip = */.git,*/build,*/prefix +quiet-level = 2 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100755 index 0000000..bac6de2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,185 @@ +name: Continuous Integration + +on: + push: + branches: + - master + - main + - develop + + pull_request: + branches: + - master + - main + - develop + +jobs: + lint: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-python@v4 + with: { python-version: "3.8" } + + - name: Install codespell + run: pip3 install codespell + + - name: Lint + if: always() + run: cmake -D FORMAT_COMMAND=clang-format-14 -P cmake/lint.cmake + + - name: Spell check + if: always() + run: cmake -P cmake/spell.cmake + + coverage: + needs: [lint] + + runs-on: ubuntu-22.04 + + # To enable coverage, delete the last line from the conditional below and + # edit the "" placeholder to your GitHub name. + # If you do not wish to use codecov, then simply delete this job from the + # workflow. + if: github.repository_owner == 'bensuperpc' + && false + + steps: + - uses: actions/checkout@v3 + + - name: Install LCov + run: sudo apt-get update -q + && sudo apt-get install lcov -q -y + + - name: Configure + run: cmake --preset=ci-coverage + + - name: Build + run: cmake --build build/coverage -j 2 + + - name: Test + working-directory: build/coverage + run: ctest --output-on-failure --no-tests=error -j 2 + + - name: Process coverage info + run: cmake --build build/coverage -t coverage + + - name: Submit to codecov.io + uses: codecov/codecov-action@v3 + with: + file: build/coverage/coverage.info + + sanitize: + needs: [lint] + + runs-on: ubuntu-22.04 + + env: { CXX: clang++-14 } + + steps: + - uses: actions/checkout@v3 + + - name: Configure + run: cmake --preset=ci-sanitize + + - name: Build + run: cmake --build build/sanitize -j 2 + + - name: Test + working-directory: build/sanitize + env: + ASAN_OPTIONS: "strict_string_checks=1:\ + detect_stack_use_after_return=1:\ + check_initialization_order=1:\ + strict_init_order=1:\ + detect_leaks=1" + UBSAN_OPTIONS: print_stacktrace=1 + run: ctest --output-on-failure --no-tests=error -j 2 + + test: + needs: [lint] + + strategy: + matrix: + os: [macos-12, ubuntu-22.04, windows-2022] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v3 + + - name: Install static analyzers + if: matrix.os == 'ubuntu-22.04' + run: >- + sudo apt-get install clang-tidy-14 cppcheck -y -q + + sudo update-alternatives --install + /usr/bin/clang-tidy clang-tidy + /usr/bin/clang-tidy-14 140 + + - name: Setup MultiToolTask + if: matrix.os == 'windows-2022' + run: | + Add-Content "$env:GITHUB_ENV" 'UseMultiToolTask=true' + Add-Content "$env:GITHUB_ENV" 'EnforceProcessCountAcrossBuilds=true' + + - name: Configure + shell: pwsh + run: cmake "--preset=ci-$("${{ matrix.os }}".split("-")[0])" + + - name: Build + run: cmake --build build --config Release -j 2 + + - name: Install + run: cmake --install build --config Release --prefix prefix + + - name: Test + working-directory: build + run: ctest --output-on-failure --no-tests=error -C Release -j 2 + + docs: + # Deploy docs only when builds succeed + needs: [sanitize, test] + + runs-on: ubuntu-22.04 + + # To enable, first you have to create an orphaned gh-pages branch: + # + # git switch --orphan gh-pages + # git commit --allow-empty -m "Initial commit" + # git push -u origin gh-pages + # + # Edit the placeholder below to your GitHub name, so this action + # runs only in your repository and no one else's fork. After these, delete + # this comment and the last line in the conditional below. + # If you do not wish to use GitHub Pages for deploying documentation, then + # simply delete this job similarly to the coverage one. + if: github.ref == 'refs/heads/master' + && github.event_name == 'push' + && github.repository_owner == 'bensuperpc' + && false + + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-python@v4 + with: { python-version: "3.8" } + + - name: Install m.css dependencies + run: pip3 install jinja2 Pygments + + - name: Install Doxygen + run: sudo apt-get update -q + && sudo apt-get install doxygen -q -y + + - name: Build docs + run: cmake "-DPROJECT_SOURCE_DIR=$PWD" "-DPROJECT_BINARY_DIR=$PWD/build" + -P cmake/docs-ci.cmake + + - name: Deploy docs + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: build/docs/html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c314b3e --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +**/.DS_Store +.idea/ +.vs/ +.vscode/ +build/ +cmake-build-*/ +prefix/ +.clangd +CMakeLists.txt.user +compile_commands.json +venv/* diff --git a/BUILDING.md b/BUILDING.md new file mode 100644 index 0000000..18b7297 --- /dev/null +++ b/BUILDING.md @@ -0,0 +1,103 @@ +# Building with CMake + +## Build + +This project doesn't require any special command-line flags to build to keep +things simple. + +### Building with Make + +You can use the Makefile provided in the root of the project to easily build multiple presets: + +```sh +make base # Build the base preset +``` + +```sh +make debug # Build the debug preset +``` + +### Building with CMake + +Here are the steps for building in release mode with a single-configuration +generator, like the Unix Makefiles one: + +```sh +cmake -S . -B build -D CMAKE_BUILD_TYPE=Release +cmake --build build +``` + +Here are the steps for building in release mode with a multi-configuration +generator, like the Visual Studio ones: + +```sh +cmake -S . -B build +cmake --build build --config Release +``` + +### Building with MSVC + +Note that MSVC by default is not standards compliant and you need to pass some +flags to make it behave properly. See the `flags-windows` preset in the +[CMakePresets.json](CMakePresets.json) file for the flags and with what +variable to provide them to CMake during configuration. + +### Building on Apple Silicon + +CMake supports building on Apple Silicon properly since 3.20.1. Make sure you +have the [latest version][1] installed. + +## Install + +This project doesn't require any special command-line flags to install to keep +things simple. As a prerequisite, the project has to be built with the above +commands already. + +The below commands require at least CMake 3.15 to run, because that is the +version in which [Install a Project][2] was added. + +Here is the command for installing the release mode artifacts with a +single-configuration generator, like the Unix Makefiles one: + +```sh +cmake --install build +``` + +Here is the command for installing the release mode artifacts with a +multi-configuration generator, like the Visual Studio ones: + +```sh +cmake --install build --config Release +``` + +### CMake package + +This project exports a CMake package to be used with the [`find_package`][3] +command of CMake: + +* Package name: `astar` +* Target name: `astar::astar` + +Example usage: + +```cmake +find_package(astar REQUIRED) +# Declare the imported target as a build requirement using PRIVATE, where +# project_target is a target created in the consuming project +target_link_libraries( + project_target PRIVATE + astar::astar +) +``` + +### Note to packagers + +The `CMAKE_INSTALL_INCLUDEDIR` is set to a path other than just `include` if +the project is configured as a top level project to avoid indirectly including +other libraries when installed to a common prefix. Please review the +[install-rules.cmake](cmake/install-rules.cmake) file for the full set of +install rules. + +[1]: https://cmake.org/download/ +[2]: https://cmake.org/cmake/help/latest/manual/cmake.1.html#install-a-project +[3]: https://cmake.org/cmake/help/latest/command/find_package.html diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..536cb6a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,60 @@ +cmake_minimum_required(VERSION 3.14) + +include(cmake/prelude.cmake) + +project( + astar + VERSION 0.1.0 + DESCRIPTION "astar" + HOMEPAGE_URL "bensuperpc.org" + LANGUAGES NONE +) + +include(cmake/project-is-top-level.cmake) +include(cmake/variables.cmake) + +# ---- Declare library ---- + +add_library(astar_astar INTERFACE) +add_library(astar::astar ALIAS astar_astar) + +set_property( + TARGET astar_astar PROPERTY + EXPORT_NAME astar +) + +target_include_directories( + astar_astar ${warning_guard} + INTERFACE + "$" +) + +target_compile_features(astar_astar INTERFACE cxx_std_20) + +# ---- Install rules ---- + +if(NOT CMAKE_SKIP_INSTALL_RULES) + include(cmake/install-rules.cmake) +endif() + +# ---- Examples ---- + +if(PROJECT_IS_TOP_LEVEL) + option(BUILD_EXAMPLES "Build examples tree." "${astar_DEVELOPER_MODE}") + if(BUILD_EXAMPLES) + add_subdirectory(example) + endif() +endif() + +# ---- Developer mode ---- + +if(NOT astar_DEVELOPER_MODE) + return() +elseif(NOT PROJECT_IS_TOP_LEVEL) + message( + AUTHOR_WARNING + "Developer mode is intended for developers of astar" + ) +endif() + +include(cmake/dev-mode.cmake) diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100755 index 0000000..08cac32 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,247 @@ +{ + "version": 2, + "cmakeMinimumRequired": { + "major": 3, + "minor": 14, + "patch": 0 + }, + "configurePresets": [ + { + "name": "cmake-pedantic", + "hidden": true, + "warnings": { + "dev": true, + "deprecated": true, + "uninitialized": true, + "unusedCli": true, + "systemVars": false + }, + "errors": { + "dev": false, + "deprecated": false + } + }, + { + "name": "dev-mode", + "hidden": true, + "inherits": "cmake-pedantic", + "cacheVariables": { + "astar_DEVELOPER_MODE": "ON" + } + }, + { + "name": "cppcheck", + "hidden": true, + "cacheVariables": { + "CMAKE_CXX_CPPCHECK": "cppcheck;--inline-suppr", + "CMAKE_C_CPPCHECK": "cppcheck;--inline-suppr" + } + }, + { + "name": "clang-tidy", + "hidden": true, + "cacheVariables": { + "CMAKE_CXX_CLANG_TIDY": "clang-tidy;--header-filter=^${sourceDir}/", + "CMAKE_C_CLANG_TIDY": "clang-tidy;--header-filter=^${sourceDir}/" + } + }, + { + "name": "ci-std", + "description": "This preset makes sure the project actually builds with at least the specified standard", + "hidden": true, + "cacheVariables": { + "CMAKE_CXX_EXTENSIONS": "ON", + "CMAKE_CXX_STANDARD": "20", + "CMAKE_CXX_STANDARD_REQUIRED": "ON", + "CMAKE_C_EXTENSIONS": "ON", + "CMAKE_C_STANDARD": "17", + "CMAKE_C_STANDARD_REQUIRED": "ON" + } + }, + { + "name": "flags-gcc-clang", + "hidden": true, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 -pipe -fstack-protector-strong -fstack-clash-protection -Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion -Wcast-qual -Wformat=2 -Wundef -Wshadow -Wcast-align -Wunused -Wnull-dereference -Wdouble-promotion -Wimplicit-fallthrough -Wextra-semi -Woverloaded-virtual -Wnon-virtual-dtor -Wold-style-cast -Wformat-security", + "CMAKE_C_FLAGS": "-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 -pipe -fstack-protector-strong -fstack-clash-protection -Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion -Wcast-qual -Wformat=2 -Wundef -Wshadow -Wcast-align -Wunused -Wnull-dereference -Wdouble-promotion -Wformat-security", + "CMAKE_EXE_LINKER_FLAGS": "-Wl,--allow-shlib-undefined,--as-needed,-z,noexecstack,-z,relro,-z,now", + "CMAKE_SHARED_LINKER_FLAGS": "-Wl,--allow-shlib-undefined,--as-needed,-z,noexecstack,-z,relro,-z,now" + } + }, + { + "name": "flags-appleclang", + "hidden": true, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "-pipe -fstack-protector-strong -Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion -Wcast-qual -Wformat=2 -Wundef -Wshadow -Wcast-align -Wunused -Wnull-dereference -Wdouble-promotion -Wimplicit-fallthrough -Wextra-semi -Woverloaded-virtual -Wnon-virtual-dtor -Wold-style-cast", + "CMAKE_C_FLAGS": "-pipe -fstack-protector-strong -Wall -Wextra -Wpedantic" + } + }, + { + "name": "flags-msvc", + "description": "Note that all the flags after /W4 are required for MSVC to conform to the language standard", + "hidden": true, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "/sdl /guard:cf /utf-8 /diagnostics:caret /w14165 /w44242 /w44254 /w44263 /w34265 /w34287 /w44296 /w44365 /w44388 /w44464 /w14545 /w14546 /w14547 /w14549 /w14555 /w34619 /w34640 /w24826 /w14905 /w14906 /w14928 /w45038 /W4 /permissive- /volatile:iso /Zc:inline /Zc:preprocessor /Zc:enumTypes /Zc:lambda /Zc:__cplusplus /Zc:externConstexpr /Zc:throwingNew /EHsc", + "CMAKE_EXE_LINKER_FLAGS": "/machine:x64 /guard:cf" + } + }, + { + "name": "flags-cuda", + "hidden": true, + "cacheVariables": { + "CMAKE_CUDA_FLAGS": "--default-stream per-thread -Xfatbin=-compress-all -arch=all-major -Xcompiler=-Wall,-Wextra,-Wconversion,-Wsign-conversion,-Wcast-qual,-Wundef,-Wshadow,-Wunused,-Wnull-dereference,-Wdouble-promotion,-Wimplicit-fallthrough,-Wextra-semi,-Woverloaded-virtual,-Wnon-virtual-dtor,-Wformat-security", + "CMAKE_CUDA_ARCHITECTURES": "50;52;53;60;61;62;70;72;75;80;86;87;89;90", + "CUDA_PROPAGATE_HOST_FLAGS": "OFF", + "CMAKE_CUDA_SEPARABLE_COMPILATION": "ON" + } + }, + { + "name": "flags-debugger", + "hidden": true, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "-O0 -g3 -ggdb3 -Wall -Wextra -Wpedantic -pg", + "CMAKE_C_FLAGS": "-O0 -g3 -ggdb3 -Wall -Wextra -Wpedantic -pg" + } + }, + { + "name": "ci-cuda", + "description": "This preset makes sure the project actually builds with at least the specified standard", + "hidden": true, + "cacheVariables": { + "CMAKE_CUDA_STANDARD": "17", + "CMAKE_CUDA_STANDARD_REQUIRED": "ON", + "CMAKE_CUDA_EXTENSIONS": "OFF" + } + }, + { + "name": "ci-linux", + "generator": "Unix Makefiles", + "hidden": true, + "inherits": ["flags-gcc-clang", "ci-std", "ci-cuda", "flags-cuda"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "ci-darwin", + "generator": "Unix Makefiles", + "hidden": true, + "inherits": ["flags-appleclang", "ci-std"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "ci-base", + "generator": "Unix Makefiles", + "hidden": true, + "inherits": ["ci-std", "flags-gcc-clang", "dev-mode", "flags-cuda", "ci-cuda"] + }, + { + "name": "ci-win64", + "inherits": ["flags-msvc", "ci-std"], + "generator": "Visual Studio 17 2022", + "architecture": "x64", + "hidden": true + }, + { + "name": "coverage-linux", + "binaryDir": "${sourceDir}/build/coverage", + "inherits": "ci-linux", + "hidden": true, + "cacheVariables": { + "ENABLE_COVERAGE": "ON", + "CMAKE_BUILD_TYPE": "Coverage", + "CMAKE_CXX_FLAGS_COVERAGE": "-O0 -g3 --coverage -fkeep-inline-functions -fkeep-static-functions", + "CMAKE_EXE_LINKER_FLAGS_COVERAGE": "--coverage", + "CMAKE_SHARED_LINKER_FLAGS_COVERAGE": "--coverage" + } + }, + { + "name": "ci-coverage", + "inherits": ["coverage-linux", "dev-mode"], + "cacheVariables": { + "COVERAGE_HTML_COMMAND": "" + } + }, + { + "name": "ci-sanitize", + "binaryDir": "${sourceDir}/build/sanitize", + "inherits": ["ci-linux", "dev-mode"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Sanitize", + "CMAKE_CXX_FLAGS_SANITIZE": "-Og -U_FORTIFY_SOURCE -g3 -fsanitize=address,undefined,leak -fno-omit-frame-pointer -fno-common", + "CMAKE_C_FLAGS_SANITIZE": "-Og -U_FORTIFY_SOURCE -g3" + }, + "environment": { + "ASAN_OPTIONS": "strict_string_checks=1 detect_stack_use_after_return=1 check_initialization_order=1 strict_init_order=1 detect_leaks=1", + "UBSAN_OPTIONS": "print_stacktrace=1" + } + }, + { + "name": "ci-build", + "binaryDir": "${sourceDir}/build", + "hidden": true + }, + { + "name": "base", + "binaryDir": "${sourceDir}/build/base", + "inherits": ["ci-base"], + "hidden": false, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "base-clang", + "binaryDir": "${sourceDir}/build/base-clang", + "inherits": ["ci-base"], + "hidden": false, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + }, + { + "name": "gprof", + "generator": "Unix Makefiles", + "binaryDir": "${sourceDir}/build/gprof", + "inherits": ["ci-std", "flags-debugger", "dev-mode"], + "hidden": false, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "debugger", + "generator": "Unix Makefiles", + "binaryDir": "${sourceDir}/build/debugger", + "inherits": ["ci-std", "flags-debugger", "dev-mode"], + "hidden": false, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "dev-common", + "hidden": true, + "inherits": ["dev-mode", "clang-tidy", "cppcheck"], + "cacheVariables": { + "BUILD_MCSS_DOCS": "ON" + } + }, + { + "name": "ci-macos", + "inherits": ["ci-build", "ci-darwin", "dev-mode"] + }, + { + "name": "ci-ubuntu", + "inherits": ["ci-build", "ci-linux", "dev-common"] + }, + { + "name": "ci-windows", + "inherits": ["ci-build", "ci-win64", "dev-mode"] + } + ] +} diff --git a/CMakeUserPresets.json b/CMakeUserPresets.json new file mode 100644 index 0000000..d7ceeb6 --- /dev/null +++ b/CMakeUserPresets.json @@ -0,0 +1,69 @@ +{ + "version": 2, + "cmakeMinimumRequired": { + "major": 3, + "minor": 14, + "patch": 0 + }, + "configurePresets": [ + { + "name": "dev-linux", + "binaryDir": "${sourceDir}/build/dev-linux", + "inherits": ["dev-common", "ci-linux"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + { + "name": "dev-darwin", + "binaryDir": "${sourceDir}/build/dev-darwin", + "inherits": ["dev-common", "ci-darwin"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + { + "name": "dev-win64", + "binaryDir": "${sourceDir}/build/dev-win64", + "inherits": ["dev-common", "ci-win64"], + "environment": { + "UseMultiToolTask": "true", + "EnforceProcessCountAcrossBuilds": "true" + } + }, + { + "name": "dev", + "binaryDir": "${sourceDir}/build/dev", + "inherits": "dev-linux" + }, + { + "name": "dev-coverage", + "binaryDir": "${sourceDir}/build/coverage", + "inherits": ["dev-mode", "coverage-linux"] + } + ], + "buildPresets": [ + { + "name": "dev", + "configurePreset": "dev", + "configuration": "Debug", + "jobs": 16 + } + ], + "testPresets": [ + { + "name": "dev", + "configurePreset": "dev", + "configuration": "Debug", + "output": { + "outputOnFailure": true + }, + "execution": { + "jobs": 16, + "noTestsAction": "error" + } + } + ] +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..d120231 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,5 @@ +# Code of Conduct + +* You will be judged by your contributions first, and your sense of humor + second. +* Nobody owes you anything. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..10cccf3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,19 @@ +# Contributing + + + +## Code of Conduct + +Please see the [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) document. + +## Getting started + +Helpful notes for developers can be found in the [`HACKING.md`](HACKING.md) +document. + +In addition to he above, if you use the presets file as instructed, then you +should NOT check it into source control, just as the CMake documentation +suggests. diff --git a/HACKING.md b/HACKING.md new file mode 100644 index 0000000..11afadb --- /dev/null +++ b/HACKING.md @@ -0,0 +1,149 @@ +# Hacking + +Here is some wisdom to help you build and test this project as a developer and +potential contributor. + +If you plan to contribute, please read the [CONTRIBUTING](CONTRIBUTING.md) +guide. + +## Developer mode + +Build system targets that are only useful for developers of this project are +hidden if the `astar_DEVELOPER_MODE` option is disabled. Enabling this +option makes tests and other developer targets and options available. Not +enabling this option means that you are a consumer of this project and thus you +have no need for these targets and options. + +Developer mode is always set to on in CI workflows. + +### Presets + +This project makes use of [presets][1] to simplify the process of configuring +the project. As a developer, you are recommended to always have the [latest +CMake version][2] installed to make use of the latest Quality-of-Life +additions. + +You have a few options to pass `astar_DEVELOPER_MODE` to the configure +command, but this project prefers to use presets. + +As a developer, you should create a `CMakeUserPresets.json` file at the root of +the project: + +```json +{ + "version": 2, + "cmakeMinimumRequired": { + "major": 3, + "minor": 14, + "patch": 0 + }, + "configurePresets": [ + { + "name": "dev", + "binaryDir": "${sourceDir}/build/dev", + "inherits": ["dev-mode", "ci-"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + } + ], + "buildPresets": [ + { + "name": "dev", + "configurePreset": "dev", + "configuration": "Debug" + } + ], + "testPresets": [ + { + "name": "dev", + "configurePreset": "dev", + "configuration": "Debug", + "output": { + "outputOnFailure": true + } + } + ] +} +``` + +You should replace `` in your newly created presets file with the name of +the operating system you have, which may be `win64`, `linux` or `darwin`. You +can see what these correspond to in the +[`CMakePresets.json`](CMakePresets.json) file. + +`CMakeUserPresets.json` is also the perfect place in which you can put all +sorts of things that you would otherwise want to pass to the configure command +in the terminal. + +> **Note** +> Some editors are pretty greedy with how they open projects with presets. +> Some just randomly pick a preset and start configuring without your consent, +> which can be confusing. Make sure that your editor configures when you +> actually want it to, for example in CLion you have to make sure only the +> `dev-dev preset` has `Enable profile` ticked in +> `File > Settings... > Build, Execution, Deployment > CMake` and in Visual +> Studio you have to set the option `Never run configure step automatically` +> in `Tools > Options > CMake` **prior to opening the project**, after which +> you can manually configure using `Project > Configure Cache`. + +### Configure, build and test + +If you followed the above instructions, then you can configure, build and test +the project respectively with the following commands from the project root on +any operating system with any build system: + +```sh +cmake --preset=dev +cmake --build --preset=dev +ctest --preset=dev +``` + +If you are using a compatible editor (e.g. VSCode) or IDE (e.g. CLion, VS), you +will also be able to select the above created user presets for automatic +integration. + +Please note that both the build and test commands accept a `-j` flag to specify +the number of jobs to use, which should ideally be specified to the number of +threads your CPU has. You may also want to add that to your preset using the +`jobs` property, see the [presets documentation][1] for more details. + +### Developer mode targets + +These are targets you may invoke using the build command from above, with an +additional `-t ` flag: + +#### `coverage` + +Available if `ENABLE_COVERAGE` is enabled. This target processes the output of +the previously run tests when built with coverage configuration. The commands +this target runs can be found in the `COVERAGE_TRACE_COMMAND` and +`COVERAGE_HTML_COMMAND` cache variables. The trace command produces an info +file by default, which can be submitted to services with CI integration. The +HTML command uses the trace command's output to generate an HTML document to +`/coverage_html` by default. + +#### `docs` + +Available if `BUILD_MCSS_DOCS` is enabled. Builds to documentation using +Doxygen and m.css. The output will go to `/docs` by default +(customizable using `DOXYGEN_OUTPUT_DIRECTORY`). + +#### `format-check` and `format-fix` + +These targets run the clang-format tool on the codebase to check errors and to +fix them respectively. Customization available using the `FORMAT_PATTERNS` and +`FORMAT_COMMAND` cache variables. + +#### `run-examples` + +Runs all the examples created by the `add_example` command. + +#### `spell-check` and `spell-fix` + +These targets run the codespell tool on the codebase to check errors and to fix +them respectively. Customization available using the `SPELL_COMMAND` cache +variable. + +[1]: https://cmake.org/cmake/help/latest/manual/cmake-presets.7.html +[2]: https://cmake.org/download/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..af00372 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Bensuperpc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100755 index 0000000..63dfc16 --- /dev/null +++ b/Makefile @@ -0,0 +1,162 @@ +#////////////////////////////////////////////////////////////// +#// ____ // +#// | __ ) ___ _ __ ___ _ _ _ __ ___ _ __ _ __ ___ // +#// | _ \ / _ \ '_ \/ __| | | | '_ \ / _ \ '__| '_ \ / __| // +#// | |_) | __/ | | \__ \ |_| | |_) | __/ | | |_) | (__ // +#// |____/ \___|_| |_|___/\__,_| .__/ \___|_| | .__/ \___| // +#// |_| |_| // +#////////////////////////////////////////////////////////////// +#// // +#// sandbox, 2023 // +#// Created: 04, June, 2021 // +#// Modified: 18, November, 2023 // +#// file: - // +#// - // +#// Source: // +#// OS: ALL // +#// CPU: ALL // +#// // +#////////////////////////////////////////////////////////////// + +PROJECT_NAME := world_of_blocks + +PARALLEL := 1 + +GENERATOR := Ninja +PROJECT_ROOT := . + +CTEST_TIMEOUT := 1500 +CTEST_OPTIONS := --output-on-failure --timeout $(CTEST_TIMEOUT) --parallel $(PARALLEL) --verbose + +# LANG := en +# LANG=$(LANG) +# -Werror=float-equal + +.PHONY: build +build: base + +.PHONY: all +all: release debug minsizerel coverage relwithdebinfo minsizerel relwithdebinfo release-clang \ + debug-clang base base-clang sanitize sanitize-clang gprof $(DOCKCROSS_IMAGE) docker valgrind gdb + +.PHONY: base +base: + cmake -B build/$@ -S $(PROJECT_ROOT) -G $(GENERATOR) --preset=$@ + cmake --build build/$@ + ctest $(CTEST_OPTIONS) --test-dir build/$@ + +.PHONY: base-clang +base-clang: + cmake -B build/$@ -S $(PROJECT_ROOT) -G $(GENERATOR) --preset=$@ + cmake --build build/$@ + ctest $(CTEST_OPTIONS) --test-dir build/$@ + +.PHONY: release +release: + cmake -B build/$@ -S $(PROJECT_ROOT) -G $(GENERATOR) --preset=base -DCMAKE_BUILD_TYPE=Release + cmake --build build/$@ + ctest $(CTEST_OPTIONS) --test-dir build/$@ + +.PHONY: release-clang +release-clang: + cmake -B build/$@ -S $(PROJECT_ROOT) -G $(GENERATOR) --preset=base -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ + cmake --build build/$@ + ctest $(CTEST_OPTIONS) --test-dir build/$@ + +.PHONY: debug +debug: + cmake -B build/$@ -S $(PROJECT_ROOT) -G $(GENERATOR) --preset=dev -DCMAKE_BUILD_TYPE=Debug + cmake --build build/$@ + ctest $(CTEST_OPTIONS) --test-dir build/$@ + +.PHONY: debug-clang +debug-clang: + cmake -B build/$@ -S $(PROJECT_ROOT) -G $(GENERATOR) --preset=dev -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ + cmake --build build/$@ + ctest $(CTEST_OPTIONS) --test-dir build/$@ + +.PHONY: coverage +coverage: + cmake -B build/$@ -S $(PROJECT_ROOT) -G $(GENERATOR) --preset=dev-coverage -DCMAKE_BUILD_TYPE=Coverage + cmake --build build/$@ + ctest $(CTEST_OPTIONS) --test-dir build/$@ + cmake --build build/$@ --target $@ + +.PHONY: sanitize +sanitize: + cmake -B build/$@ -S $(PROJECT_ROOT) -G $(GENERATOR) --preset=ci-sanitize + cmake --build build/$@ + ctest $(CTEST_OPTIONS) --test-dir build/$@ + +sanitize-clang: + cmake -B build/$@ -S $(PROJECT_ROOT) -G $(GENERATOR) --preset=ci-sanitize \ + -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ + cmake --build build/$@ + ctest $(CTEST_OPTIONS) --test-dir build/$@ + +.PHONY: minsizerel +minsizerel: + cmake -B build/$@ -S $(PROJECT_ROOT) -G $(GENERATOR) --preset=dev -DCMAKE_BUILD_TYPE=MinSizeRel + cmake --build build/$@ + ctest $(CTEST_OPTIONS) --test-dir build/$@ + +.PHONY: relwithdebinfo +relwithdebinfo: + cmake -B build/$@ -S $(PROJECT_ROOT) -G $(GENERATOR) --preset=dev -DCMAKE_BUILD_TYPE=RelWithDebInfo + cmake --build build/$@ + ctest $(CTEST_OPTIONS) --test-dir build/$@ + +.PHONY: gprof +gprof: + cmake --preset=$@ -G $(GENERATOR) + cmake --build build/$@ + @echo "Run executable and after gprof gmon.out | less" + +.PHONY: perf +perf: + cmake --preset=base -G $(GENERATOR) + cmake --build build/base + perf record --all-user -e branch-misses ./build/base/bin/$(PROJECT_NAME) + +.PHONY: graph +graph: + cmake -B build/$@ -S $(PROJECT_ROOT) -G $(GENERATOR) --graphviz=build/$@/graph.dot + cmake --build build/base + dot -Tpng -o build/$@/graph.png build/$@/graph.dot + +.PHONY: valgrind +valgrind: + cmake -B build/$@ -S $(PROJECT_ROOT) -G $(GENERATOR) --preset=debugger + cmake --build build/$@ + valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes --verbose --log-file=build/$@/valgrind.log ./build/$@/bin/$(PROJECT_NAME) + +.PHONY: gdb +gdb: + cmake -B build/$@ -S $(PROJECT_ROOT) -G $(GENERATOR) --preset=debugger + cmake --build build/$@ + gdb build/$@/bin/$(PROJECT_NAME) + +.PHONY: lint +lint: + cmake -D FORMAT_COMMAND=clang-format -P cmake/lint.cmake + cmake -P cmake/spell.cmake + +.PHONY: format +format: + time find . -regex '.*\.\(cpp\|cxx\|hpp\|hxx\|c\|h\|cu\|cuh\|cuhpp\|tpp\)' -not -path '*/build/*' -not -path '.git/*' | parallel clang-format -style=file -i {} \; + +.PHONY: cloc +cloc: + cloc --fullpath --not-match-d="(build|.git)" --not-match-f="(.git)" . + +.PHONY: update +update: +# git submodule update --recursive --remote --force --rebase + git submodule update --init --recursive + git pull --recurse-submodules --all --progress + +.PHONY: clear +clear: + rm -rf build/* diff --git a/README.md b/README.md new file mode 100755 index 0000000..7aadb47 --- /dev/null +++ b/README.md @@ -0,0 +1,207 @@ +# astar + +Fast and easy to use header only 2D astar algorithm library in C++20. + +I made it for learning how the astar algorithm works, try to make the fastest, tested and configurable as possible for my needs (future games and works). + +# How does it work + +It is an [astar algorithm](https://en.wikipedia.org/wiki/A*_search_algorithm), the main idea is to find the shortest path between two points in a grid/map. + +# Screenshots + +![astar](resources/Screenshot_20240128_093812.png) + +# Features + +* [x] Header-only library C++20 +* [x] Support 2D map +* [ ] Support 3D map +* [x] Configurable heuristic function and movement cost +* [x] Configurable (diagonal and more) movement +* [x] Debug mode in template argument and lambda function +* [x] Support direct access and not access to the map +* [x] Unit tests and benchmarks + +# How to use it + +This project is a header-only library and easy to use, just copy the `include/astar` folder in your project and include the `astar/astar.hpp` header or via CMake FetchContent_Declare. + +Now you can use the `Astar::Astar` class to find the shortest path between two points in a grid. + +```cpp +#include +#include + +auto main() -> int { + // Create the template class with optional a type (e.g. uint32_t) and a boolean + // if you want enable debug mode (AStar::AStar) + AStar::AStar pathFinder; + + // Define the map size (width, height) + pathFinder.setWorldSize({10, 10}); + + // Set the heuristic function (manhattan, euclidean, octagonal etc...), it is optional, default is euclidean + pathFinder.setHeuristic(AStar::Heuristic::manhattan); + + // if you want to enable diagonal movement, it is optional, default is false + pathFinder.setDiagonalMovement(true); + + // Add a obstacle point (5, 5) and (5, 6) + pathFinder.addObstacle({5, 5}); + pathFinder.addObstacle({5, 6}); + + // Find the path from (0, 0) to (9, 9) + auto path = pathFinder.findPath({0, 0}, {9, 9}); + + // Print the path + for (auto& p : path) { + std::cout << p.x << " " << p.y << std::endl; + } + + return 0; +} +``` + +### Alternative version (direct access to the map) + +You can use the alternative version of the library if you want astar have direct access to the map, this version is faster than the non-direct access version. + +```cpp +#include +#include + +auto main() -> int { + // Create the template class with optional a type (e.g. uint32_t) and a boolean + // if you want enable debug mode (AStar::AStar) + AStar::AStarFast pathFinder; + + // Set the heuristic function (manhattan, euclidean, octagonal etc...), it is optional, default is euclidean + pathFinder.setHeuristic(AStar::Heuristic::manhattan); + + // if you want to enable diagonal movement, it is optional, default is false + pathFinder.setDiagonalMovement(true); + + // Create world 9x9 filled with 0 + std::vector world(9 * 9, 0); + + // set lambda function to check if is an obstacle (value == 1) + auto isObstacle = [](uint32_t value) -> bool { return value == 1; }; + pathFinder.setObstacle(isObstacle); + + // Add a obstacle point (5, 5) and (5, 6) + world[5 + 5 * 9] = 1; + world[5 + 6 * 9] = 1; + + // Find the path from (0, 0) to (9, 9), it it equal to 0, then the path is not found + // This version of findPath() is faster due direct access to the world + auto path = pathFinder.findPath({0, 0}, {9, 9}, world, {9, 9}); + + // Print the path + for (auto& p : path) { + std::cout << p.x << " " << p.y << std::endl; + } + + return 0; +} +``` + +### Debug mode + +You can enable the debug mode to call a lambda function when new node is visiting by the algorithm and when new node is added to the open list. + +```cpp +#include + +#include + +auto main() -> int { + // Enable debug mode with template argument, this helps avoid performance issues on non-debug classes + AStar::AStar pathFinder; + + // Set lambda function to debug current node + std::function* node)> debugCurrentNode = [](const AStar::Node* node) { + std::cout << "Current node: " << node->pos.x << ", " << node->pos.y << std::endl; + }; + pathFinder.setDebugCurrentNode(debugCurrentNode); + + // Set lambda function to debug open node + std::function* node)> debugOpenNode = [](const AStar::Node* node) { + std::cout << "Add to open list: " << node->pos.x << ", " << node->pos.y << std::endl; + }; + pathFinder.setDebugOpenNode(debugOpenNode); + + // Define the map size (width, height) + pathFinder.setWorldSize({10, 10}); + + // Set the heuristic function (manhattan, euclidean, octagonal etc...), it is optional, default is euclidean + pathFinder.setHeuristic(AStar::Heuristic::manhattan); + + // if you want to enable diagonal movement, it is optional, default is false + pathFinder.setDiagonalMovement(true); + + // Add a obstacle point (5, 5) and (5, 6) + pathFinder.addObstacle({5, 5}); + pathFinder.addObstacle({5, 6}); + + // Find the path from (0, 0) to (9, 9) + auto path = pathFinder.findPath({0, 0}, {9, 9}); + + // Print the path + for (auto& p : path) { + std::cout << p.x << " " << p.y << std::endl; + } + + return 0; +} +``` + +# Building and installing + +See the [BUILDING](BUILDING.md) document. + +# Contributing + +See the [CONTRIBUTING](CONTRIBUTING.md) document. + +# Sources, references and ideas + +You can find here the sources, references, libs and ideas that I have used to make this library. + +## Astar + +Sources and references that I have used to make this library. + +* [Wikipedia A* search algorithm](https://en.wikipedia.org/wiki/A*_search_algorithm) +* [A* Pathfinding](https://www.youtube.com/watch?v=-L-WgKMFuhE) +* [AStar](https://github.com/yatima1460/AStar) +* [Introduction to A*](https://theory.stanford.edu/~amitp/GameProgramming/AStarComparison.html) +* [Easy A* (star) Pathfinding](https://medium.com/@nicholas.w.swift/easy-a-star-pathfinding-7e6689c7f7b2) +* [a-star](https://www.ce.unipr.it/people/medici/a-star.html)$ +* [A* Search Algorithm](https://yuminlee2.medium.com/a-search-algorithm-42c1a13fcf9f) + +## Bench others astar implementations + +The list of others astar implementations that I have benchmarked to compare the performance of my implementation. + +* [A* Search Algorithm](https://www.geeksforgeeks.org/a-search-algorithm/) +* [a-star](https://github.com/daancode/a-star) +* [A-Star-Search-Algorithm](https://github.com/lychengrex/A-Star-Search-Algorithm) +* [Pathfinding](https://github.com/Gerard097/Pathfinding) + +## Libraries + +Libraries used in this project. + +* [cmake-init](https://github.com/friendlyanon/cmake-init) +* [google test](https://github.com/google/googletest) +* [google benchmark](https://github.com/google/benchmark) +* [Raylib](https://github.com/raysan5/raylib) + +# Others + +* [Benchmark visualization](https://int-i.github.io/python/2021-11-07/matplotlib-google-benchmark-visualization/) + +# Licensing + +[LICENSE](LICENSE) diff --git a/cmake/coverage.cmake b/cmake/coverage.cmake new file mode 100644 index 0000000..c89cc16 --- /dev/null +++ b/cmake/coverage.cmake @@ -0,0 +1,33 @@ +# ---- Variables ---- + +# We use variables separate from what CTest uses, because those have +# customization issues +set( + COVERAGE_TRACE_COMMAND + lcov -c -q + -o "${PROJECT_BINARY_DIR}/coverage.info" + -d "${PROJECT_BINARY_DIR}" + --include "${PROJECT_SOURCE_DIR}/*" + CACHE STRING + "; separated command to generate a trace for the 'coverage' target" +) + +set( + COVERAGE_HTML_COMMAND + genhtml --legend -f -q + "${PROJECT_BINARY_DIR}/coverage.info" + -p "${PROJECT_SOURCE_DIR}" + -o "${PROJECT_BINARY_DIR}/coverage_html" + CACHE STRING + "; separated command to generate an HTML report for the 'coverage' target" +) + +# ---- Coverage target ---- + +add_custom_target( + coverage + COMMAND ${COVERAGE_TRACE_COMMAND} + COMMAND ${COVERAGE_HTML_COMMAND} + COMMENT "Generating coverage report" + VERBATIM +) diff --git a/cmake/dev-mode.cmake b/cmake/dev-mode.cmake new file mode 100644 index 0000000..0011f5c --- /dev/null +++ b/cmake/dev-mode.cmake @@ -0,0 +1,21 @@ +include(cmake/folders.cmake) + +include(CTest) +if(BUILD_TESTING) + add_subdirectory(test) +endif() + +option(BUILD_MCSS_DOCS "Build documentation using Doxygen and m.css" OFF) +if(BUILD_MCSS_DOCS) + include(cmake/docs.cmake) +endif() + +option(ENABLE_COVERAGE "Enable coverage support separate from CTest's" OFF) +if(ENABLE_COVERAGE) + include(cmake/coverage.cmake) +endif() + +include(cmake/lint-targets.cmake) +include(cmake/spell-targets.cmake) + +add_folders(Project) diff --git a/cmake/docs-ci.cmake b/cmake/docs-ci.cmake new file mode 100644 index 0000000..ae7f0c7 --- /dev/null +++ b/cmake/docs-ci.cmake @@ -0,0 +1,112 @@ +cmake_minimum_required(VERSION 3.14) + +foreach(var IN ITEMS PROJECT_BINARY_DIR PROJECT_SOURCE_DIR) + if(NOT DEFINED "${var}") + message(FATAL_ERROR "${var} must be defined") + endif() +endforeach() +set(bin "${PROJECT_BINARY_DIR}") +set(src "${PROJECT_SOURCE_DIR}") + +# ---- Dependencies ---- + +set(mcss_SOURCE_DIR "${bin}/docs/.ci") +if(NOT IS_DIRECTORY "${mcss_SOURCE_DIR}") + file(MAKE_DIRECTORY "${mcss_SOURCE_DIR}") + file( + DOWNLOAD + https://github.com/friendlyanon/m.css/releases/download/release-1/mcss.zip + "${mcss_SOURCE_DIR}/mcss.zip" + STATUS status + EXPECTED_MD5 00cd2757ebafb9bcba7f5d399b3bec7f + ) + if(NOT status MATCHES "^0;") + message(FATAL_ERROR "Download failed with ${status}") + endif() + execute_process( + COMMAND "${CMAKE_COMMAND}" -E tar xf mcss.zip + WORKING_DIRECTORY "${mcss_SOURCE_DIR}" + RESULT_VARIABLE result + ) + if(NOT result EQUAL "0") + message(FATAL_ERROR "Extraction failed with ${result}") + endif() + file(REMOVE "${mcss_SOURCE_DIR}/mcss.zip") +endif() + +find_program(Python3_EXECUTABLE NAMES python3 python) +if(NOT Python3_EXECUTABLE) + message(FATAL_ERROR "Python executable was not found") +endif() + +# ---- Process project() call in CMakeLists.txt ---- + +file(READ "${src}/CMakeLists.txt" content) + +string(FIND "${content}" "project(" index) +if(index EQUAL "-1") + message(FATAL_ERROR "Could not find \"project(\"") +endif() +string(SUBSTRING "${content}" "${index}" -1 content) + +string(FIND "${content}" "\n)\n" index) +if(index EQUAL "-1") + message(FATAL_ERROR "Could not find \"\\n)\\n\"") +endif() +string(SUBSTRING "${content}" 0 "${index}" content) + +file(WRITE "${bin}/docs-ci.project.cmake" "docs_${content}\n)\n") + +macro(list_pop_front list out) + list(GET "${list}" 0 "${out}") + list(REMOVE_AT "${list}" 0) +endmacro() + +function(docs_project name) + cmake_parse_arguments(PARSE_ARGV 1 "" "" "VERSION;DESCRIPTION;HOMEPAGE_URL" LANGUAGES) + set(PROJECT_NAME "${name}" PARENT_SCOPE) + if(DEFINED _VERSION) + set(PROJECT_VERSION "${_VERSION}" PARENT_SCOPE) + string(REGEX MATCH "^[0-9]+(\\.[0-9]+)*" versions "${_VERSION}") + string(REPLACE . ";" versions "${versions}") + set(suffixes MAJOR MINOR PATCH TWEAK) + while(NOT versions STREQUAL "" AND NOT suffixes STREQUAL "") + list_pop_front(versions version) + list_pop_front(suffixes suffix) + set("PROJECT_VERSION_${suffix}" "${version}" PARENT_SCOPE) + endwhile() + endif() + if(DEFINED _DESCRIPTION) + set(PROJECT_DESCRIPTION "${_DESCRIPTION}" PARENT_SCOPE) + endif() + if(DEFINED _HOMEPAGE_URL) + set(PROJECT_HOMEPAGE_URL "${_HOMEPAGE_URL}" PARENT_SCOPE) + endif() +endfunction() + +include("${bin}/docs-ci.project.cmake") + +# ---- Generate docs ---- + +if(NOT DEFINED DOXYGEN_OUTPUT_DIRECTORY) + set(DOXYGEN_OUTPUT_DIRECTORY "${bin}/docs") +endif() +set(out "${DOXYGEN_OUTPUT_DIRECTORY}") + +foreach(file IN ITEMS Doxyfile conf.py) + configure_file("${src}/docs/${file}.in" "${bin}/docs/${file}" @ONLY) +endforeach() + +set(mcss_script "${mcss_SOURCE_DIR}/documentation/doxygen.py") +set(config "${bin}/docs/conf.py") + +file(REMOVE_RECURSE "${out}/html" "${out}/xml") + +execute_process( + COMMAND "${Python3_EXECUTABLE}" "${mcss_script}" "${config}" + WORKING_DIRECTORY "${bin}/docs" + RESULT_VARIABLE result +) +if(NOT result EQUAL "0") + message(FATAL_ERROR "m.css returned with ${result}") +endif() diff --git a/cmake/docs.cmake b/cmake/docs.cmake new file mode 100644 index 0000000..c6cdda6 --- /dev/null +++ b/cmake/docs.cmake @@ -0,0 +1,46 @@ +# ---- Dependencies ---- + +set(extract_timestamps "") +if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.24") + set(extract_timestamps DOWNLOAD_EXTRACT_TIMESTAMP YES) +endif() + +include(FetchContent) +FetchContent_Declare( + mcss URL + https://github.com/friendlyanon/m.css/releases/download/release-1/mcss.zip + URL_MD5 00cd2757ebafb9bcba7f5d399b3bec7f + SOURCE_DIR "${PROJECT_BINARY_DIR}/mcss" + UPDATE_DISCONNECTED YES + ${extract_timestamps} +) +FetchContent_MakeAvailable(mcss) + +find_package(Python3 3.6 REQUIRED) + +# ---- Declare documentation target ---- + +set( + DOXYGEN_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/docs" + CACHE PATH "Path for the generated Doxygen documentation" +) + +set(working_dir "${PROJECT_BINARY_DIR}/docs") + +foreach(file IN ITEMS Doxyfile conf.py) + configure_file("docs/${file}.in" "${working_dir}/${file}" @ONLY) +endforeach() + +set(mcss_script "${mcss_SOURCE_DIR}/documentation/doxygen.py") +set(config "${working_dir}/conf.py") + +add_custom_target( + docs + COMMAND "${CMAKE_COMMAND}" -E remove_directory + "${DOXYGEN_OUTPUT_DIRECTORY}/html" + "${DOXYGEN_OUTPUT_DIRECTORY}/xml" + COMMAND "${Python3_EXECUTABLE}" "${mcss_script}" "${config}" + COMMENT "Building documentation using Doxygen and m.css" + WORKING_DIRECTORY "${working_dir}" + VERBATIM +) diff --git a/cmake/folders.cmake b/cmake/folders.cmake new file mode 100644 index 0000000..da7bd33 --- /dev/null +++ b/cmake/folders.cmake @@ -0,0 +1,21 @@ +set_property(GLOBAL PROPERTY USE_FOLDERS YES) + +# Call this function at the end of a directory scope to assign a folder to +# targets created in that directory. Utility targets will be assigned to the +# UtilityTargets folder, otherwise to the ${name}Targets folder. If a target +# already has a folder assigned, then that target will be skipped. +function(add_folders name) + get_property(targets DIRECTORY PROPERTY BUILDSYSTEM_TARGETS) + foreach(target IN LISTS targets) + get_property(folder TARGET "${target}" PROPERTY FOLDER) + if(DEFINED folder) + continue() + endif() + set(folder Utility) + get_property(type TARGET "${target}" PROPERTY TYPE) + if(NOT type STREQUAL "UTILITY") + set(folder "${name}") + endif() + set_property(TARGET "${target}" PROPERTY FOLDER "${folder}Targets") + endforeach() +endfunction() diff --git a/cmake/install-config.cmake b/cmake/install-config.cmake new file mode 100644 index 0000000..625e644 --- /dev/null +++ b/cmake/install-config.cmake @@ -0,0 +1 @@ +include("${CMAKE_CURRENT_LIST_DIR}/astarTargets.cmake") diff --git a/cmake/install-rules.cmake b/cmake/install-rules.cmake new file mode 100644 index 0000000..dc71a17 --- /dev/null +++ b/cmake/install-rules.cmake @@ -0,0 +1,66 @@ +if(PROJECT_IS_TOP_LEVEL) + set( + CMAKE_INSTALL_INCLUDEDIR "include/astar-${PROJECT_VERSION}" + CACHE STRING "" + ) + set_property(CACHE CMAKE_INSTALL_INCLUDEDIR PROPERTY TYPE PATH) +endif() + +# Project is configured with no languages, so tell GNUInstallDirs the lib dir +set(CMAKE_INSTALL_LIBDIR lib CACHE PATH "") + +include(CMakePackageConfigHelpers) +include(GNUInstallDirs) + +# find_package() call for consumers to find this project +set(package astar) + +install( + DIRECTORY include/ + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" + COMPONENT astar_Development +) + +install( + TARGETS astar_astar + EXPORT astarTargets + INCLUDES DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" +) + +write_basic_package_version_file( + "${package}ConfigVersion.cmake" + COMPATIBILITY SameMajorVersion + ARCH_INDEPENDENT +) + +# Allow package maintainers to freely override the path for the configs +set( + astar_INSTALL_CMAKEDIR "${CMAKE_INSTALL_DATADIR}/${package}" + CACHE STRING "CMake package config location relative to the install prefix" +) +set_property(CACHE astar_INSTALL_CMAKEDIR PROPERTY TYPE PATH) +mark_as_advanced(astar_INSTALL_CMAKEDIR) + +install( + FILES cmake/install-config.cmake + DESTINATION "${astar_INSTALL_CMAKEDIR}" + RENAME "${package}Config.cmake" + COMPONENT astar_Development +) + +install( + FILES "${PROJECT_BINARY_DIR}/${package}ConfigVersion.cmake" + DESTINATION "${astar_INSTALL_CMAKEDIR}" + COMPONENT astar_Development +) + +install( + EXPORT astarTargets + NAMESPACE astar:: + DESTINATION "${astar_INSTALL_CMAKEDIR}" + COMPONENT astar_Development +) + +if(PROJECT_IS_TOP_LEVEL) + include(CPack) +endif() diff --git a/cmake/lib/backward-cpp.cmake b/cmake/lib/backward-cpp.cmake new file mode 100644 index 0000000..fcbee5c --- /dev/null +++ b/cmake/lib/backward-cpp.cmake @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.14.0) + +include(FetchContent) + +FetchContent_Declare( + backward-cpp + GIT_REPOSITORY https://github.com/bombela/backward-cpp.git + GIT_TAG 0ddfadc4b0f5c53e63259fe804ee595e6f01f4df) # 23-10-2022 + +FetchContent_MakeAvailable(backward-cpp) + +# TODO: target_include_directories instead of include_directories +include_directories(${backward-cpp_SOURCE_DIR}) \ No newline at end of file diff --git a/cmake/lib/benchmark.cmake b/cmake/lib/benchmark.cmake new file mode 100755 index 0000000..75b03da --- /dev/null +++ b/cmake/lib/benchmark.cmake @@ -0,0 +1,70 @@ +cmake_minimum_required(VERSION 3.14.0) + +find_package(benchmark QUIET) + +if (NOT benchmark_FOUND) + message(STATUS "benchmark not found on system, downloading...") + include(FetchContent) + + set(CMAKE_CXX_CLANG_TIDY_TMP "${CMAKE_CXX_CLANG_TIDY}") + set(CMAKE_CXX_CLANG_TIDY "") + + FetchContent_Declare( + googlebenchmark + GIT_REPOSITORY https://github.com/google/benchmark.git + GIT_TAG ca8d0f7b613ac915cd6b161ab01b7be449d1e1cd + #GIT_SHALLOW TRUE + ) # 12-10-2023 + + # Disable tests on google benchmark + set(BENCHMARK_ENABLE_TESTING + OFF + CACHE BOOL "" FORCE) + set(BENCHMARK_ENABLE_WERROR + OFF + CACHE BOOL "" FORCE) + set(BENCHMARK_FORCE_WERROR + OFF + CACHE BOOL "" FORCE) + + set(BENCHMARK_ENABLE_INSTALL + OFF + CACHE BOOL "" FORCE) + + set(BENCHMARK_DOWNLOAD_DEPENDENCIES + ON + CACHE BOOL "" FORCE) + + set(BENCHMARK_CXX_LINKER_FLAGS + "" + CACHE STRING "" FORCE) + + set(BENCHMARK_CXX_LIBRARIES + "" + CACHE STRING "" FORCE) + + set(BENCHMARK_CXX_FLAGS + "" + CACHE STRING "" FORCE) + + set(CMAKE_CXX_FLAGS_COVERAGE + "" + CACHE STRING "" FORCE) + + set(CMAKE_REQUIRED_FLAGS + "" + CACHE STRING "" FORCE) + + FetchContent_MakeAvailable(googlebenchmark) + # Lib: benchmark::benchmark benchmark::benchmark_main + + set(CMAKE_CXX_CLANG_TIDY "${CMAKE_CXX_CLANG_TIDY_TMP}") + + set_target_properties(benchmark + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" + ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" + PDB_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" + ) +endif() diff --git a/cmake/lib/boost.cmake b/cmake/lib/boost.cmake new file mode 100755 index 0000000..ab816d0 --- /dev/null +++ b/cmake/lib/boost.cmake @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.14.0) + +include(FetchContent) + +if(NOT DEFINED BOOST_INCLUDE_LIBRARIES) + set(BOOST_INCLUDE_LIBRARIES system) +endif() + + +if(NOT DEFINED BOOST_ENABLE_CMAKE) + set(BOOST_ENABLE_CMAKE ON) +endif() + + +FetchContent_Declare( + Boost + GIT_REPOSITORY https://github.com/boostorg/boost.git + GIT_TAG boost-1.81.0 + #GIT_SHALLOW TRUE +) +FetchContent_MakeAvailable(Boost) + diff --git a/cmake/lib/drogon.cmake b/cmake/lib/drogon.cmake new file mode 100644 index 0000000..b242f57 --- /dev/null +++ b/cmake/lib/drogon.cmake @@ -0,0 +1,15 @@ + + +# https://github.com/drogonframework/drogon/issues/1288#issuecomment-1163902139 +FetchContent_Declare(drogon + GIT_REPOSITORY https://github.com/drogonframework/drogon.git + GIT_TAG v1.8.4 # 08-04-2023 +) + +# Reset CXX_FLAGS to avoid warnings from drogon +set(CMAKE_CXX_FLAGS_OLD "${CMAKE_CXX_FLAGS}") +set(CMAKE_CXX_FLAGS "-std=c++17 -O3") + +FetchContent_MakeAvailable(drogon) + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS_OLD}") \ No newline at end of file diff --git a/cmake/lib/fast_noise2.cmake b/cmake/lib/fast_noise2.cmake new file mode 100644 index 0000000..925c474 --- /dev/null +++ b/cmake/lib/fast_noise2.cmake @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.14.0) + +include(FetchContent) + +set(FASTNOISE2_NOISETOOL OFF CACHE BOOL "Build Noise Tool" FORCE) + +FetchContent_Declare(FastNoise2 + GIT_REPOSITORY https://github.com/Auburn/FastNoise2.git + GIT_TAG 0928ca22cd4cfd50e9b17cec4fe9d867b59c3943 # 2023-06-07 +) +FetchContent_MakeAvailable(FastNoise2) diff --git a/cmake/lib/gtest.cmake b/cmake/lib/gtest.cmake new file mode 100755 index 0000000..72b3bd3 --- /dev/null +++ b/cmake/lib/gtest.cmake @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.14.0) + +find_package(GTest QUIET) + +if (NOT GTEST_FOUND) + message(STATUS "GTest not found on system, downloading...") + include(FetchContent) + + FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG 2dd1c131950043a8ad5ab0d2dda0e0970596586a) # 12-10-2023 + + # Disable tests on gtest + set(gtest_build_tests + OFF + CACHE BOOL "" FORCE) + set(gtest_build_samples + OFF + CACHE BOOL "" FORCE) + + FetchContent_MakeAvailable(googletest) + # Lib: gtest gtest_main + + set_target_properties(gtest + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" + ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" + PDB_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" + ) +endif() diff --git a/cmake/lib/json.cmake b/cmake/lib/json.cmake new file mode 100755 index 0000000..5d563e8 --- /dev/null +++ b/cmake/lib/json.cmake @@ -0,0 +1,31 @@ +cmake_minimum_required(VERSION 3.14.0) + +find_package(nlohmann_json QUIET) + +if (NOT nlohmann_json_FOUND) + message(STATUS "nlohmann_json not found on system, downloading...") + include(FetchContent) + + #set(CMAKE_MODULE_PATH + # "" + # CACHE STRING "" FORCE) + + #set(NLOHMANN_JSON_SYSTEM_INCLUDE + # "" + # CACHE STRING "" FORCE) + + FetchContent_Declare(nlohmann_json + GIT_REPOSITORY https://github.com/nlohmann/json.git + GIT_TAG f56c6e2e30241b9245161a86ae9fecf6543bf411 # 2023-11-26 + ) + FetchContent_MakeAvailable(nlohmann_json) + # nlohmann_json::nlohmann_json + set_target_properties(nlohmann_json + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" + ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" + PDB_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" + ) + include_directories(${nlohmann_json_SOURCE_DIR}/include) +endif() diff --git a/cmake/lib/opencv.cmake b/cmake/lib/opencv.cmake new file mode 100644 index 0000000..9e5005b --- /dev/null +++ b/cmake/lib/opencv.cmake @@ -0,0 +1,42 @@ +cmake_minimum_required(VERSION 3.14.0) + +include(FetchContent) +set(OpenCV_DIR ${CMAKE_CURRENT_BINARY_DIR}) + +find_package(OpenCV QUIET) + +if (NOT OpenCV_FOUND) + #set(OpenCV_STATIC ON) + set(BUILD_EXAMPLES CACHE BOOL OFF) + set(BUILD_DOCS CACHE BOOL OFF) + set(BUILD_TESTS CACHE BOOL OFF) + set(BUILD_PERF_TESTS CACHE BOOL OFF) + #set(BUILD_PACKAGE CACHE BOOL OFF) + + + set(BUILD_opencv_apps CACHE BOOL OFF) + + FetchContent_Declare( + OpenCV + GIT_REPOSITORY https://github.com/opencv/opencv.git + GIT_TAG 4.7.0 + #GIT_SHALLOW TRUE + GIT_PROGRESS TRUE + ) + FetchContent_MakeAvailable(OpenCV) + #set(OpenCV_DIR ${CMAKE_CURRENT_BINARY_DIR}) + #include_directories(${OpenCV_INCLUDE_DIRS}) + #message(FATAL_ERROR "OpenCV_INCLUDE_DIRS: ${OpenCV_INCLUDE_DIRS}") + #find_package(OpenCV REQUIRED) + + #include_directories(${OpenCV_INCLUDE_DIRS}) + #target_include_directories("${NAME}" PRIVATE + #${OPENCV_CONFIG_FILE_INCLUDE_DIR} + #${OPENCV_MODULE_opencv_core_LOCATION}/include + #${OPENCV_MODULE_opencv_highgui_LOCATION}/include + #) + #target_link_libraries("${NAME}" PRIVATE opencv_core opencv_highgui) + #target_link_libraries("${NAME}" PRIVATE ${OpenCV_LIBS}) + #opencv_add_module() + +endif() diff --git a/cmake/lib/openmp.cmake b/cmake/lib/openmp.cmake new file mode 100755 index 0000000..58b49eb --- /dev/null +++ b/cmake/lib/openmp.cmake @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.14.0) + +find_package(OpenMP) +if(OpenMP_CXX_FOUND) + # message("OpenMP found") +endif() \ No newline at end of file diff --git a/cmake/lib/perlin_noise.cmake b/cmake/lib/perlin_noise.cmake new file mode 100755 index 0000000..c8957ad --- /dev/null +++ b/cmake/lib/perlin_noise.cmake @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.14.0) + +include(FetchContent) + +FetchContent_Declare(perlin_noise + GIT_REPOSITORY https://github.com/Reputeless/PerlinNoise.git + GIT_TAG bdf39fe92b2a585cdef485bcec2bca8ab5614095 # 2022-12-30 + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND "" + TEST_COMMAND "" +) +FetchContent_MakeAvailable(perlin_noise) +include_directories("${perlin_noise_SOURCE_DIR}") diff --git a/cmake/lib/pybind11.cmake b/cmake/lib/pybind11.cmake new file mode 100755 index 0000000..460c978 --- /dev/null +++ b/cmake/lib/pybind11.cmake @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.15) + +find_package(Python 3.8 COMPONENTS Interpreter Development REQUIRED) +find_package(pybind11) +# add_subdirectory(pybind11) + +if (NOT pybind11_FOUND) + include(FetchContent) + FetchContent_Declare( + pybind11 + GIT_REPOSITORY https://github.com/pybind/pybind11.git + GIT_TAG v2.10.3 + GIT_SHALLOW TRUE + ) + FetchContent_MakeAvailable(pybind11) +endif() + +#pybind11_add_module(${PROJECT_NAME} main.cpp) diff --git a/cmake/lib/raygui.cmake b/cmake/lib/raygui.cmake new file mode 100755 index 0000000..e69dae3 --- /dev/null +++ b/cmake/lib/raygui.cmake @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.14.0) + +include(FetchContent) + +set(BUILD_RAYLIB_CPP_EXAMPLES OFF CACHE BOOL "" FORCE) + +find_package(raygui QUIET) + +if (NOT raygui_FOUND) + FetchContent_Declare(raygui + GIT_REPOSITORY https://github.com/raysan5/raygui.git + GIT_TAG 4.0 + ) + FetchContent_MakeAvailable(raygui) + include_directories(${raygui_SOURCE_DIR}) + include_directories(${raygui_SOURCE_DIR}/src) +endif() \ No newline at end of file diff --git a/cmake/lib/raylib-cpp.cmake b/cmake/lib/raylib-cpp.cmake new file mode 100755 index 0000000..241a455 --- /dev/null +++ b/cmake/lib/raylib-cpp.cmake @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.14.0) + +include(FetchContent) + +#find_package(raylib_cpp QUIET) + +if (NOT raylib_cpp_FOUND) + FetchContent_Declare(raylib_cpp + GIT_REPOSITORY https://github.com/RobLoach/raylib-cpp.git + GIT_TAG v5.0.0 # 08-12-2023 + ) + FetchContent_MakeAvailable(raylib_cpp) +endif() \ No newline at end of file diff --git a/cmake/lib/raylib.cmake b/cmake/lib/raylib.cmake new file mode 100755 index 0000000..5dce797 --- /dev/null +++ b/cmake/lib/raylib.cmake @@ -0,0 +1,47 @@ +cmake_minimum_required(VERSION 3.14.0) + +find_package(raylib QUIET) + +if (NOT raylib_FOUND AND NOT FETCHCONTENT_FULLY_DISCONNECTED) + message(STATUS "raylib not found on system, downloading...") + + include(FetchContent) + + if(NOT DEFINED BUILD_EXAMPLES) + set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) + endif() + + if(NOT DEFINED BUILD_GAMES) + set(BUILD_GAMES OFF CACHE BOOL "" FORCE) + endif() + + if(NOT DEFINED INCLUDE_EVERYTHING) + set(INCLUDE_EVERYTHING ON CACHE BOOL "" FORCE) + endif() + + if(NOT DEFINED OPENGL_VERSION) + #set(OPENGL_VERSION OFF CACHE STRING "4.3" FORCE) + endif() + + #set (CMAKE_INSTALL_PREFIX ${CMAKE_CURRENT_SOURCE_DIR}/install CACHE PATH "default install path" FORCE) + #set (CMAKE_INSTALL_LIBDIR ${CMAKE_BINARY_DIR}/lib CACHE PATH "default install path" FORCE) + + #message(STATUS "CMAKE_INSTALL_LIBDIR: ${CMAKE_INSTALL_LIBDIR}") + FetchContent_Declare(raylib + GIT_REPOSITORY https://github.com/raysan5/raylib.git + GIT_TAG 5.0 # 08-12-2023 + ) + FetchContent_MakeAvailable(raylib) + + set_target_properties(raylib + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" + ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" + PDB_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" + ) + + set(raylib_FOUND TRUE) +else() + find_package(raylib 5.0.0 REQUIRED) +endif() \ No newline at end of file diff --git a/cmake/lib/spdlog.cmake b/cmake/lib/spdlog.cmake new file mode 100644 index 0000000..3787c4c --- /dev/null +++ b/cmake/lib/spdlog.cmake @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.14.0) + +include(FetchContent) + +FetchContent_Declare( + spdlog + GIT_REPOSITORY https://github.com/gabime/spdlog.git + GIT_TAG 7e635fca68d014934b4af8a1cf874f63989352b7) # 2023-07-09 + +FetchContent_MakeAvailable(spdlog) +include_directories("${spdlog_SOURCE_DIR}") \ No newline at end of file diff --git a/cmake/lib/threadpool.cmake b/cmake/lib/threadpool.cmake new file mode 100644 index 0000000..a1ddc65 --- /dev/null +++ b/cmake/lib/threadpool.cmake @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.14.0) + +include(FetchContent) + +FetchContent_Declare(bs-thread-pool + GIT_REPOSITORY https://github.com/bshoshany/thread-pool.git + GIT_TAG 6790920f61ab3e928ddaea835ab6a803d467f41d # 2023-12-28 + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND "" + TEST_COMMAND "" +) +FetchContent_MakeAvailable(bs-thread-pool) +include_directories("${bs-thread-pool_SOURCE_DIR}/include") diff --git a/cmake/lib/vector.cmake b/cmake/lib/vector.cmake new file mode 100755 index 0000000..e513321 --- /dev/null +++ b/cmake/lib/vector.cmake @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.14.0) + +include(FetchContent) + +FetchContent_Declare( + vector + GIT_REPOSITORY https://github.com/bensuperpc/vector.git + GIT_TAG 9febb9c84e7b73e6c621afd920dd3c8bb47a130c) # 2022-10-23 + +FetchContent_MakeAvailable(vector) diff --git a/cmake/lib/zlib.cmake b/cmake/lib/zlib.cmake new file mode 100755 index 0000000..9ee9918 --- /dev/null +++ b/cmake/lib/zlib.cmake @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.14.0) + +include(FetchContent) + +find_package(zlib QUIET) + +set(ZLIB_LIBRARY zlib) + +if (NOT zlib_FOUND) + FetchContent_Declare( + zlib + GIT_REPOSITORY https://github.com/madler/zlib.git + GIT_TAG v1.2.13 + ) + FetchContent_MakeAvailable(zlib) +endif() \ No newline at end of file diff --git a/cmake/lint-targets.cmake b/cmake/lint-targets.cmake new file mode 100644 index 0000000..244d521 --- /dev/null +++ b/cmake/lint-targets.cmake @@ -0,0 +1,34 @@ +set( + FORMAT_PATTERNS + source/*.cpp source/*.hpp + include/*.hpp + test/*.cpp test/*.hpp + example/*.cpp example/*.hpp + CACHE STRING + "; separated patterns relative to the project source dir to format" +) + +set(FORMAT_COMMAND clang-format CACHE STRING "Formatter to use") + +add_custom_target( + format-check + COMMAND "${CMAKE_COMMAND}" + -D "FORMAT_COMMAND=${FORMAT_COMMAND}" + -D "PATTERNS=${FORMAT_PATTERNS}" + -P "${PROJECT_SOURCE_DIR}/cmake/lint.cmake" + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" + COMMENT "Linting the code" + VERBATIM +) + +add_custom_target( + format-fix + COMMAND "${CMAKE_COMMAND}" + -D "FORMAT_COMMAND=${FORMAT_COMMAND}" + -D "PATTERNS=${FORMAT_PATTERNS}" + -D FIX=YES + -P "${PROJECT_SOURCE_DIR}/cmake/lint.cmake" + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" + COMMENT "Fixing the code" + VERBATIM +) diff --git a/cmake/lint.cmake b/cmake/lint.cmake new file mode 100644 index 0000000..c0d2725 --- /dev/null +++ b/cmake/lint.cmake @@ -0,0 +1,52 @@ +cmake_minimum_required(VERSION 3.14) + +macro(default name) + if(NOT DEFINED "${name}") + set("${name}" "${ARGN}") + endif() +endmacro() + +default(FORMAT_COMMAND clang-format) +default( + PATTERNS + source/*.cpp source/*.hpp + include/*.hpp + test/*.cpp test/*.hpp + example/*.cpp example/*.hpp +) +default(FIX NO) + +set(flag --output-replacements-xml) +set(args OUTPUT_VARIABLE output) +if(FIX) + set(flag -i) + set(args "") +endif() + +file(GLOB_RECURSE files ${PATTERNS}) +set(badly_formatted "") +set(output "") +string(LENGTH "${CMAKE_SOURCE_DIR}/" path_prefix_length) + +foreach(file IN LISTS files) + execute_process( + COMMAND "${FORMAT_COMMAND}" --style=file "${flag}" "${file}" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + RESULT_VARIABLE result + ${args} + ) + if(NOT result EQUAL "0") + message(FATAL_ERROR "'${file}': formatter returned with ${result}") + endif() + if(NOT FIX AND output MATCHES "\n") +endif() diff --git a/cmake/utile/ninja_color.cmake b/cmake/utile/ninja_color.cmake new file mode 100755 index 0000000..fbe8a8f --- /dev/null +++ b/cmake/utile/ninja_color.cmake @@ -0,0 +1,9 @@ + +option (FORCE_COLORED_OUTPUT "Always produce ANSI-colored output (GNU/Clang only)." TRUE) +if (${FORCE_COLORED_OUTPUT}) + if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") + add_compile_options (-fdiagnostics-color=always) + elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") + add_compile_options (-fcolor-diagnostics) + endif () +endif () diff --git a/cmake/variables.cmake b/cmake/variables.cmake new file mode 100644 index 0000000..6762989 --- /dev/null +++ b/cmake/variables.cmake @@ -0,0 +1,28 @@ +# ---- Developer mode ---- + +# Developer mode enables targets and code paths in the CMake scripts that are +# only relevant for the developer(s) of astar +# Targets necessary to build the project must be provided unconditionally, so +# consumers can trivially build and package the project +if(PROJECT_IS_TOP_LEVEL) + option(astar_DEVELOPER_MODE "Enable developer mode" OFF) +endif() + +# ---- Warning guard ---- + +# target_include_directories with the SYSTEM modifier will request the compiler +# to omit warnings from the provided paths, if the compiler supports that +# This is to provide a user experience similar to find_package when +# add_subdirectory or FetchContent is used to consume this project +set(warning_guard "") +if(NOT PROJECT_IS_TOP_LEVEL) + option( + astar_INCLUDES_WITH_SYSTEM + "Use SYSTEM modifier for astar's includes, disabling warnings" + ON + ) + mark_as_advanced(astar_INCLUDES_WITH_SYSTEM) + if(astar_INCLUDES_WITH_SYSTEM) + set(warning_guard SYSTEM) + endif() +endif() diff --git a/codespell.ignore-words.txt b/codespell.ignore-words.txt new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/codespell.ignore-words.txt @@ -0,0 +1 @@ + diff --git a/docs/Doxyfile.in b/docs/Doxyfile.in new file mode 100644 index 0000000..dc37a2a --- /dev/null +++ b/docs/Doxyfile.in @@ -0,0 +1,32 @@ +# Configuration for Doxygen for use with CMake +# Only options that deviate from the default are included +# To create a new Doxyfile containing all available options, call `doxygen -g` + +# Get Project name and version from CMake +PROJECT_NAME = "@PROJECT_NAME@" +PROJECT_NUMBER = "@PROJECT_VERSION@" + +# Add sources +INPUT = "@PROJECT_SOURCE_DIR@/README.md" "@PROJECT_SOURCE_DIR@/include" "@PROJECT_SOURCE_DIR@/docs/pages" +EXTRACT_ALL = YES +RECURSIVE = YES +OUTPUT_DIRECTORY = "@DOXYGEN_OUTPUT_DIRECTORY@" + +# Use the README as a main page +USE_MDFILE_AS_MAINPAGE = "@PROJECT_SOURCE_DIR@/README.md" + +# set relative include paths +FULL_PATH_NAMES = YES +STRIP_FROM_PATH = "@PROJECT_SOURCE_DIR@/include" "@PROJECT_SOURCE_DIR@" +STRIP_FROM_INC_PATH = + +# We use m.css to generate the html documentation, so we only need XML output +GENERATE_XML = YES +GENERATE_HTML = NO +GENERATE_LATEX = NO +XML_PROGRAMLISTING = NO +CREATE_SUBDIRS = NO + +# Include all directories, files and namespaces in the documentation +# Disable to include only explicitly documented objects +M_SHOW_UNDOCUMENTED = YES diff --git a/docs/conf.py.in b/docs/conf.py.in new file mode 100644 index 0000000..b81e3d9 --- /dev/null +++ b/docs/conf.py.in @@ -0,0 +1,6 @@ +DOXYFILE = 'Doxyfile' + +LINKS_NAVBAR1 = [ + (None, 'pages', [(None, 'about')]), + (None, 'namespaces', []), +] diff --git a/docs/pages/about.dox b/docs/pages/about.dox new file mode 100644 index 0000000..2efbda9 --- /dev/null +++ b/docs/pages/about.dox @@ -0,0 +1,7 @@ +/** + * @page about About + * @section about-doxygen Doxygen documentation + * This page is auto generated using + * Doxygen, making use of some useful + * special commands. + */ diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt new file mode 100644 index 0000000..0cfbe1c --- /dev/null +++ b/example/CMakeLists.txt @@ -0,0 +1,27 @@ +cmake_minimum_required(VERSION 3.14) + +project(astarExamples CXX) + +include(../cmake/project-is-top-level.cmake) +include(../cmake/folders.cmake) + +if(PROJECT_IS_TOP_LEVEL) + find_package(astar REQUIRED) +endif() + +add_custom_target(run-examples) + +function(add_example NAME) + add_executable("${NAME}" "${NAME}.cpp") + target_link_libraries("${NAME}" PRIVATE astar::astar) + target_compile_features("${NAME}" PRIVATE cxx_std_20) + add_custom_target("run_${NAME}" COMMAND "${NAME}" VERBATIM) + add_dependencies("run_${NAME}" "${NAME}") + add_dependencies(run-examples "run_${NAME}") +endfunction() + +add_example(basic_example) +add_example(debug_example) +add_example(basic_fast_example) + +add_folders(Example) diff --git a/example/basic_example.cpp b/example/basic_example.cpp new file mode 100644 index 0000000..fc8c6ca --- /dev/null +++ b/example/basic_example.cpp @@ -0,0 +1,31 @@ +#include +#include + +auto main() -> int { + // Create the template class with optional a type (e.g. uint32_t) and a boolean + // if you want enable debug mode (AStar::AStar) + AStar::AStar pathFinder; + + // Define the map size (width, height) + pathFinder.setWorldSize({10, 10}); + + // Set the heuristic function (manhattan, euclidean, octagonal etc...), it is optional, default is euclidean + pathFinder.setHeuristic(AStar::Heuristic::manhattan); + + // if you want to enable diagonal movement, it is optional, default is false + pathFinder.setDiagonalMovement(true); + + // Add a obstacle point (5, 5) and (5, 6) + pathFinder.addObstacle({5, 5}); + pathFinder.addObstacle({5, 6}); + + // Find the path from (0, 0) to (9, 9), it it equal to 0, then the path is not found + auto path = pathFinder.findPath({0, 0}, {9, 9}); + + // Print the path + for (auto& p : path) { + std::cout << p.x << " " << p.y << std::endl; + } + + return 0; +} diff --git a/example/basic_fast_example.cpp b/example/basic_fast_example.cpp new file mode 100755 index 0000000..92eee04 --- /dev/null +++ b/example/basic_fast_example.cpp @@ -0,0 +1,36 @@ +#include +#include + +auto main() -> int { + // Create the template class with optional a type (e.g. uint32_t) and a boolean + // if you want enable debug mode (AStar::AStar) + AStar::AStarFast pathFinder; + + // Set the heuristic function (manhattan, euclidean, octagonal etc...), it is optional, default is euclidean + pathFinder.setHeuristic(AStar::Heuristic::manhattan); + + // if you want to enable diagonal movement, it is optional, default is false + pathFinder.setDiagonalMovement(true); + + // Create world 9x9 filled with 0 + std::vector world(9 * 9, 0); + + // set lambda function to check if is an obstacle (value == 1) + auto isObstacle = [](uint32_t value) -> bool { return value == 1; }; + pathFinder.setObstacle(isObstacle); + + // Add a obstacle point (5, 5) and (5, 6) + world[5 + 5 * 9] = 1; + world[5 + 6 * 9] = 1; + + // Find the path from (0, 0) to (9, 9), it it equal to 0, then the path is not found + // This version of findPath() is faster due direct access to the world + auto path = pathFinder.findPath({0, 0}, {9, 9}, world, {9, 9}); + + // Print the path + for (auto& p : path) { + std::cout << p.x << " " << p.y << std::endl; + } + + return 0; +} diff --git a/example/debug_example.cpp b/example/debug_example.cpp new file mode 100644 index 0000000..843b158 --- /dev/null +++ b/example/debug_example.cpp @@ -0,0 +1,43 @@ +#include + +#include + +auto main() -> int { + // Enable debug mode with template argument, this helps avoid performance issues on non-debug classes + AStar::AStar pathFinder; + + // Set lambda function to debug current node + std::function* node)> debugCurrentNode = [](const AStar::Node* node) { + std::cout << "Current node: " << node->pos.x << ", " << node->pos.y << std::endl; + }; + pathFinder.setDebugCurrentNode(debugCurrentNode); + + // Set lambda function to debug open node + std::function* node)> debugOpenNode = [](const AStar::Node* node) { + std::cout << "Add to open list: " << node->pos.x << ", " << node->pos.y << std::endl; + }; + pathFinder.setDebugOpenNode(debugOpenNode); + + // Define the map size (width, height) + pathFinder.setWorldSize({10, 10}); + + // Set the heuristic function (manhattan, euclidean, octagonal etc...), it is optional, default is euclidean + pathFinder.setHeuristic(AStar::Heuristic::manhattan); + + // if you want to enable diagonal movement, it is optional, default is false + pathFinder.setDiagonalMovement(true); + + // Add a obstacle point (5, 5) and (5, 6) + pathFinder.addObstacle({5, 5}); + pathFinder.addObstacle({5, 6}); + + // Find the path from (0, 0) to (9, 9) + auto path = pathFinder.findPath({0, 0}, {9, 9}); + + // Print the path + for (auto& p : path) { + std::cout << p.x << " " << p.y << std::endl; + } + + return 0; +} diff --git a/include/astar/astar.hpp b/include/astar/astar.hpp new file mode 100644 index 0000000..b4e03fa --- /dev/null +++ b/include/astar/astar.hpp @@ -0,0 +1,360 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +template +concept ArithmeticType = std::is_arithmetic::value; + +template +concept IntegerType = std::is_integral::value; + +template +concept FloatingPointType = std::is_floating_point::value; + +namespace AStar { + +template +class Vec2 { + public: + Vec2() = default; + Vec2(T x_, T y_) : x(x_), y(y_) {} + + bool operator==(const Vec2& pos) const noexcept { return (x == pos.x && y == pos.y); } + Vec2 operator=(const Vec2& pos) noexcept { + x = pos.x; + y = pos.y; + return *this; + } + Vec2 operator+(const Vec2& pos) noexcept { return {x + pos.x, y + pos.y}; } + Vec2 operator-(const Vec2& pos) noexcept { return {x - pos.x, y - pos.y}; } + Vec2 operator*(const Vec2& pos) noexcept { return {x * pos.x, y * pos.y}; } + Vec2 operator/(const Vec2& pos) noexcept { return {x / pos.x, y / pos.y}; } + struct hash { + size_t operator()(const Vec2& pos) const noexcept { return std::hash()(pos.x ^ (pos.y << 4)); } + }; + + T x = 0; + T y = 0; +}; +typedef Vec2 Vec2i; + +template +class Node { + public: + explicit Node() : pos(Vec2i(0, 0)), parentNode(nullptr) {} + explicit Node(const Vec2i& pos, Node* parent = nullptr) : pos(pos), parentNode(parent) {} + explicit Node(const Vec2i& pos, const T pathCost, const T heuristicCost, Node* parent = nullptr) + : pathCost(pathCost), heuristicCost(heuristicCost), pos(pos), parentNode(parent) {} + inline T getTotalCost() const noexcept { return pathCost + heuristicCost; } + struct hash { + size_t operator()(const Node* node) const noexcept { return std::hash()(node->pos.x ^ (node->pos.y << 4)); } + }; + + T pathCost = 0; + T heuristicCost = 0; + Vec2i pos = {0, 0}; + Node* parentNode = nullptr; +}; + +namespace Heuristic { +static inline Vec2i deltaVec(const Vec2i& source, const Vec2i& target) noexcept { + return {std::abs(source.x - target.x), std::abs(source.y - target.y)}; +} + +static inline uint32_t manhattan(const Vec2i& source, const Vec2i& target, const uint32_t weight) noexcept { + auto delta = deltaVec(source, target); + return weight * (delta.x + delta.y); +} + +static inline uint32_t octagonal(const Vec2i& source, const Vec2i& target, const uint32_t weight) noexcept { + auto delta = deltaVec(source, target); + return weight * (delta.x + delta.y) + (-6) * std::min(delta.x, delta.y); +} + +static inline uint32_t euclidean(const Vec2i& source, const Vec2i& target, const uint32_t weight) noexcept { + auto delta = deltaVec(source, target); + return weight * static_cast(std::sqrt(std::pow(delta.x, 2) + std::pow(delta.y, 2))); +} + +static inline uint32_t chebyshev(const Vec2i& source, const Vec2i& target, const uint32_t weight) noexcept { + auto delta = deltaVec(source, target); + return weight * std::max(delta.x, delta.y); +} + +static inline uint32_t euclideanNoSQR(const Vec2i& source, const Vec2i& target, const uint32_t weight) noexcept { + auto delta = deltaVec(source, target); + return weight * static_cast(std::pow(delta.x, 2) + std::pow(delta.y, 2)); +} + +static constexpr uint32_t dijkstra([[maybe_unused]] const Vec2i& source, + [[maybe_unused]] const Vec2i& target, + const uint32_t weight = 0) noexcept { + return 0; +} +}; // namespace Heuristic + +template +class AStarVirtual { + public: + explicit AStarVirtual() + : _heuristicFunction(&Heuristic::euclidean), + _directionsCount(4), + _heuristicWeight(10), + _mouvemementCost(10), + _debugCurrentNode([](Node*) {}), + _debugOpenNode([](Node*) {}) { + _directions = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}, {1, 1}, {1, -1}, {-1, 1}, {-1, -1}}; + } + void setHeuristic(const std::function& heuristic) { + _heuristicFunction = std::bind(heuristic, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + } + std::function& getHeuristic() noexcept { return _heuristicFunction; } + + void setHeuristicWeight(const uint32_t weight) noexcept { _heuristicWeight = weight; } + uint32_t getHeuristicWeight() const noexcept { return _heuristicWeight; } + + void setDiagonalMovement(const bool enableDiagonalMovement) noexcept { + _directionsCount = (enableDiagonalMovement ? _directions.size() : _directions.size() / 2); + } + + void setMouvemementCost(const size_t cost) noexcept { _mouvemementCost = cost; } + size_t getMouvemementCost() const noexcept { return _mouvemementCost; } + + void setCustomDirections(const std::vector& directions) noexcept { + _directions = directions; + _directionsCount = static_cast(directions.size()); + } + + std::vector& getDirections() noexcept { return _directions; } + + void setDebugCurrentNode(const std::function*)>& debugCurrentNode) noexcept { _debugCurrentNode = debugCurrentNode; } + void setDebugOpenNode(const std::function*)>& debugOpenNode) noexcept { _debugOpenNode = debugOpenNode; } + + protected: + std::function _heuristicFunction; + std::vector _directions; + size_t _directionsCount; + T _heuristicWeight; + size_t _mouvemementCost = 10; + + // Only used if enableDebug is true + std::function*)> _debugCurrentNode; + std::function*)> _debugOpenNode; +}; + +template +class AStar final : public AStarVirtual { + public: + explicit AStar() {} + + std::vector findPath(const Vec2i source, const Vec2i& target) { + if (target.x < 0 || target.x >= _worldSize.x || target.y < 0 || target.y >= _worldSize.y) { + return {}; + } + + Node* currentNode = nullptr; + + auto compareFn = [](const Node* a, const Node* b) { return a->getTotalCost() > b->getTotalCost(); }; + std::priority_queue*, std::vector*>, decltype(compareFn)> openNodeVecPQueue = + std::priority_queue*, std::vector*>, decltype(compareFn)>(compareFn); + + std::unordered_map*, Vec2i::hash> openNodeMap; + std::unordered_map*, Vec2i::hash> closedNodeMap; + + openNodeVecPQueue.push(new Node(source)); + openNodeMap.insert({source, openNodeVecPQueue.top()}); + + while (!openNodeVecPQueue.empty()) { + currentNode = openNodeVecPQueue.top(); + + if constexpr (enableDebug) { + AStarVirtual::_debugCurrentNode(currentNode); + } + + if (currentNode->pos == target) { + break; + } + + openNodeVecPQueue.pop(); + openNodeMap.erase(currentNode->pos); + closedNodeMap.insert({currentNode->pos, currentNode}); + + for (size_t i = 0; i < AStarVirtual::_directionsCount; ++i) { + Vec2i newPos = currentNode->pos + AStarVirtual::_directions[i]; + + if (_obstacles.contains(newPos)) { + continue; + } + + if (closedNodeMap.contains(newPos)) { + continue; + } + + if (newPos.x < 0 || newPos.x >= _worldSize.x || newPos.y < 0 || newPos.y >= _worldSize.y) { + continue; + } + + T nextCost = currentNode->pathCost + AStarVirtual::_mouvemementCost; + Node* nextNode = openNodeMap.find(newPos) != openNodeMap.end() ? openNodeMap[newPos] : nullptr; + + if (nextNode == nullptr) { + nextNode = new Node(newPos, currentNode); + nextNode->pathCost = nextCost; + nextNode->heuristicCost = static_cast(AStarVirtual::_heuristicFunction( + nextNode->pos, target, AStarVirtual::_heuristicWeight)); + openNodeVecPQueue.push(nextNode); + openNodeMap.insert({nextNode->pos, nextNode}); + } else if (nextCost < nextNode->pathCost) { + nextNode->parentNode = currentNode; + nextNode->pathCost = nextCost; + } + + if constexpr (enableDebug) { + AStarVirtual::_debugOpenNode(nextNode); + } + } + } + + std::vector path; + + if (currentNode->pos == target) [[likely]] { + path.reserve(currentNode->getTotalCost() / 10); + while (currentNode != nullptr) { + path.push_back(currentNode->pos); + currentNode = currentNode->parentNode; + } + } + for (auto& [key, value] : openNodeMap) { + delete value; + } + + for (auto& [key, value] : closedNodeMap) { + delete value; + } + + return path; + } + + void addObstacle(const Vec2i& pos) { _obstacles.insert(pos); } + void removeObstacle(const Vec2i& pos) { _obstacles.erase(pos); } + std::unordered_set& getObstacles() noexcept { return _obstacles; } + + void clear() { _obstacles.clear(); } + void setWorldSize(const Vec2i& worldSize_) noexcept { _worldSize = worldSize_; } + + private: + std::unordered_set _obstacles; + Vec2i _worldSize = {0, 0}; +}; + +// Fast AStar are faster than normal AStar but use more ram and direct access to the map +template +class AStarFast final : public AStarVirtual { + public: + explicit AStarFast() : _isObstacleFunction([](U value) { return value == 1; }) {} + + // Same as AStar::findPath() but use direct access to the map + std::vector findPath(const Vec2i& source, const Vec2i& target, const std::vector& map, const Vec2i& worldSize) { + if (target.x < 0 || target.x >= worldSize.x || target.y < 0 || target.y >= worldSize.y) { + return {}; + } + + Node* currentNode = nullptr; + + auto compareFn = [](const Node* a, const Node* b) { return a->getTotalCost() > b->getTotalCost(); }; + std::priority_queue*, std::vector*>, decltype(compareFn)> openNodeVecPQueue = + std::priority_queue*, std::vector*>, decltype(compareFn)>(compareFn); + std::unordered_map*, Vec2i::hash> openNodeMap; + std::unordered_map*, Vec2i::hash> closedNodeMap; + + openNodeVecPQueue.push(new Node(source)); + openNodeMap.insert({source, openNodeVecPQueue.top()}); + + while (!openNodeVecPQueue.empty()) { + currentNode = openNodeVecPQueue.top(); + + if constexpr (enableDebug) { + AStarVirtual::_debugCurrentNode(currentNode); + } + + if (currentNode->pos == target) { + break; + } + + openNodeVecPQueue.pop(); + openNodeMap.erase(currentNode->pos); + closedNodeMap.insert({currentNode->pos, currentNode}); + + for (size_t i = 0; i < AStarVirtual::_directionsCount; ++i) { + Vec2i newPos = currentNode->pos + AStarVirtual::_directions[i]; + + if (_isObstacleFunction(map[newPos.x + newPos.y * worldSize.x])) { + continue; + } + + if (closedNodeMap.contains(newPos)) { + continue; + } + + if (newPos.x < 0 || newPos.x >= worldSize.x || newPos.y < 0 || newPos.y >= worldSize.y) { + continue; + } + + T nextCost = currentNode->pathCost + AStarVirtual::_mouvemementCost; + Node* nextNode = openNodeMap.find(newPos) != openNodeMap.end() ? openNodeMap[newPos] : nullptr; + if (nextNode == nullptr) { + nextNode = new Node(newPos, currentNode); + nextNode->pathCost = nextCost; + nextNode->heuristicCost = static_cast(AStarVirtual::_heuristicFunction( + nextNode->pos, target, AStarVirtual::_heuristicWeight)); + openNodeVecPQueue.push(nextNode); + openNodeMap.insert({nextNode->pos, nextNode}); + } else if (nextCost < nextNode->pathCost) [[likely]] { + nextNode->parentNode = currentNode; + nextNode->pathCost = nextCost; + } + + if constexpr (enableDebug) { + AStarVirtual::_debugOpenNode(nextNode); + } + } + } + + std::vector path; + + if (currentNode->pos == target) [[likely]] { + path.reserve(currentNode->getTotalCost() / 10); + while (currentNode != nullptr) { + path.push_back(currentNode->pos); + currentNode = currentNode->parentNode; + } + } + for (auto& [key, value] : openNodeMap) { + delete value; + } + + for (auto& [key, value] : closedNodeMap) { + delete value; + } + + return path; + } + void setObstacle(const std::function& isObstacleFunction) noexcept { _isObstacleFunction = isObstacleFunction; } + std::function& getObstacle() noexcept { return _isObstacleFunction; } + + private: + std::function _isObstacleFunction; +}; + +} // namespace AStar diff --git a/resources/Screenshot_20240128_093812.png b/resources/Screenshot_20240128_093812.png new file mode 100644 index 0000000..d8188cf Binary files /dev/null and b/resources/Screenshot_20240128_093812.png differ diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..19d8ce2 --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,82 @@ +cmake_minimum_required(VERSION 3.14) + +project(astarTests LANGUAGES C CXX) + +include(../cmake/project-is-top-level.cmake) +include(../cmake/folders.cmake) + +# ---- Dependencies ---- + +if(PROJECT_IS_TOP_LEVEL) + find_package(astar REQUIRED) + enable_testing() +endif() + + +function(test_bench_generator TEST_BENCH_NAME IS_TEST ADD_TO_TEST) + if (IS_TEST) + add_executable("${TEST_BENCH_NAME}" "source/test/${TEST_BENCH_NAME}.cpp" source/generator/generator.cpp source/generator/generator.hpp) + else() + add_executable("${TEST_BENCH_NAME}" "source/benchmark/${TEST_BENCH_NAME}.cpp" source/generator/generator.cpp source/generator/generator.hpp) + endif() + + + if (IS_TEST) + target_link_libraries("${TEST_BENCH_NAME}" PRIVATE gtest) + else() + target_link_libraries("${TEST_BENCH_NAME}" PRIVATE benchmark::benchmark) + endif() + + target_link_libraries("${TEST_BENCH_NAME}" PRIVATE astar::astar) + target_link_libraries("${TEST_BENCH_NAME}" PRIVATE raylib) + target_link_libraries("${TEST_BENCH_NAME}" PRIVATE FastNoise2) + #target_link_libraries("${TEST_BENCH_NAME}" PRIVATE spdlog::spdlog nlohmann_json::nlohmann_json) + + #if (OpenMP_FOUND OR OpenMP_CXX_FOUND) + # target_link_libraries("${TEST_BENCH_NAME}" PRIVATE OpenMP::OpenMP_CXX) + #endif() + + set_target_properties("${TEST_BENCH_NAME}" + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" + ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" + PDB_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" + ) + + #if(NOT CMAKE_BUILD_TYPE MATCHES Debug AND NOT CMAKE_BUILD_TYPE MATCHES Coverage) + # add_test(NAME "${TEST_BENCH_NAME}" COMMAND $) + #elseif() + # message(STATUS "Disable ${BENCH_NAME}, Performance benchmark test only run on Release/RelWithDebInfo/MinSizeRel") + #endif() + + if (ADD_TO_TEST) + add_test(NAME "${TEST_BENCH_NAME}" COMMAND $) + endif() + target_compile_features("${TEST_BENCH_NAME}" PRIVATE cxx_std_20) +endfunction() + +# ---- Tests ---- + +if(NOT WIN32) + include(../cmake/lib/gtest.cmake) + include(../cmake/lib/benchmark.cmake) + #include(../cmake/lib/openmp.cmake) + include(../cmake/lib/raygui.cmake) + + include(../cmake/lib/raylib.cmake) + include(../cmake/lib/fast_noise2.cmake) + #include(../cmake/lib/spdlog.cmake) + #include(../cmake/lib/json.cmake) + include(../cmake/utile/ccache.cmake) + + include_directories(source) + + test_bench_generator(astar_test true true) + test_bench_generator(astar_bench false true) + test_bench_generator(path_finder false false) +endif() + +# ---- End-of-file commands ---- + +add_folders(Test) diff --git a/test/source/benchmark/astar_bench.cpp b/test/source/benchmark/astar_bench.cpp new file mode 100644 index 0000000..a078308 --- /dev/null +++ b/test/source/benchmark/astar_bench.cpp @@ -0,0 +1,195 @@ +#include +#include +#include +#include + +#include +#include "astar/astar.hpp" +#include "generator/generator.hpp" + +static constexpr int64_t multiplier = 4; +static constexpr int64_t minRange = 16; +static constexpr int64_t maxRange = 256; +static constexpr int64_t minThreadRange = 1; +static constexpr int64_t maxThreadRange = 1; +static constexpr int64_t repetitions = 1; + +static void DoSetup([[maybe_unused]] const benchmark::State& state) {} + +static void DoTeardown([[maybe_unused]] const benchmark::State& state) {} + +template +static void astar_bench(benchmark::State& state) { + auto range = state.range(0); + + int mapWidth = range; + int mapHeight = range; + + float lacunarity = 1.6f; + float octaves = 6; + float gain = 3.5f; + float frequency = 1.7f; + float weightedStrength = 0.034f; + float multiplier = 118; + + Generator generator(-972960945); + benchmark::DoNotOptimize(generator); + generator.setLacunarity(lacunarity); + generator.setOctaves((uint32_t)octaves); + generator.setGain(gain); + generator.setFrequency(frequency); + generator.setWeightedStrength(weightedStrength); + generator.setMultiplier((uint32_t)multiplier); + + std::vector heightmap = generator.generate2dMeightmap(0, 0, 0, mapWidth, 0, mapHeight); + benchmark::DoNotOptimize(heightmap); + + std::vector blocks = std::vector(mapWidth * mapHeight, 0); + benchmark::DoNotOptimize(blocks); + + AStar::AStar pathFinder; + benchmark::DoNotOptimize(pathFinder); + pathFinder.setWorldSize({mapWidth, mapHeight}); + pathFinder.setHeuristic(AStar::Heuristic::euclidean); + pathFinder.setDiagonalMovement(true); + + for (uint64_t x = 0; x < mapWidth; x++) { + for (uint64_t y = 0; y < mapHeight; y++) { + uint64_t index = x + y * mapWidth; + uint8_t value = static_cast(heightmap[index]); + + if (value < 128) { + blocks[index] = 0; + } else { + blocks[index] = 1; + pathFinder.addObstacle({static_cast(x), static_cast(y)}); + } + } + } + + blocks[0] = 0; + pathFinder.removeObstacle({0, 0}); + blocks[mapWidth * mapHeight - 1] = 0; + pathFinder.removeObstacle({mapWidth - 1, mapHeight - 1}); + + AStar::Vec2i source(0, 0); + AStar::Vec2i target(mapWidth - 1, mapHeight - 1); + + std::vector path; + benchmark::DoNotOptimize(path); + + for (auto _ : state) { + path = pathFinder.findPath(source, target); + state.PauseTiming(); + if (path.size() == 0) { + state.SkipWithError("No path found"); + } + state.ResumeTiming(); + + benchmark::ClobberMemory(); + } + state.SetItemsProcessed(state.iterations()); + state.SetBytesProcessed(state.iterations() * sizeof(path)); +} + +BENCHMARK(astar_bench) + ->Name("astar_bench") + ->RangeMultiplier(multiplier) + ->Range(minRange, maxRange) + ->ThreadRange(minThreadRange, maxThreadRange) + ->Unit(benchmark::kNanosecond) + ->Setup(DoSetup) + ->Teardown(DoTeardown) + ->MeasureProcessCPUTime() + ->UseRealTime() + ->Repetitions(repetitions); + +template +static void astar_bench_fast(benchmark::State& state) { + auto range = state.range(0); + + int mapWidth = range; + int mapHeight = range; + + float lacunarity = 1.6f; + float octaves = 6; + float gain = 3.5f; + float frequency = 1.7f; + float weighted_strength = 0.034f; + float multiplier = 118; + + Generator generator(-972960945); + benchmark::DoNotOptimize(generator); + generator.setLacunarity(lacunarity); + generator.setOctaves((uint32_t)octaves); + generator.setGain(gain); + generator.setFrequency(frequency); + generator.setWeightedStrength(0.0f); + generator.setMultiplier((uint32_t)multiplier); + + std::vector heightmap = generator.generate2dMeightmap(0, 0, 0, mapWidth, 0, mapHeight); + benchmark::DoNotOptimize(heightmap); + + std::vector blocks = std::vector(mapWidth * mapHeight, 0); + benchmark::DoNotOptimize(blocks); + + AStar::AStarFast pathFinder; + benchmark::DoNotOptimize(pathFinder); + pathFinder.setHeuristic(AStar::Heuristic::euclidean); + pathFinder.setDiagonalMovement(true); + + for (uint64_t x = 0; x < mapWidth; x++) { + for (uint64_t y = 0; y < mapHeight; y++) { + uint64_t index = x + y * mapWidth; + uint8_t value = static_cast(heightmap[index]); + + if (value < 128) { + blocks[index] = 0; + } else { + blocks[index] = 1; + } + } + } + + blocks[0] = 0; + blocks[mapWidth * mapHeight - 1] = 0; + + AStar::Vec2i source(0, 0); + AStar::Vec2i target(mapWidth - 1, mapHeight - 1); + + std::vector path; + benchmark::DoNotOptimize(path); + + for (auto _ : state) { + path = pathFinder.findPath(source, target, blocks, {mapWidth, mapHeight}); + state.PauseTiming(); + if (path.size() == 0) { + state.SkipWithError("No path found"); + } + state.ResumeTiming(); + + benchmark::ClobberMemory(); + } + state.SetItemsProcessed(state.iterations()); + state.SetBytesProcessed(state.iterations() * sizeof(path)); +} + +BENCHMARK(astar_bench_fast) + ->Name("astar_bench_fast") + ->RangeMultiplier(multiplier) + ->Range(minRange, maxRange) + ->ThreadRange(minThreadRange, maxThreadRange) + ->Unit(benchmark::kNanosecond) + ->Setup(DoSetup) + ->Teardown(DoTeardown) + ->MeasureProcessCPUTime() + ->UseRealTime() + ->Repetitions(repetitions); + +// Run the benchmark +// BENCHMARK_MAIN(); + +int main(int argc, char** argv) { + ::benchmark::Initialize(&argc, argv); + ::benchmark::RunSpecifiedBenchmarks(); +} diff --git a/test/source/benchmark/path_finder.cpp b/test/source/benchmark/path_finder.cpp new file mode 100644 index 0000000..4253bb0 --- /dev/null +++ b/test/source/benchmark/path_finder.cpp @@ -0,0 +1,258 @@ +#include // std::array +#include // std::chrono::system_clock +#include // std::abs +#include // std::uint32_t +#include // std::cout, std::endl +#include // std::map +#include // std::unique_ptr +#include // std::random_device, std::mt19937, std::uniform_int_distribution +#include // std::vector + +#include "astar/astar.hpp" +#include "generator/generator.hpp" + +#include "raylib.h" + +#define RAYGUI_IMPLEMENTATION +extern "C" { +#include "src/raygui.h" +} + +auto main() -> int { + // Set log level for Raylib + SetTraceLogLevel(LOG_WARNING); + + const int screenWidth = 1920; + const int screenHeight = 1080; + + const int mapWidth = 192; + const int mapHeight = 108; + + const uint32_t targetFPS = 120; + + const uint32_t ImageUpdatePerSecond = 30; + + SetConfigFlags(FLAG_WINDOW_RESIZABLE | FLAG_MSAA_4X_HINT); + InitWindow(screenWidth, screenHeight, "Path finder by Bensuperpc"); + + SetTargetFPS(targetFPS); + + float lacunarity = 1.6f; + float octaves = 6; + float gain = 3.5f; + float frequency = 1.7f; + float weighted_strength = 0.034f; + float multiplier = 118; + + Generator generator_2(-972960945); + generator_2.setLacunarity(lacunarity); + generator_2.setOctaves((uint32_t)octaves); + generator_2.setGain(gain); + generator_2.setFrequency(frequency); + generator_2.setWeightedStrength(0.0f); + generator_2.setMultiplier((uint32_t)multiplier); + + AStar::AStar pathFinder; + pathFinder.setWorldSize({mapWidth, mapHeight}); + pathFinder.setHeuristic(AStar::Heuristic::manhattan); + pathFinder.setDiagonalMovement(true); + + size_t manhattanPathSize = 0; + size_t euclideanPathSize = 0; + size_t euclideanNoSQRPathSize = 0; + size_t octagonalPathSize = 0; + size_t chebyshevPathSize = 0; + size_t dijkstraPathSize = 0; + + std::vector heightmap; + + heightmap = generator_2.generate2dMeightmap(0, 0, 0, mapWidth, 0, mapHeight); + + std::vector blocks = std::vector(mapWidth * mapHeight, 0); + + uint64_t framesCounter = 0; + + bool needUpdate = true; + + while (!WindowShouldClose()) { + framesCounter++; + if (IsKeyPressed(KEY_S)) { + const std::string filename = "screenshot_" + std::to_string(std::chrono::system_clock::now().time_since_epoch().count()) + ".png"; + TakeScreenshot(filename.c_str()); + } + if (IsKeyPressed(KEY_R)) { + generator_2.randomizeSeed(); + needUpdate = true; + } + + if (framesCounter % (targetFPS / ImageUpdatePerSecond) == 0) { + if (needUpdate) { + needUpdate = false; + generator_2.setLacunarity(lacunarity); + generator_2.setOctaves((uint32_t)octaves); + generator_2.setGain(gain); + generator_2.setFrequency(frequency); + generator_2.setWeightedStrength(weighted_strength); + generator_2.setMultiplier((uint32_t)multiplier); + + pathFinder.clear(); + + heightmap = generator_2.generate2dMeightmap(0, 0, 0, screenWidth, 0, screenHeight); + + for (uint64_t x = 0; x < mapWidth; x++) { + for (uint64_t y = 0; y < mapHeight; y++) { + uint64_t index = x + y * mapWidth; + uint8_t value = static_cast(heightmap[index]); + + if (value < 128) { + blocks[index] = 0; + } else { + blocks[index] = 1; + pathFinder.addObstacle({static_cast(x), static_cast(y)}); + } + } + } + blocks[0] = 0; + pathFinder.removeObstacle({0, 0}); + blocks[mapWidth * mapHeight - 1] = 0; + pathFinder.removeObstacle({mapWidth - 1, mapHeight - 1}); + + pathFinder.setHeuristic(AStar::Heuristic::manhattan); + auto start1 = std::chrono::high_resolution_clock::now(); + auto path = pathFinder.findPath({0, 0}, {mapWidth - 1, mapHeight - 1}); + auto stop1 = std::chrono::high_resolution_clock::now(); + if (path.empty()) { + std::cout << "Path not found" << std::endl; + } + auto duration1 = std::chrono::duration_cast(stop1 - start1); + std::cout << "Path search: " << duration1.count() << " microseconds" << std::endl; + + manhattanPathSize = path.size(); + for (auto& i : path) { + uint64_t index = i.x + i.y * mapWidth; + blocks[index] = 2; + } + + pathFinder.setHeuristic(AStar::Heuristic::euclidean); + path = pathFinder.findPath({0, 0}, {mapWidth - 1, mapHeight - 1}); + euclideanPathSize = path.size(); + + for (auto& i : path) { + uint64_t index = i.x + i.y * mapWidth; + blocks[index] = 3; + } + + pathFinder.setHeuristic(AStar::Heuristic::octagonal); + path = pathFinder.findPath({0, 0}, {mapWidth - 1, mapHeight - 1}); + octagonalPathSize = path.size(); + + for (auto& i : path) { + uint64_t index = i.x + i.y * mapWidth; + blocks[index] = 4; + } + + pathFinder.setHeuristic(AStar::Heuristic::chebyshev); + path = pathFinder.findPath({0, 0}, {mapWidth - 1, mapHeight - 1}); + chebyshevPathSize = path.size(); + + for (auto& i : path) { + uint64_t index = i.x + i.y * mapWidth; + blocks[index] = 5; + } + + pathFinder.setHeuristic(AStar::Heuristic::euclideanNoSQR); + path = pathFinder.findPath({0, 0}, {mapWidth - 1, mapHeight - 1}); + euclideanNoSQRPathSize = path.size(); + + for (auto& i : path) { + uint64_t index = i.x + i.y * mapWidth; + blocks[index] = 6; + } + + pathFinder.setHeuristic(AStar::Heuristic::dijkstra); + path = pathFinder.findPath({0, 0}, {mapWidth - 1, mapHeight - 1}); + dijkstraPathSize = path.size(); + + for (auto& i : path) { + uint64_t index = i.x + i.y * mapWidth; + blocks[index] = 7; + } + } + } + + ClearBackground(RAYWHITE); + BeginDrawing(); + + // Draw white if blocks[index] == 0 else black + int size = 10; + for (uint64_t x = 0; x < mapWidth; x++) { + for (uint64_t y = 0; y < mapHeight; y++) { + uint64_t index = x + y * mapWidth; + if (blocks[index] == 0) { + DrawRectangle(static_cast(x * size), static_cast(y * size), size, size, WHITE); + } else if (blocks[index] == 1) { + DrawRectangle(static_cast(x * size), static_cast(y * size), size, size, BLACK); + } else if (blocks[index] == 2) { + DrawRectangle(static_cast(x * size), static_cast(y * size), size, size, RED); + } else if (blocks[index] == 3) { + DrawRectangle(static_cast(x * size), static_cast(y * size), size, size, GREEN); + } else if (blocks[index] == 4) { + DrawRectangle(static_cast(x * size), static_cast(y * size), size, size, BLUE); + } else if (blocks[index] == 5) { + DrawRectangle(static_cast(x * size), static_cast(y * size), size, size, YELLOW); + } else if (blocks[index] == 6) { + DrawRectangle(static_cast(x * size), static_cast(y * size), size, size, ORANGE); + } else if (blocks[index] == 7) { + DrawRectangle(static_cast(x * size), static_cast(y * size), size, size, PURPLE); + } + } + } + + // display FPS + DrawRectangle(screenWidth - 90, 10, 80, 20, Fade(SKYBLUE, 0.95f)); + DrawText(TextFormat("FPS: %02d", GetFPS()), screenWidth - 80, 15, 15, DARKGRAY); + + DrawRectangle(0, 0, 275, 200, Fade(SKYBLUE, 0.95f)); + DrawRectangleLines(0, 0, 275, 200, BLUE); + GuiSlider((Rectangle){70, 10, 165, 20}, "Lacunarity", TextFormat("%2.3f", lacunarity), &lacunarity, -0.0f, 5.0f); + GuiSlider((Rectangle){70, 40, 165, 20}, "Octaves", TextFormat("%2.3f", octaves), &octaves, 1, 12); + GuiSlider((Rectangle){70, 70, 165, 20}, "Gain", TextFormat("%2.3f", gain), &gain, -0.0f, 16.0f); + GuiSlider((Rectangle){70, 100, 165, 20}, "Frequency", TextFormat("%2.3f", frequency), &frequency, -0.0f, 10.0f); + GuiSlider((Rectangle){70, 130, 165, 20}, "Weight", TextFormat("%2.3f", weighted_strength), &weighted_strength, -5.0f, 5.0f); + GuiSlider((Rectangle){70, 160, 165, 20}, "Multiplier", TextFormat("%2.3f", multiplier), &multiplier, 1, 512); + + // display info each color for each heuristic + DrawRectangle(0, 200, 275, 190, Fade(SKYBLUE, 0.95f)); + DrawRectangleLines(0, 200, 275, 190, BLUE); + + std::string manhattanText = "Manhattan: " + std::to_string(manhattanPathSize); + DrawRectangle(10, 210, 20, 20, RED); + DrawText(manhattanText.c_str(), 40, 210, 20, DARKGRAY); + + std::string euclideanText = "Euclidean: " + std::to_string(euclideanPathSize); + DrawRectangle(10, 240, 20, 20, GREEN); + DrawText(euclideanText.c_str(), 40, 240, 20, DARKGRAY); + + std::string octagonalText = "Octagonal: " + std::to_string(octagonalPathSize); + DrawRectangle(10, 270, 20, 20, BLUE); + DrawText(octagonalText.c_str(), 40, 270, 20, DARKGRAY); + + std::string chebyshevText = "Chebyshev: " + std::to_string(chebyshevPathSize); + DrawRectangle(10, 300, 20, 20, YELLOW); + DrawText(chebyshevText.c_str(), 40, 300, 20, DARKGRAY); + + std::string euclideanNoSQRText = "EuclideanNoSQR: " + std::to_string(euclideanNoSQRPathSize); + DrawRectangle(10, 330, 20, 20, ORANGE); + DrawText(euclideanNoSQRText.c_str(), 40, 330, 20, DARKGRAY); + + std::string dijkstraText = "Dijkstra: " + std::to_string(dijkstraPathSize); + DrawRectangle(10, 360, 20, 20, PURPLE); + DrawText(dijkstraText.c_str(), 40, 360, 20, DARKGRAY); + + EndDrawing(); + } + + CloseWindow(); + + return 0; +} diff --git a/test/source/generator/generator.cpp b/test/source/generator/generator.cpp new file mode 100644 index 0000000..637b626 --- /dev/null +++ b/test/source/generator/generator.cpp @@ -0,0 +1,310 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// #include + +#include "generator.hpp" + +Generator::Generator(int32_t _seed) : seed(_seed) { + fnSimplex = FastNoise::New(); + fnFractal = FastNoise::New(); + + fnFractal->SetSource(fnSimplex); + fnFractal->SetOctaveCount(octaves); + fnFractal->SetGain(gain); + fnFractal->SetLacunarity(lacunarity); + fnFractal->SetWeightedStrength(weighted_strength); +} + +Generator::Generator() { + fnSimplex = FastNoise::New(); + fnFractal = FastNoise::New(); + + fnFractal->SetSource(fnSimplex); + fnFractal->SetOctaveCount(octaves); + fnFractal->SetGain(gain); + fnFractal->SetLacunarity(lacunarity); + fnFractal->SetWeightedStrength(weighted_strength); + + randomizeSeed(); +} + +Generator::~Generator() {} + +void Generator::reseed(int32_t _seed) { + this->seed = _seed; +} + +int32_t Generator::randomizeSeed() { + std::random_device rd; + std::mt19937_64 gen(rd()); + std::uniform_int_distribution dis(std::numeric_limits::min(), std::numeric_limits::max()); + this->seed = dis(gen); + + return seed; +} + +uint32_t Generator::get_seed() const { + return seed; +} + +void Generator::setOctaves(uint32_t _octaves) { + this->octaves = _octaves; + fnFractal->SetOctaveCount(octaves); +} + +uint32_t Generator::getOctaves() const { + return octaves; +} + +void Generator::setLacunarity(float _lacunarity) { + this->lacunarity = _lacunarity; + fnFractal->SetLacunarity(lacunarity); +} + +float Generator::getLacunarity() const { + return lacunarity; +} + +void Generator::setGain(float _gain) { + this->gain = _gain; + fnFractal->SetGain(gain); +} + +float Generator::getGain() const { + return gain; +} + +void Generator::setFrequency(float _frequency) { + this->frequency = _frequency; +} + +float Generator::getFrequency() const { + return frequency; +} + +void Generator::setWeightedStrength(float _weighted_strength) { + this->weighted_strength = _weighted_strength; + fnFractal->SetWeightedStrength(weighted_strength); +} + +float Generator::getWeightedStrength() const { + return weighted_strength; +} + +void Generator::setMultiplier(uint32_t _multiplier) { + this->multiplier = _multiplier; +} + +uint32_t Generator::getMultiplier() const { + return multiplier; +} + +std::vector Generator::generate2dMeightmap(const int32_t begin_x, + [[maybe_unused]] const int32_t begin_y, + const int32_t begin_z, + const uint32_t size_x, + [[maybe_unused]] const uint32_t size_y, + const uint32_t size_z) { + constexpr bool debug = false; + + std::vector heightmap(size_x * size_z); + + std::vector noise_output(size_x * size_z); + + if (fnFractal.get() == nullptr) { + std::cout << "fnFractal is nullptr" << std::endl; + return heightmap; + } + + fnFractal->GenUniformGrid2D(noise_output.data(), begin_x, begin_z, size_x, size_z, frequency, seed); + + // Convert noise_output to heightmap + for (uint32_t i = 0; i < size_x * size_z; i++) { + heightmap[i] = static_cast((noise_output[i] + 1.0) * multiplier); + if constexpr (debug) { + std::cout << "i: " << i << ", value: " << noise_output[i] << ", heightmap: " << heightmap[i] << std::endl; + } + } + + if constexpr (debug) { + // cout max and min + auto minmax = std::minmax_element(heightmap.begin(), heightmap.end()); + std::cout << "min: " << static_cast(*minmax.first) << std::endl; + std::cout << "max: " << static_cast(*minmax.second) << std::endl; + } + return heightmap; +} + +std::vector Generator::generate3dHeightmap(const int32_t begin_x, + const int32_t begin_y, + const int32_t begin_z, + const uint32_t size_x, + const uint32_t size_y, + const uint32_t size_z) { + constexpr bool debug = false; + + std::vector heightmap(size_x * size_y * size_z); + + std::vector noise_output(size_x * size_y * size_z); + + if (fnFractal.get() == nullptr) { + std::cout << "fnFractal is nullptr" << std::endl; + return heightmap; + } + + fnFractal->GenUniformGrid3D(noise_output.data(), begin_x, begin_y, begin_z, size_x, size_y, size_z, frequency, seed); + + // Convert noise_output to heightmap + for (uint32_t i = 0; i < size_x * size_y * size_z; i++) { + heightmap[i] = static_cast((noise_output[i] + 1.0) * multiplier); + if constexpr (debug) { + std::cout << "i: " << i << ", noise_output: " << noise_output[i] << ", heightmap: " << heightmap[i] << std::endl; + } + } + + if constexpr (debug) { + // cout max and min + auto minmax = std::minmax_element(heightmap.begin(), heightmap.end()); + std::cout << "min: " << static_cast(*minmax.first) << std::endl; + std::cout << "max: " << static_cast(*minmax.second) << std::endl; + } + + return heightmap; +} + +/* +std::unique_ptr Generator::generateChunk(const int32_t chunk_x, + const int32_t chunk_y, + const int32_t chunk_z, + const bool generate_3d_terrain) { + const int32_t real_x = chunk_x * Chunk::chunk_size_x; + const int32_t real_y = chunk_y * Chunk::chunk_size_y; + const int32_t real_z = chunk_z * Chunk::chunk_size_z; + + std::vector blocks; + + std::unique_ptr _chunk = std::make_unique(); + + if (generate_3d_terrain) { + blocks = std::move(generate3d(real_x, real_y, real_z, Chunk::chunk_size_x, Chunk::chunk_size_y, Chunk::chunk_size_z)); + } else { + blocks = std::move(generate2d(real_x, real_y, real_z, Chunk::chunk_size_x, Chunk::chunk_size_y, Chunk::chunk_size_z)); + } + + _chunk->set_blocks(blocks); + _chunk->set_chuck_pos(chunk_x, chunk_y, chunk_z); + + return _chunk; +} + +[[nodiscard]] std::vector> Generator::generateChunks(const int32_t begin_chunk_x, + const int32_t begin_chunk_y, + const int32_t begin_chunk_z, + const uint32_t size_x, + const uint32_t size_y, + const uint32_t size_z, + const bool generate_3d_terrain) { + constexpr bool debug = false; + + std::vector> chunks; + chunks.reserve(size_x * size_y * size_z); + +#pragma omp parallel for collapse(3) schedule(auto) + for (int32_t x = begin_chunk_x; x < begin_chunk_x + size_x; x++) { + for (int32_t z = begin_chunk_y; z < begin_chunk_y + size_z; z++) { + for (int32_t y = begin_chunk_z; y < begin_chunk_z + size_y; y++) { + auto gen_chunk = generateChunk(x, y, z, generate_3d_terrain); +#pragma omp critical + chunks.emplace_back(std::move(gen_chunk)); + } + } + } + + return chunks; +} +std::vector Generator::generate2d(const int32_t begin_x, + const int32_t begin_y, + const int32_t begin_z, + const uint32_t size_x, + const uint32_t size_y, + const uint32_t size_z) { + constexpr bool debug = false; + + std::vector heightmap; + std::vector blocks = std::vector(size_x * size_y * size_z, Block()); + + heightmap = std::move(generate2dMeightmap(begin_x, begin_y, begin_z, size_x, size_y, size_z)); + + // Generate blocks + for (uint32_t x = 0; x < size_x; x++) { + for (uint32_t z = 0; z < size_z; z++) { + // Noise value is divided by 4 to make it smaller and it is used as the height of the Block (z) + std::vector::size_type vec_index = math::convert_to_1d(x, z, size_x, size_z); + + uint32_t noise_value = heightmap[vec_index] / 4; + + for (uint32_t y = 0; y < size_y; y++) { + // Calculate real y from begin_y + vec_index = math::convert_to_1d(x, y, z, size_x, size_y, size_z); + + if constexpr (debug) { + std::cout << "x: " << x << ", z: " << z << ", y: " << y << " index: " << vec_index + << ", noise: " << static_cast(noise_value) << std::endl; + } + + Block& current_block = blocks[vec_index]; + + // If the noise value is greater than the current Block, make it air + if (noise_value > 120) { + current_block.block_type = block_type::stone; + continue; + } + } + } + } + return blocks; +} + +std::vector Generator::generate3d(const int32_t begin_x, + const int32_t begin_y, + const int32_t begin_z, + const uint32_t size_x, + const uint32_t size_y, + const uint32_t size_z) { + constexpr bool debug = false; + + std::vector blocks = std::vector(size_x * size_y * size_z, Block()); + + std::vector heightmap = generate3dHeightmap(begin_x, begin_y, begin_z, size_x, size_y, size_z); + + // Generate blocks + for (uint32_t x = 0; x < size_x; x++) { + for (uint32_t z = 0; z < size_z; z++) { + for (uint32_t y = 0; y < size_y; y++) { + size_t vec_index = math::convert_to_1d(x, y, z, size_x, size_y, size_z); + const uint32_t noise_value = heightmap[vec_index]; + auto& current_block = blocks[vec_index]; + + if constexpr (debug) { + std::cout << "x: " << x << ", z: " << z << ", y: " << y << " index: " << vec_index + << ", noise: " << static_cast(noise_value) << std::endl; + } + + if (noise_value > 120) { + current_block.block_type = block_type::stone; + continue; + } + } + } + } + return blocks; +} +*/ diff --git a/test/source/generator/generator.hpp b/test/source/generator/generator.hpp new file mode 100644 index 0000000..acd4e6d --- /dev/null +++ b/test/source/generator/generator.hpp @@ -0,0 +1,115 @@ +#ifndef WORLD_OF_CUBE_GENERATOR_HPP +#define WORLD_OF_CUBE_GENERATOR_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// #include + +#ifndef FASTNOISE_HPP_INCLUDED +#define FASTNOISE_HPP_INCLUDED +#ifdef __GNUC__ +#pragma GCC system_header +#endif +#ifdef __clang__ +#pragma clang system_header +#endif +#include "FastNoise/FastNoise.h" +#endif + +class Generator { + public: + explicit Generator(int32_t _seed); + + explicit Generator(); + + ~Generator(); + + void reseed(int32_t _seed); + + int32_t randomizeSeed(); + + uint32_t get_seed() const; + + void setOctaves(uint32_t _octaves); + + uint32_t getOctaves() const; + + void setLacunarity(float _lacunarity); + + float getLacunarity() const; + + void setGain(float _gain); + + float getGain() const; + + void setFrequency(float _frequency); + float getFrequency() const; + + void setWeightedStrength(float _weighted_strength); + float getWeightedStrength() const; + + void setMultiplier(uint32_t _multiplier); + + uint32_t getMultiplier() const; + + std::vector generate2dMeightmap(const int32_t begin_x, + [[maybe_unused]] const int32_t begin_y, + const int32_t begin_z, + const uint32_t size_x, + [[maybe_unused]] const uint32_t size_y, + const uint32_t size_z); + + std::vector generate3dHeightmap(const int32_t begin_x, + const int32_t begin_y, + const int32_t begin_z, + const uint32_t size_x, + const uint32_t size_y, + const uint32_t size_z); + /* + std::unique_ptr generateChunk(const int32_t chunk_x, const int32_t chunk_y, const int32_t chunk_z, const bool generate_3d_terrain); + + [[nodiscard]] std::vector> generateChunks(const int32_t begin_chunk_x, + const int32_t begin_chunk_y, + const int32_t begin_chunk_z, + const uint32_t size_x, + const uint32_t size_y, + const uint32_t size_z, + const bool generate_3d_terrain); + + std::vector generate2d(const int32_t begin_x, + const int32_t begin_y, + const int32_t begin_z, + const uint32_t size_x, + const uint32_t size_y, + const uint32_t size_z); + + std::vector generate3d(const int32_t begin_x, + const int32_t begin_y, + const int32_t begin_z, + const uint32_t size_x, + const uint32_t size_y, + const uint32_t size_z); + */ + private: + // default seed + int32_t seed = 404; + FastNoise::SmartNode fnSimplex; + FastNoise::SmartNode fnFractal; + + int32_t octaves = 6; + float lacunarity = 0.5f; + float gain = 3.5f; + float frequency = 0.4f; + float weighted_strength = 0.0f; + uint32_t multiplier = 128; +}; + +#endif // WORLD_OF_CUBE_GENERATOR_HPP \ No newline at end of file diff --git a/test/source/test/astar_test.cpp b/test/source/test/astar_test.cpp new file mode 100644 index 0000000..7add942 --- /dev/null +++ b/test/source/test/astar_test.cpp @@ -0,0 +1,87 @@ +#include "astar/astar.hpp" + +#include "gtest/gtest.h" + +TEST(AStar, basic_path_1) { + int mapWidth = 4; + int mapHeight = 4; + AStar::AStar pathFinder; + pathFinder.setWorldSize({mapWidth, mapWidth}); + pathFinder.setHeuristic(AStar::Heuristic::euclidean); + pathFinder.setDiagonalMovement(true); + + AStar::Vec2i source(0, 0); + AStar::Vec2i target(mapWidth - 1, mapHeight - 1); + + std::cout << "AStar::AStar pathFinder;" << std::endl; + auto path = pathFinder.findPath(source, target); + + EXPECT_EQ(path.size(), 4); + + for (size_t i = 0; i < path.size(); i++) { + EXPECT_EQ(path[i].x, path.size() - i - 1); + EXPECT_EQ(path[i].y, path.size() - i - 1); + } +} + +TEST(AStar, basic_path_2) { + int mapWidth = 10; + int mapHeight = 10; + AStar::AStar pathFinder; + pathFinder.setWorldSize({mapWidth, mapWidth}); + pathFinder.setHeuristic(AStar::Heuristic::euclidean); + pathFinder.setDiagonalMovement(true); + + AStar::Vec2i source(0, 0); + AStar::Vec2i target(mapWidth - 1, mapHeight - 1); + + auto path = pathFinder.findPath(source, target); + + EXPECT_EQ(path.size(), 10); + + for (size_t i = 0; i < path.size(); i++) { + EXPECT_EQ(path[i].x, path.size() - i - 1); + EXPECT_EQ(path[i].y, path.size() - i - 1); + } +} + +TEST(AStar, basic_diagonal_path_wrong_1) { + int mapWidth = 10; + int mapHeight = 10; + AStar::AStar pathFinder; + pathFinder.setWorldSize({mapWidth, mapHeight}); + pathFinder.setHeuristic(AStar::Heuristic::euclidean); + pathFinder.setDiagonalMovement(true); + + AStar::Vec2i source(0, 0); + AStar::Vec2i target(19, 19); + + auto path = pathFinder.findPath(source, target); + + EXPECT_EQ(path.size(), 0); +} + +TEST(AStar, basic_diagonal_path_wrong_2) { + int mapWidth = 10; + int mapHeight = 10; + AStar::AStar pathFinder; + pathFinder.setWorldSize({mapWidth, mapHeight}); + pathFinder.setHeuristic(AStar::Heuristic::euclidean); + pathFinder.setDiagonalMovement(true); + + pathFinder.addObstacle({0, 1}); + pathFinder.addObstacle({1, 1}); + pathFinder.addObstacle({1, 0}); + + AStar::Vec2i source(0, 0); + AStar::Vec2i target(9, 9); + + auto path = pathFinder.findPath(source, target); + + EXPECT_EQ(path.size(), 0); +} + +auto main(int argc, char** argv) -> int { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tools/graphic.py b/tools/graphic.py new file mode 100755 index 0000000..b19b082 --- /dev/null +++ b/tools/graphic.py @@ -0,0 +1,91 @@ +# Based on work: https://int-i.github.io/python/2021-11-07/matplotlib-google-benchmark-visualization/ +# Modified by: Bensuperpc + +from argparse import ArgumentParser +from itertools import groupby +import json +import operator +import matplotlib.pyplot as plt +import numpy as np +import seaborn as sns +import pandas as pd + +# Extract the benchmark name from the benchmark name string +def extract_label_from_benchmark(benchmark): + bench_full_name = benchmark['name'] + bench_name = bench_full_name.split('/')[0] # Remove all after / + if (bench_name.startswith('BM_')): # Remove if string start with BM_ + return bench_name[3:] # Remove BM_ + else: + return bench_name + +# Extract the benchmark size from the benchmark +def extract_size_from_benchmark(benchmark): + bench_name = benchmark['name'] + return bench_name.split('/')[1] # Remove all before / + +if __name__ == "__main__": + # ./prog_name --benchmark_format=json --benchmark_out=result.json + parser = ArgumentParser() + parser.add_argument('path', help='benchmark result json file') + args = parser.parse_args() + + with open(args.path) as file: + benchmark_result = json.load(file) + benchmarks = benchmark_result['benchmarks'] + elapsed_times = groupby(benchmarks, extract_label_from_benchmark) + + data1 = None + data2 = None + + for key, group in elapsed_times: + benchmark = list(group) + x = list(map(extract_size_from_benchmark, benchmark)) + y1 = list(map(operator.itemgetter('bytes_per_second'), benchmark)) + y2 = list(map(operator.itemgetter('items_per_second'), benchmark)) + + if data1 is None: + data1 = pd.DataFrame({'size': x, key: y1}) + else: + data1[key] = y1 + + if data2 is None: + data2 = pd.DataFrame({'size': x, key: y2}) + else: + data2[key] = y2 + + df1 = pd.melt(data1, id_vars=['size'], var_name='Benchmark', value_name='bytes_per_second') + df1_max_indices = df1.groupby(['size', 'Benchmark'])['bytes_per_second'].transform(max) == df1['bytes_per_second'] + df1 = df1.loc[df1_max_indices] + df1.reset_index(drop=True, inplace=True) + + + df2 = pd.melt(data2, id_vars=['size'], var_name='Benchmark', value_name='items_per_second') + df2_max_indices = df2.groupby(['size', 'Benchmark'])['items_per_second'].transform(max) == df2['items_per_second'] + df2 = df2.loc[df2_max_indices] + df2.reset_index(drop=True, inplace=True) + + sns.set_theme() + + fig, ax = plt.subplots(2, 1) + + fig.set_size_inches(16, 9) + fig.set_dpi(96) + + sns.lineplot(data=df1, x='size', y='bytes_per_second', hue='Benchmark', ax=ax[0]) + sns.lineplot(data=df2, x='size', y='items_per_second', hue='Benchmark', ax=ax[1]) + + ax[0].set_title('Bytes per second') + ax[1].set_title('Items per second') + + ax[0].set_xlabel('Array size') + ax[1].set_xlabel('Array size') + + ax[0].set_ylabel('byte per second') + ax[1].set_ylabel('items per second') + + fig.tight_layout() + + plt.savefig('benchmark.png', bbox_inches='tight', dpi=300) + + plt.show()