From 7d81e22900a4e541720effbf8b28b7104969e3a2 Mon Sep 17 00:00:00 2001 From: Bensuperpc Date: Mon, 29 Jan 2024 21:08:29 +0100 Subject: [PATCH] Add files Signed-off-by: Bensuperpc --- .clang-format | 259 ++++++++++++++++ .clang-tidy | 155 ++++++++++ .codespellrc | 6 + .github/workflows/ci.yml | 185 ++++++++++++ .gitignore | 11 + BUILDING.md | 103 +++++++ CMakeLists.txt | 60 ++++ CMakePresets.json | 247 ++++++++++++++++ CMakeUserPresets.json | 69 +++++ CODE_OF_CONDUCT.md | 5 + CONTRIBUTING.md | 19 ++ HACKING.md | 149 ++++++++++ LICENSE | 21 ++ Makefile | 162 ++++++++++ README.md | 207 +++++++++++++ cmake/coverage.cmake | 33 +++ cmake/dev-mode.cmake | 21 ++ cmake/docs-ci.cmake | 112 +++++++ cmake/docs.cmake | 46 +++ cmake/folders.cmake | 21 ++ cmake/install-config.cmake | 1 + cmake/install-rules.cmake | 66 +++++ cmake/lib/backward-cpp.cmake | 13 + cmake/lib/benchmark.cmake | 70 +++++ cmake/lib/boost.cmake | 22 ++ cmake/lib/drogon.cmake | 15 + cmake/lib/fast_noise2.cmake | 11 + cmake/lib/gtest.cmake | 32 ++ cmake/lib/json.cmake | 31 ++ cmake/lib/opencv.cmake | 42 +++ cmake/lib/openmp.cmake | 6 + cmake/lib/perlin_noise.cmake | 14 + cmake/lib/pybind11.cmake | 18 ++ cmake/lib/raygui.cmake | 17 ++ cmake/lib/raylib-cpp.cmake | 13 + cmake/lib/raylib.cmake | 47 +++ cmake/lib/spdlog.cmake | 11 + cmake/lib/threadpool.cmake | 14 + cmake/lib/vector.cmake | 10 + cmake/lib/zlib.cmake | 16 + cmake/lint-targets.cmake | 34 +++ cmake/lint.cmake | 52 ++++ cmake/prelude.cmake | 10 + cmake/project-is-top-level.cmake | 6 + cmake/spell-targets.cmake | 22 ++ cmake/spell.cmake | 31 ++ cmake/utile/ccache.cmake | 9 + cmake/utile/distcc.cmake | 11 + cmake/utile/lto.cmake | 11 + cmake/utile/ninja_color.cmake | 9 + cmake/variables.cmake | 28 ++ codespell.ignore-words.txt | 1 + docs/Doxyfile.in | 32 ++ docs/conf.py.in | 6 + docs/pages/about.dox | 7 + example/CMakeLists.txt | 27 ++ example/basic_example.cpp | 31 ++ example/basic_fast_example.cpp | 36 +++ example/debug_example.cpp | 43 +++ include/astar/astar.hpp | 360 +++++++++++++++++++++++ resources/Screenshot_20240128_093812.png | Bin 0 -> 60630 bytes test/CMakeLists.txt | 82 ++++++ test/source/benchmark/astar_bench.cpp | 195 ++++++++++++ test/source/benchmark/path_finder.cpp | 258 ++++++++++++++++ test/source/generator/generator.cpp | 310 +++++++++++++++++++ test/source/generator/generator.hpp | 115 ++++++++ test/source/test/astar_test.cpp | 87 ++++++ tools/graphic.py | 91 ++++++ 68 files changed, 4264 insertions(+) create mode 100755 .clang-format create mode 100644 .clang-tidy create mode 100644 .codespellrc create mode 100755 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 BUILDING.md create mode 100644 CMakeLists.txt create mode 100755 CMakePresets.json create mode 100644 CMakeUserPresets.json create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 HACKING.md create mode 100644 LICENSE create mode 100755 Makefile create mode 100755 README.md create mode 100644 cmake/coverage.cmake create mode 100644 cmake/dev-mode.cmake create mode 100644 cmake/docs-ci.cmake create mode 100644 cmake/docs.cmake create mode 100644 cmake/folders.cmake create mode 100644 cmake/install-config.cmake create mode 100644 cmake/install-rules.cmake create mode 100644 cmake/lib/backward-cpp.cmake create mode 100755 cmake/lib/benchmark.cmake create mode 100755 cmake/lib/boost.cmake create mode 100644 cmake/lib/drogon.cmake create mode 100644 cmake/lib/fast_noise2.cmake create mode 100755 cmake/lib/gtest.cmake create mode 100755 cmake/lib/json.cmake create mode 100644 cmake/lib/opencv.cmake create mode 100755 cmake/lib/openmp.cmake create mode 100755 cmake/lib/perlin_noise.cmake create mode 100755 cmake/lib/pybind11.cmake create mode 100755 cmake/lib/raygui.cmake create mode 100755 cmake/lib/raylib-cpp.cmake create mode 100755 cmake/lib/raylib.cmake create mode 100644 cmake/lib/spdlog.cmake create mode 100644 cmake/lib/threadpool.cmake create mode 100755 cmake/lib/vector.cmake create mode 100755 cmake/lib/zlib.cmake create mode 100644 cmake/lint-targets.cmake create mode 100644 cmake/lint.cmake create mode 100644 cmake/prelude.cmake create mode 100644 cmake/project-is-top-level.cmake create mode 100644 cmake/spell-targets.cmake create mode 100755 cmake/spell.cmake create mode 100755 cmake/utile/ccache.cmake create mode 100755 cmake/utile/distcc.cmake create mode 100755 cmake/utile/lto.cmake create mode 100755 cmake/utile/ninja_color.cmake create mode 100644 cmake/variables.cmake create mode 100644 codespell.ignore-words.txt create mode 100644 docs/Doxyfile.in create mode 100644 docs/conf.py.in create mode 100644 docs/pages/about.dox create mode 100644 example/CMakeLists.txt create mode 100644 example/basic_example.cpp create mode 100755 example/basic_fast_example.cpp create mode 100644 example/debug_example.cpp create mode 100644 include/astar/astar.hpp create mode 100644 resources/Screenshot_20240128_093812.png create mode 100644 test/CMakeLists.txt create mode 100644 test/source/benchmark/astar_bench.cpp create mode 100644 test/source/benchmark/path_finder.cpp create mode 100644 test/source/generator/generator.cpp create mode 100644 test/source/generator/generator.hpp create mode 100644 test/source/test/astar_test.cpp create mode 100755 tools/graphic.py 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 0000000000000000000000000000000000000000..d8188cfb8433373b588f727d446a53d67d476f08 GIT binary patch literal 60630 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 literal 0 HcmV?d00001 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()