diff --git a/.bazelrc b/.bazelrc index 8d1bd6a8..bf57c0dd 100644 --- a/.bazelrc +++ b/.bazelrc @@ -19,38 +19,44 @@ build:linux --host_cxxopt=-std=c++20 build:opt --compilation_mode=opt # AddressSanitizer configuration -# Usage: -# Linux: bazel test --config=asan //path:target -# macOS: bazel test --config=asan --config=asan_macos //path:target +# Usage: bazel test --config=asan //path:target +# macOS uses the Xcode toolchain (build:asan_macos). See docs/BUILD_SYSTEM.md +# ("AddressSanitizer and ThreadSanitizer (macOS)") for clang/Xcode version coupling. +build:asan --config=asan_macos + build:asan --copt=-fsanitize=address build:asan --linkopt=-fsanitize=address +# Undefine _FORTIFY_SOURCE; it conflicts with ASAN instrumentation. +build:asan --copt=-U_FORTIFY_SOURCE build:asan --strip=never build:asan --features=dbg build:asan --compilation_mode=dbg test:asan --test_timeout=120,300,900,3600 -# GoogleTest headers can trigger conversion warnings on Clang; suppress for tests. +# GoogleTest headers can trigger conversion warnings on Clang; suppress them. +# Scoped to `build` (not `test`) so the suppression also applies to plain +# `bazel build` of test targets and tooling that compiles GoogleTest, not just +# `bazel test`. The `build` config is inherited by `test`, `run`, etc. # On Linux, use -Wno-conversion for portability when the system compiler is GCC. -test:macos --cxxopt=-Wno-character-conversion -test:linux --cxxopt=-Wno-conversion -# macOS-specific AddressSanitizer runtime lookup configuration. -# The LLVM toolchain bundles the ASAN runtime. On macOS tests run from the -# runfiles tree, so use @loader_path-based rpaths that walk back to execroot -# and then into external/toolchains_llvm... -# Keep the clang major version in these paths aligned with darwin-aarch64 -# in MODULE.bazel. -# Bazel test binaries live at different depths under bazel-out/.../bin. -# Provide a small set of distinct @loader_path traversals to reach -# execroot/_main/external from common test binary depths. -build:asan_macos --linkopt=-Wl,-rpath,@loader_path/../../../../../external/toolchains_llvm++llvm+llvm_toolchain_llvm/lib/clang/20/lib/darwin -build:asan_macos --linkopt=-Wl,-rpath,@loader_path/../../../../../../external/toolchains_llvm++llvm+llvm_toolchain_llvm/lib/clang/20/lib/darwin -build:asan_macos --linkopt=-Wl,-rpath,@loader_path/../../../../../../../external/toolchains_llvm++llvm+llvm_toolchain_llvm/lib/clang/20/lib/darwin -build:asan_macos --linkopt=-Wl,-rpath,@loader_path/../../../../../../../../external/toolchains_llvm++llvm+llvm_toolchain_llvm/lib/clang/20/lib/darwin +build:macos --cxxopt=-Wno-character-conversion +build:linux --cxxopt=-Wno-conversion +# macOS AddressSanitizer must use the Xcode compiler/runtime. LLVM-instrumented +# binaries hang with LLVM's ASAN dylib and abort with Xcode's (version mismatch). +# Chained from build:asan; Apple toolchains do not match on Linux and are ignored. +build:asan_macos --extra_toolchains=@local_config_apple_cc_toolchains//:all +build:asan_macos --copt=-Wno-macro-redefined # ThreadSanitizer configuration # Usage: bazel test --config=tsan //path:target +# macOS loads Xcode's compiler-rt via build:tsan_macos (rpath uses clang/N). +# Keep N aligned with MODULE.bazel llvm_versions and installed Xcode; see +# docs/BUILD_SYSTEM.md ("AddressSanitizer and ThreadSanitizer (macOS)"). +build:tsan --config=tsan_macos build:tsan --copt=-fsanitize=thread build:tsan --linkopt=-fsanitize=thread build:tsan --strip=never build:tsan --features=dbg build:tsan --compilation_mode=dbg +test:tsan --test_timeout=120,300,900,3600 +# Hardcoded rpath: .../lib/clang/21/... must match MODULE.bazel llvm major and Xcode. +build:tsan_macos --linkopt=-Wl,-rpath,/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/lib/darwin diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 67694860..7a482c61 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -41,8 +41,8 @@ bazel build -c opt //... # Build with debug symbols bazel build -c dbg //... -# Build with specific flags (example: enable ASAN) -bazel build --define=asan=true //... +# Build with AddressSanitizer (see .bazelrc build:asan and docs/BUILD_SYSTEM.md) +bazel build --config=asan //... ``` ### Build Time Expectations @@ -200,9 +200,9 @@ If CI fails: Before finalizing changes, optionally run: ```bash -# Check for memory leaks (Linux only) -bazel build --define=asan=true //... -bazel test --define=asan=true //... +# Check for memory leaks (see docs/BUILD_SYSTEM.md for macOS/Linux notes) +bazel build --config=asan //... +bazel test --config=asan //... # Performance test with real hands bazel build //library/tests:dtest @@ -276,7 +276,7 @@ bazel build //library/tests:dtest **Problem**: Tests fail with "Segmentation fault" - **Cause**: Usually memory management bug or uninitialized variable -- **Fix**: Run with ASAN: `bazel test --define=asan=true //library/tests:failing_test` +- **Fix**: Run with ASAN: `bazel test --config=asan //library/tests:failing_test` **Problem**: Tests fail with different results than expected - **Cause**: Possibly heuristic sorting or transposition table issues diff --git a/.github/instructions/git.instructions.md b/.github/instructions/git.instructions.md index e12da055..2eb66889 100644 --- a/.github/instructions/git.instructions.md +++ b/.github/instructions/git.instructions.md @@ -21,6 +21,6 @@ alwaysApply: false 2. **Make changes** locally. 3. **Commit** with a clear message. 4. **Push** the branch. -5. **Open a PR** via the `github` MCP server (see `github.instructions.md`). +5. **Open a PR** via the GitHub CLI (`gh`). --- \ No newline at end of file diff --git a/.github/instructions/github.instructions.md b/.github/instructions/github.instructions.md index 5bbff444..de59b6ce 100644 --- a/.github/instructions/github.instructions.md +++ b/.github/instructions/github.instructions.md @@ -7,7 +7,6 @@ alwaysApply: false ## Overview This project uses GitHub as its primary version control and collaboration platform. All code contributions, fixes, and features must go through a pull request (PR) workflow. -You have access to an MCP server named **`github`** (running `mcp-github`) which can create branches, push commits, and open PRs directly from the Continue.dev environment. See `.vscode/mcp.json` for configuration. ## Branching Strategy - **Default branch:** `main` @@ -29,28 +28,17 @@ You have access to an MCP server named **`github`** (running `mcp-github`) which - CI build - All unit and integration tests - Any lint/format checks -5. Small PRs are preferred — keep them focused on one logical change. +5. Small PRs are preferred — keep each focused on one logical change. ## Tooling -Use the **`github`** MCP server (configured in `.vscode/mcp.json`) to: +Use `git` for local version control, and the GitHub CLI (`gh`) for GitHub operations: - Create branches - Commit and push changes - Open pull requests - Check PR status -## Example MCP Server Commands -You can instruct Continue.dev to: -- `Use the github MCP server to create a new branch called fix/memory-leak` -- `Push the current branch and open a pull request titled "Fix memory leak in cache manager"` -- `List open pull requests for this repo` -- `Merge PR #45 after approval` ## Automation -- CI/CD runs automatically for all PRs. +- CI runs automatically for all PRs. - Approved PRs can be merged by maintainers. -- Use **squash merging** to keep history clean. -## Notes for Continue.dev -- You are allowed to automate branch creation and PR submission using the `github` MCP server. -- If multiple PRs are required, ensure each is isolated to its own branch. -- You may request human review before merging. ``` diff --git a/.github/workflows/ci_linux.yml b/.github/workflows/ci_linux.yml index 471e2aeb..870fd207 100644 --- a/.github/workflows/ci_linux.yml +++ b/.github/workflows/ci_linux.yml @@ -9,6 +9,7 @@ on: jobs: build_and_test: runs-on: ubuntu-latest + timeout-minutes: 20 steps: # 1️⃣ Checkout the repository - name: Checkout repository @@ -31,3 +32,61 @@ jobs: # 6️⃣ Run all tests (including Python) - name: Run all tests run: bazel test --verbose_failures //... + + # 7️⃣ Upload test logs + - name: Upload test logs - Linux + if: always() + uses: actions/upload-artifact@v6 + with: + name: bazel-test-logs-linux + path: bazel-testlogs/ + if-no-files-found: ignore + retention-days: 30 + + asan: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Bazelisk + uses: bazel-contrib/setup-bazel@0.19.0 + with: + bazelisk-version: "1.29.0" + + - name: Run tests (AddressSanitizer) + run: bazel test --config=asan --verbose_failures --test_output=errors //library/tests/... + + - name: Upload ASAN test logs + if: always() + uses: actions/upload-artifact@v6 + with: + name: bazel-test-logs-linux-asan + path: bazel-testlogs/ + if-no-files-found: ignore + retention-days: 30 + + tsan: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Bazelisk + uses: bazel-contrib/setup-bazel@0.19.0 + with: + bazelisk-version: "1.29.0" + + - name: Run system tests (ThreadSanitizer) + run: bazel test --config=tsan --verbose_failures --test_output=errors //library/tests/system/... + + - name: Upload TSAN test logs + if: always() + uses: actions/upload-artifact@v6 + with: + name: bazel-test-logs-linux-tsan + path: bazel-testlogs/ + if-no-files-found: ignore + retention-days: 30 diff --git a/.github/workflows/ci_macos.yml b/.github/workflows/ci_macos.yml index 7211123c..248d2993 100644 --- a/.github/workflows/ci_macos.yml +++ b/.github/workflows/ci_macos.yml @@ -8,6 +8,7 @@ on: jobs: build_and_test: runs-on: macos-26 + timeout-minutes: 20 steps: # Checkout the repository - name: Checkout repository @@ -30,3 +31,40 @@ jobs: # Run all tests (including Python) - name: Run all tests run: bazelisk test --verbose_failures //... + + # Upload test logs + - name: Upload test logs - macOS + if: always() + uses: actions/upload-artifact@v6 + with: + name: bazel-test-logs-macos + path: bazel-testlogs/ + if-no-files-found: ignore + retention-days: 30 + + sanitizers: + runs-on: macos-26 + timeout-minutes: 20 + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Bazelisk + uses: bazel-contrib/setup-bazel@0.19.0 + with: + bazelisk-version: "1.29.0" + + - name: Run system tests (AddressSanitizer) + run: bazelisk test --config=asan --verbose_failures --test_output=errors //library/tests/system/... + + - name: Run system tests (ThreadSanitizer) + run: bazelisk test --config=tsan --verbose_failures --test_output=errors //library/tests/system/... + + - name: Upload sanitizer test logs + if: always() + uses: actions/upload-artifact@v6 + with: + name: bazel-test-logs-macos-sanitizers + path: bazel-testlogs/ + if-no-files-found: ignore + retention-days: 30 \ No newline at end of file diff --git a/.github/workflows/ci_wasm.yml b/.github/workflows/ci_wasm.yml index 397285c8..1bdcf3ae 100644 --- a/.github/workflows/ci_wasm.yml +++ b/.github/workflows/ci_wasm.yml @@ -9,6 +9,7 @@ on: jobs: build_and_test: runs-on: ubuntu-latest + timeout-minutes: 20 steps: # 1️⃣ Checkout the repository - name: Checkout repository @@ -29,4 +30,14 @@ jobs: # 4️⃣ Smoke test: run solve_board under Node.js — pass if it does not crash - name: Smoke test solve_board_wasm - run: node bazel-bin/examples/wasm/solve_board.js \ No newline at end of file + run: node bazel-bin/examples/wasm/solve_board.js + + # 5️⃣ Upload test logs + - name: Upload test logs - WASM + if: always() + uses: actions/upload-artifact@v6 + with: + name: bazel-test-logs-wasm + path: bazel-testlogs/ + if-no-files-found: ignore + retention-days: 30 \ No newline at end of file diff --git a/.github/workflows/ci_windows.yml b/.github/workflows/ci_windows.yml index 9c309941..fda2dc27 100644 --- a/.github/workflows/ci_windows.yml +++ b/.github/workflows/ci_windows.yml @@ -8,6 +8,7 @@ on: jobs: build_and_test: runs-on: windows-latest + timeout-minutes: 20 steps: # Checkout the repository - name: Checkout repository @@ -26,3 +27,13 @@ jobs: # Run all tests (including Python) - name: Run all tests run: bazel test --verbose_failures //... + + # Upload test logs + - name: Upload test logs - Windows + if: always() + uses: actions/upload-artifact@v6 + with: + name: bazel-test-logs-windows + path: bazel-testlogs/ + if-no-files-found: ignore + retention-days: 30 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 128d4cba..87b45633 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ !.vscode/mcp.json !.vscode/tasks.json +# Ignore Cursor or VS Code workspace files +*.code-workspace + # Ignore Visual Studio directory and files .vs #*/.vs/* @@ -12,6 +15,8 @@ *.vcxproj.user Build/bin Build/int +obj +.cr # Prerequisites *.d @@ -103,3 +108,5 @@ doxygen_output/ .cce/ # Claude Code local settings written by cce init .claude/settings.local.json + +/dotnet/DDS_Core/.github/copilot-instructions.md diff --git a/BUILD.bazel b/BUILD.bazel index 50327287..1799dab3 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -56,6 +56,12 @@ config_setting( define_values = {"scheduler": "true"}, ) +# Enable DDS_AB_STATS via --define=ab_stats=true +config_setting( + name = "ab_stats", + define_values = {"ab_stats": "true"}, +) + # Enable DDS_TT_CONTEXT_OWNERSHIP via --define=tt_context_ownership=true config_setting( name = "tt_context_ownership", @@ -68,12 +74,6 @@ config_setting( define_values = {"tt_reset_debug": "true"}, ) -# Enable AddressSanitizer via --define=asan=true -config_setting( - name = "asan", - define_values = {"asan": "true"}, -) - # Matches cc_* targets built under the wasm_cc_binary platform transition. # Use platform constraints (not deprecated `cpu` select) so this works # consistently across host OSes, including Windows. @@ -93,6 +93,11 @@ genrule( outs = ["doxygen_docs.zip"], cmd = """ set -e + case "$$(uname -s)" in + Darwin) PATH="/opt/homebrew/bin:/usr/local/bin:$$PATH" ;; + Linux) PATH="/usr/local/bin:$$PATH" ;; + MINGW*|MSYS*|CYGWIN*) PATH="/usr/bin:/usr/local/bin:$$PATH" ;; + esac DOXYFILE_GEN="$(@D)/Doxyfile.generated" OUT_DIR="$(@D)/doxygen_output" OUT_ZIP="$$(pwd)/$@" diff --git a/Build b/Build deleted file mode 120000 index 16ce6a9b..00000000 --- a/Build +++ /dev/null @@ -1 +0,0 @@ -../../../../Build/DDS3 \ No newline at end of file diff --git a/CPPVARIABLES.bzl b/CPPVARIABLES.bzl index 68e26416..6055bd8f 100644 --- a/CPPVARIABLES.bzl +++ b/CPPVARIABLES.bzl @@ -3,6 +3,7 @@ DDS_CPPOPTS = select({ "//:build_macos": [ "-O3", + "-flto=thin", "-mtune=generic", "-fPIC", "-Wpedantic", @@ -62,9 +63,6 @@ DDS_CPPOPTS = select({ "//conditions:default": [ "-std=c++20" ], -}) + select({ - "//:asan": ["-fsanitize=address"], - "//conditions:default": [], }) DDS_LOCAL_DEFINES = select({ @@ -72,7 +70,7 @@ DDS_LOCAL_DEFINES = select({ "//:debug_build_macos": [], "//:build_linux": [], "//:debug_build_linux": [], - "//:build_wasm": ["__WASM__"], + "//:build_wasm": [], "//conditions:default": [], }) + select({ "//:debug_all": ["DDS_DEBUG_ALL"], @@ -83,17 +81,17 @@ DDS_LOCAL_DEFINES = select({ }) + select({ "//:tt_reset_debug": ["DDS_DEBUG_TT_RESET"], "//conditions:default": [], +}) + select({ + "//:ab_stats": ["DDS_AB_STATS"], + "//conditions:default": [], }) DDS_LINKOPTS = select({ - "//:build_macos": [], + "//:build_macos": ["-flto=thin"], "//:debug_build_macos": [], "//:build_linux": [], "//:debug_build_linux": [], "//conditions:default": [], -}) + select({ - "//:asan": ["-fsanitize=address"], - "//conditions:default": [], }) # Per-target define to enable scheduler timing when desired. diff --git a/MODULE.bazel b/MODULE.bazel index e9766b27..7c75ba76 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -9,6 +9,9 @@ bazel_dep(name = "googletest", version = "1.17.0.bcr.2") bazel_dep(name = "pybind11_bazel", version = "3.0.1") bazel_dep(name = "rules_python", version = "2.0.1") bazel_dep(name = "toolchains_llvm", version = "1.7.0") +# Xcode CC toolchain for macOS AddressSanitizer builds (--config=asan). LLVM's ASAN +# runtime hangs on current macOS; see .bazelrc build:asan_macos. +bazel_dep(name = "apple_support", version = "1.24.2") # WASM builds (//examples/wasm:*, //web:dds_mvp_wasm). If you bump this version, # rebuild and run web/update_wasm.sh; see docs/wasm_build.md and patch_mvp_wasm.py. bazel_dep(name = "emsdk", version = "5.0.7") @@ -16,6 +19,8 @@ bazel_dep(name = "emsdk", version = "5.0.7") llvm = use_extension("@toolchains_llvm//toolchain/extensions:llvm.bzl", "llvm") llvm.toolchain( name = "llvm_toolchain", + # When bumping these versions, also update .bazelrc build:tsan_macos rpath + # (clang/N) and confirm Xcode ships the same major; see docs/BUILD_SYSTEM.md. llvm_versions = { "darwin-aarch64": "21.1.8", "linux-x86_64": "21.1.8", @@ -24,6 +29,10 @@ llvm.toolchain( ) use_repo(llvm, "llvm_toolchain") +# Apple toolchain repo; selected only for --config=asan via .bazelrc (not registered globally). +apple_cc_configure = use_extension("@apple_support//crosstool:setup.bzl", "apple_cc_configure_extension") +use_repo(apple_cc_configure, "local_config_apple_cc", "local_config_apple_cc_toolchains") + python = use_extension("@rules_python//python/extensions:python.bzl", "python") python.toolchain( configure_coverage_tool = True, diff --git a/README.md b/README.md index 73fcfa91..afc39493 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ Martin Nygren, May 2026 [Python Interface](docs/python_interface.md) +[DotNet Interface](docs/dotnet_interface.md) + [Legacy C Interface](docs/legacy_c_api.md) [Migrating to the modern API](docs/api_migration.md) diff --git a/benchmark.sh b/benchmark.sh new file mode 100755 index 00000000..e4f2a605 --- /dev/null +++ b/benchmark.sh @@ -0,0 +1,475 @@ +#!/usr/bin/env bash +# Benchmark dtest performance on one or two binaries. +# +# Runs all combinations of solver (solve, calc) and hand file +# (list100/1000/…/1), largest files first. With --compare, prints summary only +# unless --details; without --compare, prints per-run rows. With --compare and a +# tty, per-run rows appear during the run then are cleared before the summary. +# Does not pass dtest options unless given after "--" (see below). +# +# Usage: +# ./benchmark.sh +# ./benchmark.sh --build +# ./benchmark.sh -- -n 8 -r +# ./benchmark.sh --build --compare /path/to/other/dtest +# ./benchmark.sh --compare /path/to/other/dtest --epsilon 1 +# ./benchmark.sh --repeats 5 -- -n 4 +# REPEATS=3 ./benchmark.sh +# +# Environment: +# BRANCH Path to branch dtest (default: bazel-bin in this repo) +# COMPARE Optional second dtest binary for comparison +# HANDS_DIR Directory containing list*.txt files (default: ./hands) +# REPEATS Runs per combination per binary (default: 1) +# MAX_DEALS Include list10^n.txt files with 10^n <= N (default: 100) +# DRY_RUN If 1, print commands only +# DETAILS If 1 with --compare, keep per-run rows in output (default: transient on tty) +# EPSILON With --compare, max % diff to treat branch/compare as equal (default: 0.5) + +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BRANCH="${BRANCH:-$ROOT/bazel-bin/library/tests/dtest}" +HANDS_DIR="${HANDS_DIR:-$ROOT/hands}" +REPEATS="${REPEATS:-1}" +MAX_DEALS="${MAX_DEALS:-100}" +DRY_RUN="${DRY_RUN:-0}" +DETAILS="${DETAILS:-0}" +EPSILON="${EPSILON:-0.5}" +BUILD=0 +REVERSE=0 +DTEST_EXTRA=() + +SOLVERS=(solve calc) + +usage() { + cat <&2 + usage >&2 + exit 1 + ;; + esac +done + +if ! [[ "$MAX_DEALS" =~ ^[0-9]+$ ]] || (( MAX_DEALS < 1 )); then + echo "error: max_deals must be a positive integer (got: $MAX_DEALS)" >&2 + exit 1 +fi + +if ! [[ "$REPEATS" =~ ^[0-9]+$ ]] || (( REPEATS < 1 )); then + echo "error: repeats must be a positive integer (got: $REPEATS)" >&2 + exit 1 +fi + +if ! [[ "$EPSILON" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then + echo "error: epsilon must be a non-negative number (got: $EPSILON)" >&2 + exit 1 +fi + +if [[ "$REVERSE" == "1" && -z "${COMPARE:-}" ]]; then + echo "error: --reverse requires --compare" >&2 + exit 1 +fi + +select_hand_files() { + is_power_of_10() { + local n="$1" + (( n >= 1 )) || return 1 + while (( n > 1 )); do + (( n % 10 == 0 )) || return 1 + n=$(( n / 10 )) + done + return 0 + } + + local -a candidates=() + local path base count + + shopt -s nullglob + for path in "$HANDS_DIR"/list*.txt; do + base="${path##*/}" + if [[ "$base" =~ ^list([0-9]+)\.txt$ ]]; then + count="${BASH_REMATCH[1]}" + if is_power_of_10 "$count" && (( count <= MAX_DEALS )); then + candidates+=("${count}:${base}") + fi + fi + done + shopt -u nullglob + + if ((${#candidates[@]} == 0)); then + echo "error: no list10^n.txt files with 10^n <= $MAX_DEALS in $HANDS_DIR" >&2 + exit 1 + fi + + FILES=() + local item + while IFS= read -r item; do + FILES+=("${item#*:}") + done < <(printf '%s\n' "${candidates[@]}" | sort -t: -k1,1rn) +} + +select_hand_files + +if [[ "$BUILD" == "1" ]]; then + if [[ "$DRY_RUN" == "1" ]]; then + echo "DRY_RUN: (cd $ROOT && bazel build //library/tests:dtest)" >&2 + else + echo "Building //library/tests:dtest..." >&2 + (cd "$ROOT" && bazel build //library/tests:dtest) + fi +fi + +if [[ "$DRY_RUN" != "1" ]]; then + if [[ ! -x "$BRANCH" ]]; then + echo "error: branch binary not found or not executable: $BRANCH" >&2 + echo "hint: bazel build //library/tests:dtest" >&2 + exit 1 + fi + + if [[ -n "${COMPARE:-}" && ! -x "$COMPARE" ]]; then + echo "error: compare binary not found or not executable: $COMPARE" >&2 + exit 1 + fi +fi + +BIN_PAIRS=("branch:$BRANCH") +if [[ -n "${COMPARE:-}" ]]; then + if [[ "$REVERSE" == "1" ]]; then + BIN_PAIRS=("compare:$COMPARE" "branch:$BRANCH") + else + BIN_PAIRS=("branch:$BRANCH" "compare:$COMPARE") + fi +fi +num_bins=${#BIN_PAIRS[@]} + +for f in "${FILES[@]}"; do + if [[ ! -f "$HANDS_DIR/$f" ]]; then + echo "error: hand file not found: $HANDS_DIR/$f" >&2 + exit 1 + fi +done + +git_branch="unknown" +if git -C "$ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + git_branch="$(git -C "$ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)" +fi + +RESULTS="$(mktemp "${TMPDIR:-/tmp}/dds-benchmark.XXXXXX")" +trap 'rm -f "$RESULTS"' EXIT + +parse_dtest_output() { + awk ' + /^Number of hands/ { hands = $NF } + /^User time \(ms\)/ { user = ($NF == "zero" ? 0 : $NF) } + /^Sys time \(ms\)/ { sys = ($NF == "zero" ? 0 : $NF) } + /^Avg user time \(ms\)/ { avg = ($NF == "zero" ? 0 : $NF) } + /^Ratio[[:space:]]/ { ratio = $NF } + END { + if (user == "") user = "NA" + if (sys == "") sys = "NA" + if (avg == "") { + if (user == 0) avg = 0 + else if (hands != "" && user != "NA" && hands > 0) avg = user / hands + else avg = "NA" + } + if (ratio == "") ratio = "NA" + print user, sys, avg, ratio + } + ' +} + +run_dtest() { + local binary="$1" + local solver="$2" + local hands="$3" + local -a cmd=("$binary" -f "$hands" -s "$solver") + if ((${#DTEST_EXTRA[@]} > 0)); then + cmd+=("${DTEST_EXTRA[@]}") + fi + + if [[ "$DRY_RUN" == "1" ]]; then + echo "DRY_RUN: ${cmd[*]}" >&2 + return 0 + fi + + local out + if ! out="$("${cmd[@]}" 2>&1)"; then + echo "error: dtest failed: ${cmd[*]}" >&2 + echo "$out" >&2 + exit 1 + fi + local parsed + parsed="$(parse_dtest_output <<<"$out")" + local parsed_user parsed_sys + read -r parsed_user parsed_sys _ _ <<<"$parsed" + if [[ "$parsed_user" == "NA" || "$parsed_sys" == "NA" ]]; then + echo "warning: incomplete dtest timing output: ${cmd[*]}" >&2 + fi + echo "$parsed" +} + +progress_lines=0 +TRANSIENT_PROGRESS=0 +show_run_lines=1 + +print_run_table_header() { + printf "%-6s %-13s %7s %8s %8s %10s %6s %s\n" \ + "solver" "file" "ver" "user_ms" "sys_ms" "avg_user" "ratio" "run" + printf "%-6s %-13s %7s %8s %8s %10s %6s %s\n" \ + "------" "-------------" "-------" "--------" "--------" "----------" "------" "---" + if [[ "$TRANSIENT_PROGRESS" == "1" ]]; then + progress_lines=$((progress_lines + 2)) + fi +} + +print_run_row() { + printf "%-6s %-13s %7s %8s %8s %10s %6s %s\n" \ + "$1" "$2" "$3" "$4" "$5" "$6" "$7" "$8" + if [[ "$TRANSIENT_PROGRESS" == "1" ]]; then + progress_lines=$((progress_lines + 1)) + fi +} + +clear_transient_progress() { + if [[ "$TRANSIENT_PROGRESS" != "1" || $progress_lines -le 0 ]]; then + return + fi + # Cursor rests on a blank line below the last row; erase it, then each table line. + # Do not move up after clearing the topmost line (would hit the header above). + printf '\033[2K' + local i + for (( i = 0; i < progress_lines; i++ )); do + printf '\033[1A\033[2K' + done + progress_lines=0 +} + +echo "DDS dtest benchmark" +echo "===================" +printf "%-12s %s\n" "branch:" "$BRANCH" +if [[ -n "${COMPARE:-}" ]]; then + printf "%-12s %s\n" "compare:" "$COMPARE" + if [[ "$DETAILS" == "1" ]]; then + printf "%-12s %s\n" "details:" "on" + elif [[ -t 1 ]]; then + printf "%-12s %s\n" "details:" "transient (cleared before summary)" + else + printf "%-12s %s\n" "details:" "off (summary only)" + fi + if [[ "$REVERSE" == "1" ]]; then + printf "%-12s %s\n" "run order:" "interleaved compare, branch" + else + printf "%-12s %s\n" "run order:" "interleaved branch, compare" + fi + printf "%-12s %s\n" "epsilon:" "${EPSILON}%" +fi +printf "%-12s %s\n" "hands dir:" "$HANDS_DIR" +printf "%-12s %s\n" "max_deals:" "$MAX_DEALS" +printf "%-12s %s\n" "files:" "${FILES[*]}" +printf "%-12s %s\n" "git branch:" "$git_branch" +printf "%-12s %s\n" "repeats:" "$REPEATS" +if ((${#DTEST_EXTRA[@]} > 0)); then + printf "%-12s %s\n" "dtest args:" "${DTEST_EXTRA[*]}" +fi +echo + +show_run_lines=1 +TRANSIENT_PROGRESS=0 +if [[ -n "${COMPARE:-}" && "$DETAILS" != "1" ]]; then + show_run_lines=0 + if [[ -t 1 ]]; then + TRANSIENT_PROGRESS=1 + fi +fi + +if [[ "$DRY_RUN" != "1" && ( "$show_run_lines" == "1" || "$TRANSIENT_PROGRESS" == "1" ) ]]; then + print_run_table_header +fi + +total_runs=$(( ${#SOLVERS[@]} * ${#FILES[@]} * num_bins * REPEATS )) +run_no=0 + +for solver in "${SOLVERS[@]}"; do + for file in "${FILES[@]}"; do + hands="$HANDS_DIR/$file" + + for (( rep = 1; rep <= REPEATS; rep++ )); do + if [[ "$REPEATS" -gt 1 ]]; then + run_label="${rep}/${REPEATS}" + else + run_label="1/1" + fi + + for pair in "${BIN_PAIRS[@]}"; do + ver="${pair%%:*}" + bin="${pair#*:}" + run_no=$((run_no + 1)) + + if [[ "$DRY_RUN" == "1" ]]; then + run_dtest "$bin" "$solver" "$hands" + continue + fi + + read -r user sys avg ratio < <(run_dtest "$bin" "$solver" "$hands") + + if [[ "$show_run_lines" == "1" || "$TRANSIENT_PROGRESS" == "1" ]]; then + print_run_row "$solver" "$file" "$ver" "$user" "$sys" "$avg" "$ratio" "$run_label" + fi + + printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n" \ + "$solver" "$file" "$ver" "$rep" "$user" "$sys" "$avg" "$ratio" \ + >>"$RESULTS" + done + done + done +done + +clear_transient_progress + +if [[ -n "${COMPARE:-}" && "$DRY_RUN" != "1" ]]; then + echo + echo "Summary (branch vs compare, avg user ms)" + echo "==============================================================================" + printf "%-6s %-13s %12s %12s %10s %-15s\n" \ + "solver" "file" "compare_avg" "branch_avg" "cmp/branch" "note" + printf "%-6s %-13s %12s %12s %10s %-15s\n" \ + "------" "-------------" "------------" "------------" "----------" "---------------" + + awk -F'\t' -v files="${FILES[*]}" -v epsilon_pct="$EPSILON" ' + function within_epsilon(a, b, eps, hi, lo) { + eps = epsilon_pct / 100 + if (a > b) { hi = a; lo = b } else { hi = b; lo = a } + return (hi <= 0 || (hi - lo) / hi <= eps) + } + { + base = $1 SUBSEP $2 + if ($3 == "compare") { + s2[base] += $7 + c2[base]++ + } else if ($3 == "branch") { + s1[base] += $7 + c1[base]++ + } + } + END { + split("solve calc", solvers, " ") + nfiles = split(files, filearr, " ") + + for (si = 1; si <= 2; si++) { + for (fi = 1; fi <= nfiles; fi++) { + base = solvers[si] SUBSEP filearr[fi] + if (!(base in c2) || !(base in c1)) continue + # Every member of s1, c1, s2, and c2 should be positive. + # If not, it will be due to rounding to zero. To fix, update + # TestTimer.cpp to accumulate microseconds rather than milliseconds. + u2 = s2[base] / c2[base] + u1 = s1[base] / c1[base] + cmp_branch = u2 / u1 + if (within_epsilon(u1, u2)) { + note = "equal" + } else if (cmp_branch >= 1) { + note = "branch faster" + } else { + note = "compare faster" + } + sp = sprintf("%9.2fx", cmp_branch) + printf "%-6s %-13s %12.2f %12.2f %10s %-15s\n", + solvers[si], filearr[fi], u2, u1, sp, note + } + } + } + ' "$RESULTS" +fi + +echo +if [[ "$DRY_RUN" == "1" ]]; then + echo "DRY_RUN: $total_runs dtest invocations (not run)." +else + echo "Completed $run_no runs ($total_runs expected)." +fi diff --git a/docs/BUILD_SYSTEM.md b/docs/BUILD_SYSTEM.md index 15655020..fc8659f6 100644 --- a/docs/BUILD_SYSTEM.md +++ b/docs/BUILD_SYSTEM.md @@ -5,17 +5,19 @@ and Visual Studio on Windows only. ## C++ Toolchain DDS uses `bazel-contrib/toolchains_llvm` for C++ compilation on supported -macOS, Linux and Windows hosts. The Bazel module configuration currently pins -host-specific LLVM versions in `MODULE.bazel`, including LLVM 20.1.8 for -`darwin-aarch64` and LLVM 21.1.8 for `linux-x86_64`, and registers the -downloaded toolchains via `@llvm_toolchain//:all`. Other hosts use Bazel's -default C++ toolchain resolution. +macOS, Linux and Windows hosts. The Bazel module configuration pins LLVM +**21.1.8** for `darwin-aarch64`, `linux-x86_64`, and `windows-x86_64` in +`MODULE.bazel`, and registers the downloaded toolchains via +`@llvm_toolchain//:all`. Other hosts use Bazel's default C++ toolchain +resolution. Project-specific warning and feature flags remain in `CPPVARIABLES.bzl`, while toolchain selection lives in `MODULE.bazel` and standard-language settings are primarily configured in `.bazelrc` (with a fallback default in `CPPVARIABLES.bzl`). +MODULE.bazel.lock is checked in and version managed as per current bazel best practises. + ### macOS SDK and Runtime Compatibility On macOS, binaries built against a newer SDK/runtime than the currently running @@ -27,8 +29,60 @@ If you see runtime loader failures after a toolchain or OS change: 2. Re-resolve Bazel toolchains (`bazelisk shutdown`, then `bazelisk clean --expunge`). 3. Re-run `bazelisk test //...` to confirm runtime compatibility. +### AddressSanitizer and ThreadSanitizer (macOS) + +Sanitizer builds use `--config=asan` or `--config=tsan` (see `.bazelrc`). +Do not use `--define=asan=true`; that path is not supported. +macOS-specific settings are chained automatically; you do not pass a separate +`asan_macos` or `tsan_macos` flag. + +**Clang / Xcode version coupling.** Three places must stay on the same **clang +major** version (currently **21**): + +| Location | Role | +|----------|------| +| `MODULE.bazel` → `llvm_versions` | Hermetic LLVM used for normal builds and for TSAN instrumentation | +| Installed Xcode (`xcrun clang++ --version`) | ASAN compiler/runtime; TSAN `compiler-rt` dylib | +| `.bazelrc` → `build:tsan_macos` rpath (`.../lib/clang/21/lib/darwin`) | Lets LLVM-instrumented TSAN binaries load Xcode's TSAN runtime | + +**ASAN (`--config=asan`).** macOS builds select the Xcode CC toolchain via +`apple_support` (`build:asan_macos`). LLVM's bundled ASAN runtime hangs at +startup; mixing LLVM-instrumented objects with Xcode's ASAN dylib aborts with +a version mismatch. Using Xcode for the full compile/link avoids that. + +**TSAN (`--config=tsan`).** Code is still compiled with the hermetic LLVM +toolchain; only the **runtime** comes from Xcode's `compiler-rt`. LLVM's TSAN +dylib segfaults on current macOS, but Xcode's dylib works with LLVM-instrumented +binaries when the clang major matches. + +**When upgrading LLVM or Xcode**, update all coupled paths together: + +1. Bump `llvm_versions` in `MODULE.bazel` (and run `bazel mod deps` to refresh + `MODULE.bazel.lock`). +2. Update the `clang/N` segment in `build:tsan_macos` in `.bazelrc` to match the + new major version. +3. Confirm Xcode ships that major: + `xcrun clang++ --version` + `xcrun clang -print-file-name=libclang_rt.tsan_osx_dynamic.dylib` +4. Re-run sanitizer smoke tests, e.g. + `bazel test --config=asan //library/tests/system:context_tt_facade_test` + `bazel test --config=tsan //library/tests/system:thread_safety_stress_test` + +If TSAN fails to start after an Xcode upgrade (dyld cannot load the runtime), +the rpath major is likely out of sync with the installed toolchain. + +**CI.** GitHub Actions runs sanitizer jobs on pull requests to `main` and +`develop`: + +| Workflow | Job | Command | +|----------|-----|---------| +| `ci_linux.yml` | `asan` | `bazel test --config=asan //library/tests/...` | +| `ci_linux.yml` | `tsan` | `bazel test --config=tsan //library/tests/system/...` | +| `ci_macos.yml` | `sanitizers` | ASAN and TSAN on `//library/tests/system/...` (validates macOS toolchain/rpath wiring) | + ## Visual Studio and Rider Build + The top-level `solution` folder contains a Visual Studio solution file `solution.slnx` and project files for the dds and all the samples. It also contains a `Directory.Build.props` file which defines the common properties for all the projects. @@ -36,8 +90,6 @@ file which defines the common properties for all the projects. Note this line in the `Directory.Build.props` file: `$(MSBuildThisFileDirectory)\..\Build\` defining the output directory for all the projects. - - ## API Layers The library is structured into three API layers: diff --git a/docs/coding_standards_and_ai_advice.md b/docs/coding_standards_and_ai_advice.md new file mode 100644 index 00000000..6fc3952a --- /dev/null +++ b/docs/coding_standards_and_ai_advice.md @@ -0,0 +1,64 @@ +# Coding Agents and Coding Standards + +Coding agents are improving quickly, and the best tool or model for a task can change from one month to the next. This note collects coding guidance and tooling recommendations assembled during the first quarter of 2026, when most of the modernisation work for release 3.0.0 was completed. + +This document does not prescribe a specific MCP server setup. MCP servers can be powerful, but they also introduce security risks, so the right deployment strategy depends on the environment. + +## Coding Standards + +Consistent style matters even more when both humans and coding agents are reading and editing the same code. The preferred conventions are documented in the `.github/instructions` directory, which is where GitHub Copilot looks for its persistent instructions. + +1. [C++](../.github/instructions/cpp.instructions.md) +2. [Bazel](../.github/instructions/bazel.instructions.md) +3. [Git](../.github/instructions/git.instructions.md) +4. [GitHub](../.github/instructions/github.instructions.md) + +## Recommended Tools for Coding Agents + +### clangd + +https://clangd.llvm.org + +Language servers are familiar to most IDE users, but coding agents usually cannot interact with them directly. MCP wrappers can expose a language server to an agent, but they often do so by forwarding raw JSON responses that still need interpretation. If you also run Serena, prefer to let Serena handle the language-server integration. + +### Serena + +https://github.com/oraios/serena + +Serena is the most useful tool in this workflow. It provides semantic analysis and retrieval features that help coding agents stay focused on the right parts of the codebase and understand the structure of the language they are working in. + +### Code Context Engine + +https://github.com/elara-labs/code-context-engine + +Code Context Engine builds and maintains an index of the codebase, letting a coding agent inspect relevant parts of a file without scanning unrelated content across many files. + +One observation from this tooling landscape is that different systems solve different problems. Serena helps explain what the code is doing, while a code index helps locate where the interesting code lives. That is more useful than simply surfacing syntactically similar code, which is often not enough to guide a change. + +## Documentation and Code Completion + +Code documentation for DDS3 is generated with Doxygen, which extracts formatted comments from the source code. Run the following command to generate the local documentation: + + bazelisk build //:doxygen_docs + +The generated HTML pages are written under `bazel-bin/doxygen_output/html/` (and packaged as `bazel-bin/doxygen_docs.zip`). Open `bazel-bin/doxygen_output/html/index.html` to read the documentation. + +### Extracting compile_commands.json + +Language servers such as clangd rely on a `compile_commands.json` file, which describes how the project is built. + +On macOS or Linux, the following command generates that file when the `bazel-compile-commands` utility is installed: + +``` +bazel-compile-commands //... +``` + +https://github.com/kiron1/bazel-compile-commands + +An alternative is Hedron Compile Commands, which can be integrated into the Bazel build. It appears to be less actively maintained, but is still worth knowing about: + +https://github.com/hedronvision/bazel-compile-commands-extractor + +## Other Tooling + +We are not maintaining an official list of recommended tools for working with the DDS codebase. The CI scripts use `homebrew` or `apt-get` to install `doxygen`, so you may have to update `PATH` variables in build scripts if you install it using another method. \ No newline at end of file diff --git a/docs/dotnet_interface.md b/docs/dotnet_interface.md new file mode 100644 index 00000000..64e19751 --- /dev/null +++ b/docs/dotnet_interface.md @@ -0,0 +1,389 @@ +# DDS_Core – .NET API for the Double Dummy Solver + +`DDS_Core` is a modern, type‑safe .NET wrapper around Bo Haglund’s **Double Dummy Solver (DDS)** . +Please note that this library does not support `.Net Framework` and requires `.Net 8.0` or later. +The library exposes the full DDS functionality through idiomatic `.Net` structures and methods, supporting both: + +- The **legacy C API** (`SolveBoard`, `CalcDDtable`, `Par`, `AnalysePlay`, …) +- The **modern C++ API** (via `SolverContext` and `SolverConfig`) + +The goal is to provide a stable, fast, and fully documented .NET interface to the DDS engine. + +--- + +## Table of Contents +1. [Introduction](#introduction) +2. [Legacy vs. Modern DDS API](#legacy-vs-modern-dds-api) +3. [Basic Usage](#basic-usage) +4. [API Overview](#api-overview) + a. [Modern API (Recommended)](#modern-api-recommended) + - [Construction & Lifetime](#construction-lifetime) + - [Single Board Solving Modern](#single-board-solving-modern) + - [Double Dummy Table Calculation Modern](#double-dummy-table-calculation-modern) + - [Par Score Calculation Modern](#par-score-calculation-modern) + - [Logging](#logging) + b. [Legacy API Overview](#legacy-api) + - [Configuration & Resources](#configuration-resources) + - [Single Board Solving](#single-board-solving) + - [Multiple Board Solving](#multiple-board-solving) + - [Double Dummy Table Calculation](#double-dummy-table-calculation) + - [Par Score Calculation](#par-score-calculation) + - [Par Text Conversion](#par-text-conversion) + - [Play Analysis](#play-analysis) + - [Utility Functions](#utility-functions) + c. [Conversion and error handling](#conversion-and-error-handling) + - [Error Handling](#error-handling) + - [Internal Error Handling](#internal-error-handling) + d. [Threading & Performance](#threading-performance) + e. [Examples](#examples) + +--- + +# Introduction + +`DDS_Core` provides a managed .NET interface to the DDS engine. +All data structures (`Deal`, `FutureTricks`, `DdTableResults`, etc.) are blittable and optimized for interoperation with the DDS library. + +The wrapper is designed for: + +- high performance +- minimal marshalling overhead +- strong type safety +- full compatibility with both DDS C and C++ APIs +- the project follows the .NET Framework Design Guidelines, including naming conventions, and leverages method overloading to provide a clear, expressive, and flexible API surface. +- one-dimensional InlineArrays and two-dimensional array-like types are used interop arrays to avoid unnecessary heap allocations and improve performance while keeping `.Net` bounds checking +- the modern `SolverContext` class encapsulates solver state and resources, allowing for efficient reuse across multiple calls and handles the resource housekeeping and guard against memory leaks. + +--- + +# Legacy vs. Modern DDS API + +DDS exposes two API layers: + +| API | Description | Status | +|-----|-------------|--------| +| **Legacy C API** | Global functions such as `SolveBoard`, `CalcDDtable`, `Par`, `AnalysePlay` | Supported but deprecated | +| **Modern C++ API** | RAII‑based `SolverContext` and `SolverConfig` | Recommended | + +In `DDS_Core`, all legacy functions are marked with: + +```csharp +[Obsolete("Use SolverContext instead.")] +``` + +# Basic Usage +Using some common declarations: +```csharp +using DDS_Core; +... +var dds = new DDS() +var hands +{ + // fill in card holdings... +}; + +``` + +Legacy API usage example: +```csharp +var rc = dds.SolveBoard(dl: deal, + target: 0, + solutions: 1, + mode: 0, + out var fut, + threadIndex: 0 // use 0 for single-threaded, or thread ID for multi-threaded contexts); +``` + + +The same sample but using the modern API: +```csharp + +var ctx = new SolverContext(); // create one context per thread, and reuse for multiple calls + rc = ctx.SolveBoard(deal, -1, 1, 0, out FutureTricks fut); +``` + +# API Overview + +This document provides a structured overview of the public API exposed by the `DDS_Core` .NET wrapper. +The API is grouped into functional areas that mirror the underlying DDS engine capabilities. +The API is divided into two layers: + +1. **Modern API** — based on `SolverContext` (recommended) +2. **Legacy API** — thin wrappers around the original C API (maintained for compatibility) + +--- + +## Modern API (Recommended) + +The modern API is built around the RAII‑style `SolverContext` object, mirroring the C++ DDS API. +Each context instance owns its own transposition table, configuration, and solver state. + +--- + +### Construction & Lifetime + +```csharp +var ctx = new SolverContext(); +var ctx = new SolverContext(new SolverConfig { ... }); +``` + +--- + +### Configuration & Resources + +- **SetMaxThreads(int userThreads)** + Sets the maximum number of threads used by the legacy solver backend. + +- **SetThreading(int code)** + Selects the threading backend. Returns `1` on success. + +- **SetResources(int maxMemoryMB, int maxThreads)** + Configures memory and thread limits for the legacy solver. + +- **FreeMemory()** + Frees memory allocated by the legacy solver. + +- **ResetBestMovesLite()** + Resets the internal cache of best moves. + +--- + +### Single Board Solving Modern + +Functions for analyzing a single bridge deal using double‑dummy analysis. + +- **int SolveBoard(Deal dl, int target, int solutions, int mode, out FutureTricks fut, int threadIndex = 0)** + Solves a binary `Deal` and returns trick results. + +- **int SolveBoard(DealPBN pbn, int target, int solutions, int mode, out FutureTricks fut, int threadIndex = 0)** + PBN‑formatted variant (legacy). + +--- + +### Double Dummy Table Calculation Modern + +Computes DDS table for one deal, including optional par calculations. + + +- **int CalcDdTable(DdTableDeal deal, out DdTableResults table)** + Computes the double‑dummy table for a binary deal. + +- **int CalcDdTable(DdTableDealPBN deal, out DdTableResults table)** + PBN variant. + +--- + +### Par Score Calculation Modern + +Computes par contracts and scores based on DDS table results. + +- **int CalcPar(DdTableDeal table_deal + , int vulnerable + , out DdTableResults table_results + , out ParResults par_results)** + +--- + +### Logging +- **void LogAppend(string message)** + Appends a log message to the per thread log file. + +- **void LogClear()** + Clears the per thread log file. + +--- + +--- + +## Legacy API + +The legacy API provides direct access to the original C functions. Many if these are marked as `[Obsolete]` +and should be used with caution, as they may not manage resources as efficiently as the modern API. + +### Configuration & Resources + +- **SetMaxThreads(int userThreads)** + Deprecated. This sets the maximum number of threads used by the legacy solver backend. + The modern API manages threading implicitly via `SolverContext` and does not require manual configuration. + +- **SetThreading(int code)** + Deprecated. configures the threading backend for the legacy API. + The modern API uses a more efficient and flexible threading model that does not require manual selection. + +- **SetResources(int maxMemoryMB, int maxThreads)** + Deprecated. Sets memory and thread resources for the legacy API. The modern API uses `SolverConfig`. + +- **FreeMemory()** + Deprecated. Frees memory allocated by the legacy API. The modern API uses RAII via `SolverContext`. + +### Single Board Solving +- **int SolveBoard(Deal dl, ...)** + Solves a single deal via double dummy analysis . Deprecated – use SolverContext. + +- **int SolveBoard(DealPBN pbn, ...)** + The same only with PBN-format. Deprecated. + +### Multiple Board Solving +- **int SolveAllBoards(Board boards, out SolvedBoards solved)** + Solves multiple boards. Deprecated, but not yet implemented in the modern api. + +- **int SolveAllBoards(BoardsPBN boards, out SolvedBoards solved)** + The same only with PBN-format. Deprecated, but not yet implemented in the modern api. + +### Double Dummy Table Calculation +- ** int CalcDdTable(DdTableDeal deal, out DdTableResults table)** + Calculates the double‑dummy table for a single deal. Deprecated – use SolverContext. + +- **int CalcDdTable(DdTableDealPBN deal, out DdTableResults table)** + The same only with PBN-format. Deprecated – use SolverContext. + +- **int CalcAllTables(...)** + As above but for multiple deals. Deprecated, but not yet implemented in the modern api. + +### Par Score Calculation +- **int Par(DdTableResults table, out ParResults pres, int vulnerable)** + Calculate par scores based on the double‑dummy table results. Deprecated – use SolverContext.CalcPar. + +- **int CalcPar(DdTableDeal deal, ...)** + Combines DD-table calculation and par-score in one function. Deprecated – use SolverContext + +- **int CalcPar(DdTableDealPBN deal, ...)** + The same only with PBN-format. Deprecated – use SolverContext. + +- **int ParSide()** + Calculates par score for a specific side. Deprecated, but not yet implemented in the modern api. + Exists in both a variant with binary and without PBN-format. + +- **int ParDealer()** + Calculates par score for the dealer. Deprecated, but not yet implemented in the modern api. + +- **int DealerParBothSides()** + Calculates par score for both sides. Deprecated, but not yet implemented in the modern api. + +- **int ParAll()** + Calculates par score for all sides. Deprecated, but not yet implemented in the modern api. + +### Play Analysis +- **int AnalysePlay(Deal dl, PlayTraceBin play, ...)** + analyses a play sequence for a single deal. Deprecated, but not yet implemented in the modern api. + +- **int AnalysePlay(DealPBN dl, PlayTracePBN play, ...)** + PBN-version. Deprecated, but not yet implemented in the modern api. + +- **int AnalyseAllPlays(...)** + analyses multiple play sequences across multiple deals. Deprecated, but not yet implemented in the modern api. + +--- + +## Conversion and error handling. +### Par Text Conversion +- **int ConvertToTextFormat(ParResultsMaster pres, out string resp)** + Converts par results to a human‑readable text format. + +- **int ConvertToTextFormat(ParResultsMasters pres, out ParTextResults resp)** + Converts multi‑side par results to structured text output. + +--- + + +### Error Handling + +Most public methods returns an error code that should be checked: + +```csharp +var rc = ctx.SolveBoard(deal, -1, 1, 0, out FutureTricks fut); + +if (rc != (int)SolveBoardResult.NoFault) + throw new InvalidOperationException($"DDS_Core failed with code: {rc}"); +``` +### Internal Error Handling + +In DEBUG mode, the library will throw exceptions for any DDS error code returned by the underlying functions. +This allows for easier debugging and error tracking during development. + +--- + +## Threading & Performance +The main advantages of the modern API is that it allows for better control over threading and resource management. +Each thread must create their own SolverContext and configure it. Then the same context can be used for multiple calls +to SolveBoard, CalcDdTable, etc., without the overhead of repeated initialization. If the context is created with a using +statement, it will automatically free resources when disposed. This will guard against memory leaks and ensure that all resources +are properly released. + +## Examples + +```csharp +using DDS_Core; +... + +public void sample() +{ + var dds = new DDS(); + + var hands = new FourHands(new uint[4][] // C# 12 syntax! + { + [ (uint)(rT | r8 | r5) + , (uint)(rA | rT | r7 | r2) + , (uint)(rK | rQ | r8) + , (uint)(rA | r3 | r2)] + , + [ (uint)(rJ | r2) + , (uint)(rK | r9) + , (uint)(rA | rJ | r7 | r4 | r3 | r2) + , (uint)(rJ | r8 | r7)] + , + [ (uint)(rK | rQ | r3) + , (uint)(rJ | r8 | r6 | r5 | r4 | r3) + , (uint)(r5) + , (uint)(rK | r9 | r6)] + , [ (uint)(rA | r9 | r7 | r6 | r4) + , (uint)(rQ) + , (uint)(rT | r9 | r6) + , (uint)(rQ | rT | r5 | r4)] + }); + + var cfg = new SolverConfig(){ + TTKind = TTKind.Small + , DefaultMemoryMB = 256 + , MaximumMemoryMB = 1024 + }; + + using var ctx = new SolverContext(cfg); + + ... + + var deal = new Deal { Trump = (int)Suit.Hearts + , First = 0 // North was to lead - ie. West is declarer + , CurrentTrickSuit = new int[3] {2, 0, 0 } // Diamonds + , CurrentTrickRank = new int[3] {8, 0, 0 } // 8 of Diamonds already lead + , RemainingCards = hands + }; + + var rc = ctx.SolveBoard(deal, -1, 1, 0, out FutureTricks fut); + + if (rc != (int)SolveBoardResult.NoFault) + { + ErrorMessage( rc , out string error); + throw new InvalidOperationException(error); + } + + if (!(int)ctx.SolveBoard(deal, -1, 1, 0, out FutureTricks fut)) + throw new InvalidOperationException("DDS_Core failed to solve the board."); + + + ... + + var ddTableDeal = new() {Cards = hands }; + + rc = ctx.CalcDdTable( ddTableDeal , out DdTableResults table_results); + + if (rc != (int)SolveBoardResult.NoFault) + { + ErrorMessage( rc , out string error); + throw new InvalidOperationException(error); + } + + ... +} + +``` \ No newline at end of file diff --git a/docs/wasm_build.md b/docs/wasm_build.md index f071beea..2e973f47 100644 --- a/docs/wasm_build.md +++ b/docs/wasm_build.md @@ -98,7 +98,6 @@ For other experiments, copy built `.js` / `.wasm` files from `bazel-bin/examples | `-O3` | Aggressive optimization | | `-flto` | Link-time optimization | | `-fexceptions` | Enable C++ exceptions | -| `-D__WASM__` | Preprocessor constant for WASM builds | | `-sWASM=1` | Emscripten WASM output (link flag) | | `-sALLOW_MEMORY_GROWTH=1` | Allow heap growth at runtime | | `-sINITIAL_MEMORY=268435456` | 256MB initial memory | @@ -136,8 +135,6 @@ The MVP link flags include `-sENVIRONMENT=web,node` so the same `.js` / `.wasm` ## Development notes -- The `__WASM__` preprocessor constant is defined for WASM builds (`CPPVARIABLES.bzl`). It was added to work around platform-specific code paths; revisit whether it can be narrowed or removed as WASM support matures. -- Some threading and platform-specific features are disabled or stubbed when `__WASM__` is set. - A reusable `cc_library` WASM artifact (not only example binaries) is not yet provided; today only `wasm_cc_binary` example targets are wired up. - The browser MVP lives under `web/`; see **Web browser (DDS MVP)** above and `//web:web_system_tests`. diff --git a/dotnet/DDS_Core/DDS.cs b/dotnet/DDS_Core/DDS.cs new file mode 100644 index 00000000..7126e4fd --- /dev/null +++ b/dotnet/DDS_Core/DDS.cs @@ -0,0 +1,641 @@ +using System.Diagnostics; +using System.Text; +using DDS_Core.Helpers; +using DDS_Core.Native; + +namespace DDS_Core; + +public class DDS +{ + #region ====== Configuration and Resource Management ====== + /// + /// Sets the maximum number of threads used by the solver. + /// + /// + /// + /// Deprecated. In the modern C++ API, thread count is controlled by the + /// embedding application — typically by creating one SolverContext + /// instance per worker thread. + /// + /// + /// New code should create and destroy SolverContext instances in the + /// application rather than calling this function. See docs/api_migration.md + /// for examples of the modern API. + /// + /// + /// This function is part of the legacy C API and is maintained for backward + /// compatibility. It has no direct equivalent in the modern API, where both + /// threading and transposition‑table memory limits are configured per instance + /// via SolverContext and SolverConfig. + /// + /// + /// Ignored; retained for backward compatibility. + // The underlying C symbol SetMaxThreads was renamed to + // InitializeStaticMemory; this P/Invoke still targets the deprecated alias. + [Obsolete("Use SolverContext instead.")] + public void SetMaxThreads(int userThreads) => DdsNative.SetMaxThreads(userThreads); + + /// + /// Sets the threading backend used by the solver. + /// + /// + /// + /// Deprecated. Use SolverContext instead — threading is implicit + /// (one context per thread). See docs/api_migration.md for modern C++ API examples. + /// + /// + /// This function is part of the legacy C API and is maintained for backward + /// compatibility. The modern C++ API does not require explicit threading + /// configuration; instead, create one SolverContext instance per thread. + /// + /// + /// Threading backend code (see documentation). + /// Error code (RETURN_NO_FAULT on success). + [Obsolete("Use SolverContext instead.")] + public int SetThreading(in int code) + { + var rc = DdsNative.SetThreading(code); + + ThrowIfError(rc, nameof(SetThreading)); + return rc; + } + + /// + /// Sets memory and thread resources for the solver. + /// + /// + /// + /// Deprecated. Use SolverContext with SolverConfig instead. + /// See docs/api_migration.md for modern C++ API examples. + /// + /// + /// This function is part of the legacy C API and is maintained for backward + /// compatibility. New code should use the modern C++ API with SolverContext, + /// which provides per‑instance configuration through SolverConfig. + /// + /// + /// Maximum memory in megabytes. + /// Maximum number of threads. + [Obsolete("Use SolverContext instead.")] + public void SetResources(in int maxMemoryMB, in int maxThreads) => DdsNative.SetResources(maxMemoryMB, maxThreads); + + /// + /// Frees memory used by the solver. + /// + /// + /// + /// Deprecated. Use SolverContext RAII instead — cleanup is automatic. + /// See docs/api_migration.md for modern C++ API examples. + /// + /// + /// This function is part of the legacy C API and is maintained for backward + /// compatibility. The modern C++ API uses RAII (Resource Acquisition Is + /// Initialization) through SolverContext, which automatically cleans up + /// resources when the context goes out of scope. + /// + /// + [Obsolete("Use SolverContext instead.")] + public void FreeMemory() => DdsNative.FreeMemory(); + #endregion + + #region ====== Single Board Solving ====== + /// + /// Solves a single bridge Deal using double dummy analysis. + /// + /// The deal to analyze. + /// Target number of tricks. + /// Solution mode (1 = best, 2 = all, etc.). + /// Analysis mode. + /// The result. + /// Index of the thread to use. + /// Error code (RETURN_NO_FAULT on success). + [Obsolete("Use SolverContext instead.")] + public int SolveBoard( in Deal dl + , in int target + , in int solutions + , in int mode + , out FutureTricks fut + , int threadIndex = 0) + { + var rc = DdsNative.SolveBoard( dl + , target + , solutions + , mode + , out fut + , threadIndex); + + ThrowIfError(rc, nameof(SolveBoard)); + return rc; + } + + /// + /// Solves a single bridge deal in PBN format using double dummy analysis. + /// + /// The PBN deal to analyze. + /// Target number of tricks. + /// Solution mode. + /// Analysis mode. + /// The result. + /// Index of the thread to use. + /// Error code (RETURN_NO_FAULT on success). + [Obsolete("Use SolverContext instead.")] + public int SolveBoard( in DealPBN pbn + , in int target + , in int solutions + , in int mode + , out FutureTricks fut + , int threadIndex = 0) + { + var rc = DdsNative.SolveBoardPBN( pbn + , target + , solutions + , mode + , out fut + , threadIndex); + + ThrowIfError(rc, nameof(SolveBoard)); + return rc; + } + #endregion + + #region ====== Multiple Board Solving ====== + /// + /// Solves multiple bridge deals in PBN format. + /// + /// Multiple PBN deals. + /// The results for solved boards. + /// Error code (RETURN_NO_FAULT on success). + public int SolveAllBoards( in BoardsPBN boards + , out SolvedBoards solved) + { + solved = default; + //Note: To step into c++ code you must set a break in c++? + var rc = DdsNative.SolveAllBoards( boards + , out solved); + + ThrowIfError(rc, nameof(SolveAllBoards)); + return rc; + } + + /// + /// Solves multiple bridge deals in binary format. + /// + /// Multiple deals. + /// The results for solved boards. + /// Error code (RETURN_NO_FAULT on success). + [Obsolete("Use SolverContext instead.")] + public int SolveAllBoards( in Boards bop + , out SolvedBoards solved) + { + solved = default; + + //Note: To step into c++ code you must set a break in c++? + var rc = DdsNative.SolveAllBoardsBin( bop + , out solved); + + ThrowIfError(rc, nameof(SolveAllBoards)); + return rc; + } + #endregion + + #region ====== Double Dummy Table Calculation ====== + /// + /// Calculates the double dummy table for a given deal. + /// + /// Deal for which to calculate the table. + /// The result table. + /// Error code (RETURN_NO_FAULT on success). + public int CalcDdTable( in DdTableDeal deal + , out DdTableResults table) + { + var rc = DdsNative.CalcDDtable( deal + , out table); + + ThrowIfError(rc, nameof(CalcDdTable)); + return rc; + } + + /// + /// Calculates the double dummy table for a PBN deal. + /// + /// PBN deal for which to calculate the table. + /// The result table. + /// Error code (RETURN_NO_FAULT on success). + public int CalcDdTable( in DdTableDealPBN tableDealPBN + , out DdTableResults table) + { + var rc = DdsNative.CalcDDtablePBN( tableDealPBN + , out table); + + ThrowIfError(rc, nameof(CalcDdTable)); + return rc; + } + + /// + /// Calculates double dummy tables and par contracts for multiple deals. + /// + /// Multiple deals. + /// Analysis mode. + /// Array of trump suit filters. + /// The result tables. + /// The par results. + /// Error code (RETURN_NO_FAULT on success). + public int CalcAllTables( in DdTableDeals deals + , in int mode + , in intArray5 trumpFilter + , out DdTablesResult resTables + , out AllParResults parResults) + { + var rc = DdsNative.CalcAllTables( deals + , mode + , trumpFilter + , out resTables + , out parResults); + + ThrowIfError(rc, nameof(CalcAllTables)); + return rc; + } + + /// + /// Calculates double dummy tables and par contracts for multiple PBN deals. + /// + /// Multiple PBN deals. + /// Analysis mode. + /// Array of trump suit filters. + /// The result tables. + /// The par results. + /// Error code (RETURN_NO_FAULT on success). + public int CalcAllTables( in DdTableDealsPBN dealsp + , in int mode + , in intArray5 trumpFilter + , out DdTablesResult ResTables + , out AllParResults parResults) + { + var rc = DdsNative.CalcAllTablesPBN( dealsp + , mode + , trumpFilter + , out ResTables + , out parResults); + + ThrowIfError(rc, nameof(CalcAllTables)); + return rc; + } + #endregion + + #region ====== Par Score Calculation ====== + /// + /// Computes the par score and contracts for both sides for a given double dummy results. + /// + /// This function analyzes the double dummy table results and determines the par score + /// and contracts for both North-South and East-West, based on vulnerability. + /// + /// + /// + /// + /// + public int Par( in DdTableResults table + , in int vulnerable + , out ParResults pres + ) + { + var rc = DdsNative.Par( table + , out pres + , vulnerable); + + ThrowIfError(rc, nameof(Par)); + return rc; + } + + /// + /// Calculates par score and contracts for a deal table. + /// + /// + /// + /// Computes the double dummy table for the given deal, then calculates par score + /// and contracts based on vulnerability. This overload creates a temporary + /// SolverContext internally. + /// + /// + /// This function is equivalent to calling CalcDDtable followed by + /// Par in the legacy C API. + /// + /// + /// + /// Deal represented as card holdings for each hand. + /// + /// + /// Vulnerability (0=None, 1=Both, 2=NS, 3=EW). + /// + /// + /// Output: double dummy table results. + /// + /// + /// Output: par score and contract strings. + /// + /// + /// Error code (RETURN_NO_FAULT on success). + /// + public int CalcPar( in DdTableDeal tableDeal + , in int vulnerable + , out DdTableResults tableResults + , out ParResults parResults) + { + var rc = DdsNative.CalcPar( tableDeal + , vulnerable + , out tableResults + , out parResults); + ThrowIfError(rc, nameof(CalcPar)); + return rc; + } + + /// + /// Calculates par score and contracts for a deal table in PBN format. + /// + /// + /// + /// Computes the double dummy table for the given deal, then calculates par score + /// and contracts based on vulnerability. This overload creates a temporary + /// SolverContext internally. + /// + /// + /// This function is equivalent to calling CalcDDtable followed by + /// Par in the legacy C API. + /// + /// + /// + /// Deal represented as a PBN string for each hand. + /// + /// + /// Vulnerability (0=None, 1=Both, 2=NS, 3=EW). + /// + /// + /// Output: double dummy table results. + /// + /// + /// Output: par score and contract strings. + /// + /// + /// Error code (RETURN_NO_FAULT on success). + /// + public int CalcPar( in DdTableDealPBN tableDealPBN + , in int vulnerable + , out DdTableResults tableResults + , out ParResults parResults) + { + var rc = DdsNative.CalcParPBN( tableDealPBN + , out tableResults + , vulnerable + , out parResults); + + ThrowIfError(rc, nameof(CalcPar)); + return rc; + } + + /// + /// Calculates par score and contracts for both sides based on the double dummy table results. + /// + /// Double dummy table results. + /// Vulnerability (0=None, 1=Both, 2=NS, 3=EW). + /// Output: par results for both sides. + /// Error code (RETURN_NO_FAULT on success). + public int ParSide( in DdTableResults table + , in int vulnerable + , out ParResultsDealers sidesRes + ) + { + var rc = DdsNative.SidesPar( table + , out sidesRes + , vulnerable); + + ThrowIfError(rc, nameof(ParSide)); + return rc; + } + + /// + /// Calculates par score and contracts for a specific dealer and vulnerability. + /// + /// + /// + /// + /// + /// + public int ParDealer( in DdTableResults table + , in int dealer + , in int vulnerable + , out ParResultsDealer pres + ) + { + var rc = DdsNative.DealerPar( table + , out pres + , dealer + , vulnerable); + + ThrowIfError(rc, nameof(ParDealer)); + return rc; + } + + /// + /// Calculates par score and contract types for both sides for a specific dealer and vulnerability. + /// + /// + /// + /// + /// + /// + public int DealerParBothSides( in DdTableResults table + , in int dealer + , in int vulnerable + , out ParResultsMaster pres + ) + { + var rc = DdsNative.DealerParBin( table + , out pres + , dealer + , vulnerable); + + ThrowIfError(rc, nameof(DealerParBothSides)); + return rc; + } + + /// + /// calculates par score and contract types for both sides based on the double dummy table results. + /// + /// Double dummy table results. + /// Vulnerability (0=None, 1=Both, 2=NS, 3=EW). + /// Output: par results for both sides. + /// Error code (RETURN_NO_FAULT on success). + public int ParAll( in DdTableResults table + , in int vulnerable + , out ParResultsMasters sidesRes + ) + { + var rc = DdsNative.SidesParBin( table + , out sidesRes + , vulnerable); + + ThrowIfError(rc, nameof(ParAll)); + return rc; + } + #endregion + + #region ====== Play Analysis ====== + /// + /// Analyzes a play trace for a given deal and determines the optimal line of play. + /// + /// The deal to analyze. + /// The play trace to analyze. + /// The thread ID for parallel processing. + /// The result of the analysis, including optimal line and tricks. + /// Error code (RETURN_NO_FAULT on success). + public int AnalysePlay( in Deal dl + , in PlayTraceBin play + , in int thrId + , out SolvedPlay solved + ) + { + var rc = DdsNative.AnalysePlayBin( dl + , in play + , out solved + , thrId); + ThrowIfError(rc, nameof(AnalysePlay)); + return rc; + } + + /// + /// analyzes a play trace for a given deal in PBN format and determines the optimal line of play. + /// + /// The PBN deal to analyze. + /// The play trace in PBN format to analyze. + /// The thread ID for parallel processing. + /// The result of the analysis, including optimal line and tricks. + /// Error code (RETURN_NO_FAULT on success). + public int AnalysePlay( in DealPBN dlPBN + , in PlayTracePBN playPBN + , in int thrId + , out SolvedPlay solved + ) + { + var rc = DdsNative.AnalysePlayPBN( dlPBN + , in playPBN + , out solved + , thrId); + ThrowIfError(rc, nameof(AnalysePlay)); + return rc; + } + + /// + /// Analyzes all play traces for a given set of boards and determines the optimal lines of play. + /// + /// The boards to analyze. + /// The play traces to analyze. + /// The chunk size for parallel processing. + /// The result of the analysis, including optimal lines and tricks. + /// Error code (RETURN_NO_FAULT on success). + public int AnalyseAllPlays( in Boards bop + , in PlayTracesBin plp + , in int chunkSize + , out SolvedPlays solved + ) + { + solved = new(); + + var rc = DdsNative.AnalyseAllPlaysBin( bop + , plp + , out solved + , chunkSize); + + ThrowIfError(rc, nameof(AnalyseAllPlays)); + return rc; + } + + /// + /// Analyzes all play traces for a given set of boards in PBN format and determines the optimal lines of play. + /// + /// The boards in PBN format to analyze. + /// The play traces in PBN format to analyze. + /// The chunk size for parallel processing. + /// The result of the analysis, including optimal lines and tricks. + /// Error code (RETURN_NO_FAULT on success). + public int AnalyseAllPlays( in BoardsPBN bopPBN + , in PlayTracesPBN plpPBN + , in int chunkSize + , out SolvedPlays solved + ) + { + solved = new(); + var rc = DdsNative.AnalyseAllPlaysPBN( bopPBN + , plpPBN + , out solved + , chunkSize); + ThrowIfError(rc, nameof(AnalyseAllPlays)); + return rc; + } + #endregion + + #region ====== Utility Functions ====== + #region ====== Par Text Conversion ====== + /// + /// Converts par results to a human-readable text format. + /// + /// Par results to convert. + /// Output: human-readable par results. + /// Error code (RETURN_NO_FAULT on success). + public int ConvertToTextFormat( in ParResultsMaster pres + , out string resp) + { + var str = new StringBuilder(80); + var rc = DdsNative.ConvertToDealerTextFormat( pres + , str); + ThrowIfError(rc, nameof(ConvertToTextFormat)); + resp = str.ToString(); + return rc; + } + + /// + /// converts par results for both sides to a human-readable text format. + /// + /// Par results for both sides to convert. + /// Output: human-readable par results for both sides. + /// Error code (RETURN_NO_FAULT on success). + public int ConvertToTextFormat( in ParResultsMasters pres + , out ParTextResults resp) + { + var rc = DdsNative.ConvertToSidesTextFormat( pres + , out resp); + ThrowIfError(rc, nameof(ConvertToTextFormat)); + return rc; + } + #endregion + + /// + /// Retrieves information about the DDS library. + /// + /// The DDS information. + public void GetDDSInfo(out DdsInfo info) + { + DdsNative.GetDDSInfo(out info); + } + + /// + /// Retrieves the error message corresponding to a given error code. + /// + /// The error code for which to retrieve the message. + /// Output: the error message corresponding to the provided code. + public void ErrorMessage( in int code + , out string line) + { + var str = new StringBuilder(80); + DdsNative.ErrorMessage(code, str); + line = str.ToString(); + } + #endregion + + #region private methods + [Conditional("DEBUG")] + private static void ThrowIfError(in int result, in string functionName) + { + if (result != (int)SolveBoardResult.NoFault) + throw new InvalidOperationException($"{functionName} failed with code {result}: {result.GetRCErrorMessage()}"); + } + #endregion +} diff --git a/dotnet/DDS_Core/DDS_Core.csproj b/dotnet/DDS_Core/DDS_Core.csproj new file mode 100644 index 00000000..35151565 --- /dev/null +++ b/dotnet/DDS_Core/DDS_Core.csproj @@ -0,0 +1,12 @@ + + + + net8.0 + enable + disable + x64 + false + ..\..\Build\bin\ + True + + diff --git a/dotnet/DDS_Core/DDS_Core.slnx b/dotnet/DDS_Core/DDS_Core.slnx new file mode 100644 index 00000000..c30e54c4 --- /dev/null +++ b/dotnet/DDS_Core/DDS_Core.slnx @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/dotnet/DDS_Core/DataModel/AllParResults.cs b/dotnet/DDS_Core/DataModel/AllParResults.cs new file mode 100644 index 00000000..e80eb242 --- /dev/null +++ b/dotnet/DDS_Core/DataModel/AllParResults.cs @@ -0,0 +1,24 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace DDS_Core; + +/// +/// Par results for all declarer/strain combinations (up to 40). +/// +[StructLayout(LayoutKind.Sequential)] +public struct AllParResults +{ + /// Array of par results (up to MAXNOOFTABLES entries). + public ParResultsArray ParResults; + + #region Nested Types + [InlineArray(DdsConstants.MaxNumberOfTables)] + public struct ParResultsArray + { + private ParResults item; + } + #endregion + +} diff --git a/dotnet/DDS_Core/DataModel/Boards.cs b/dotnet/DDS_Core/DataModel/Boards.cs new file mode 100644 index 00000000..a0ec5852 --- /dev/null +++ b/dotnet/DDS_Core/DataModel/Boards.cs @@ -0,0 +1,56 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace DDS_Core; + +/// +/// Represents multiple bridge deals for batch analysis. +/// +[StructLayout(LayoutKind.Sequential)] +public struct Boards +{ + /// Number of deals in this batch. + public int NumberOfBoards; + + /// Array of deals. + public DealsArray Deals; + + /// Target tricks for each board. + public intArray200 Target; + + /// Solution mode for each board (1=best, 2=all, 3=all+par). + + public intArray200 Solutions; + + /// Solve mode for each board. + public intArray200 Modes; + + #region Nested Types + [InlineArray(DdsConstants.MaxNumberOfBoards)] + public struct DealsArray + { + private Deal item; + + public static implicit operator DealsArray(Deal[] array) + { + var result = new DealsArray(); + + if (array != null) + { + var span = result.AsSpan(); + + for (int i = 0; i < Math.Min(array.Length, span.Length); i++) + span[i] = array[i]; + } + + return result; + } + + // Implicit conversion from DealsArray to Span + private Span AsSpan() + { + return System.Runtime.InteropServices.MemoryMarshal.CreateSpan(ref item, DdsConstants.MaxNumberOfBoards); + } + } + #endregion +} diff --git a/dotnet/DDS_Core/DataModel/BoardsPBN.cs b/dotnet/DDS_Core/DataModel/BoardsPBN.cs new file mode 100644 index 00000000..9c0b5088 --- /dev/null +++ b/dotnet/DDS_Core/DataModel/BoardsPBN.cs @@ -0,0 +1,58 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace DDS_Core; + +/// +/// Multiple boards in PBN format for batch solving. +/// +/// Similar to Boards but uses PBN (Portable Bridge Notation) format +/// for deal representation. Used for solving multiple boards efficiently. +/// +[StructLayout(LayoutKind.Sequential)] +public struct BoardsPBN +{ + /// Number of boards to solve. + public int NumberOfBoards; + + /// Array of deals in PBN format. + public DealsPBNArray Deals; + + /// Target tricks for each board. + public intArray200 Target; + + /// Solution mode for each board. + public intArray200 Solutions; + + /// Solve mode for each board. + public intArray200 Modes; + + #region Nested Types + [InlineArray(DdsConstants.MaxNumberOfBoards)] + public struct DealsPBNArray + { + private DealPBN item; + + public static implicit operator DealsPBNArray(DealPBN[] array) + { + var result = new DealsPBNArray(); + + if (array != null) + { + var span = result.AsSpan(); + + for (int i = 0; i < Math.Min(array.Length, span.Length); i++) + span[i] = array[i]; + } + + return result; + } + + // Implicit conversion from DealsPBNArray to Span + private Span AsSpan() + { + return System.Runtime.InteropServices.MemoryMarshal.CreateSpan(ref item, DdsConstants.MaxNumberOfBoards); + } + #endregion + } +} diff --git a/dotnet/DDS_Core/DataModel/ContractType.cs b/dotnet/DDS_Core/DataModel/ContractType.cs new file mode 100644 index 00000000..8f02d573 --- /dev/null +++ b/dotnet/DDS_Core/DataModel/ContractType.cs @@ -0,0 +1,38 @@ +using System; +using System.Runtime.InteropServices; +using DDS_Core.Helpers; + +namespace DDS_Core; + +/// +/// Contract details (level, strain, tricks, seats). +/// +[StructLayout(LayoutKind.Sequential)] +public struct ContractType +{ + /// Under tricks (0 = make, 1-13 = sacrifice). + public int UnderTricks; + + /// Over tricks (0-3, e.g., 1 for 4S + 1). + public int OverTricks; + + /// Contract level (1-7). + public int Level; + + /// Denomination (0 = NT, 1 = Spades, 2 = Hearts, 3 = Diamonds, 4 = Clubs). + public int Denomination; + + /// Seats playing contract (0 = N, 1 = E, 2 = S, 3 = W, 4 = NS, 5 = EW). + public int Seats; + + public override string ToString() + { + if (UnderTricks > 0) + return $"{Global.Seats[Seats]}:{Level}{Global.Denoms[Denomination]}(-{UnderTricks})"; + else + if (OverTricks > 0) + return $"{Global.Seats[Seats]}:{Level}{Global.Denoms[Denomination]}(+{OverTricks})"; + else + return $"{Global.Seats[Seats]}:{Level}{Global.Denoms[Denomination]}(=)"; + } +} diff --git a/dotnet/DDS_Core/DataModel/DDSInfo.cs b/dotnet/DDS_Core/DataModel/DDSInfo.cs new file mode 100644 index 00000000..b654b7b5 --- /dev/null +++ b/dotnet/DDS_Core/DataModel/DDSInfo.cs @@ -0,0 +1,57 @@ +using System; +using System.Runtime.InteropServices; + +namespace DDS_Core; + +/// +/// DDS library information and configuration details. +/// +/// Contains version, platform, compiler, threading, and resource information. +/// +[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] +public struct DdsInfo +{ + /// Major version number (e.g., 3 for 3.0.0). + public int Major; + + /// Minor version number. + public int Minor; + + /// Patch version number. + public int Patch; + + /// Version string (e.g., "3.0.0"), max 10 characters. + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 10)] + public readonly string VersionString; + + /// Platform (0 = unknown, 1 = Windows, 2 = Cygwin, 3 = Linux, 4 = macOS). + public int System; + + /// Bit width (32 or 64). + public int NumberOfBits; + + /// Compiler (0 = unknown, 1 = MSVC, 2 = mingw, 3 = GCC, 4 = clang). + public int Compiler; + + /// Constructor type (0 = none, 1 = DllMain, 2 = Unix-style). + public int constructor; + + /// Number of processor cores available. + public int NumberOfCores; + + /// Threading system (0 = none, 1 = Windows, 2 = OpenMP, 3 = GCD, 4 = Boost, 5 = STL, 6 = TBB, etc.). + public int Threading; + + /// Actual number of threads configured. + public int NumberOfThreads; + + /// Thread memory sizes configuration string (max 128 characters). + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + public readonly string ThreadSizes; + + /// Detailed system information string (max 1024 characters). + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 1024)] + public readonly string SystemString; + + +} diff --git a/dotnet/DDS_Core/DataModel/DdTableDealPBN.cs b/dotnet/DDS_Core/DataModel/DdTableDealPBN.cs new file mode 100644 index 00000000..7aa217fc --- /dev/null +++ b/dotnet/DDS_Core/DataModel/DdTableDealPBN.cs @@ -0,0 +1,15 @@ +using System.Runtime.InteropServices; + +namespace DDS_Core; + +/// +/// A single deal in PBN format for double dummy table calculation. +/// +/// Uses PBN (Portable Bridge Notation) string representation. +/// +[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] +public struct DdTableDealPBN +{ + /// Cards in PBN format (max 80 characters). + public string80 Cards; +} diff --git a/dotnet/DDS_Core/DataModel/DdTableDeals.cs b/dotnet/DDS_Core/DataModel/DdTableDeals.cs new file mode 100644 index 00000000..cfaaffd5 --- /dev/null +++ b/dotnet/DDS_Core/DataModel/DdTableDeals.cs @@ -0,0 +1,48 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace DDS_Core; + +/// +/// Multiple deals for batch double dummy table calculation. +/// +[StructLayout(LayoutKind.Sequential)] +public struct DdTableDeals +{ + /// Number of tables. + public int NumberOfTables; + + /// Array of deals (up to MAXNOOFTABLES * DDS_STRAINS). + public DdTableDealsArray Deals; + + + #region Nested Types + + [InlineArray(DdsConstants.DdsStrains)] + public struct DdTableDealsArray + { + private DdTableDeal item; + + public static implicit operator DdTableDealsArray(DdTableDeal[] array) + { + var result = new DdTableDealsArray(); + + if (array != null) + { + var span = result.AsSpan(); + + for (int i = 0; i < Math.Min(array.Length, span.Length); i++) + span[i] = array[i]; + } + + return result; + } + + // Implicit conversion from DdTableDealsArray to Span + private Span AsSpan() + { + return System.Runtime.InteropServices.MemoryMarshal.CreateSpan(ref item, DdsConstants.DdsStrains); + } + } + #endregion +} diff --git a/dotnet/DDS_Core/DataModel/DdTableDealsPBN.cs b/dotnet/DDS_Core/DataModel/DdTableDealsPBN.cs new file mode 100644 index 00000000..2b044dea --- /dev/null +++ b/dotnet/DDS_Core/DataModel/DdTableDealsPBN.cs @@ -0,0 +1,46 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace DDS_Core; + +/// +/// Multiple deals in PBN format for batch double dummy table calculation. +/// +[StructLayout(LayoutKind.Sequential)] +public struct DdTableDealsPBN +{ + /// Number of tables. + public int NumberOfTables; + + /// Array of PBN deals (up to MAXNOOFTABLES * DDS_STRAINS). + public DdTableDealsPBNArray Deals; + + #region Nested Types + [InlineArray(DdsConstants.DdsStrains)] + public struct DdTableDealsPBNArray + { + private DdTableDealPBN item; + + public static implicit operator DdTableDealsPBNArray(DdTableDealPBN[] array) + { + var result = new DdTableDealsPBNArray(); + + if (array != null) + { + var span = result.AsSpan(); + + for (int i = 0; i < Math.Min(array.Length, span.Length); i++) + span[i] = array[i]; + } + + return result; + } + + // Implicit conversion from DdTableDealsPBNArray to Span + private Span AsSpan() + { + return System.Runtime.InteropServices.MemoryMarshal.CreateSpan(ref item, DdsConstants.DdsStrains); + } + } + #endregion +} diff --git a/dotnet/DDS_Core/DataModel/DdTablesResult.cs b/dotnet/DDS_Core/DataModel/DdTablesResult.cs new file mode 100644 index 00000000..800897c8 --- /dev/null +++ b/dotnet/DDS_Core/DataModel/DdTablesResult.cs @@ -0,0 +1,69 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace DDS_Core; + +/// +/// Multiple double dummy table results. +/// +[StructLayout(LayoutKind.Sequential)] +public struct DdTablesResult +{ + /// Number of boards. + public int no_of_boards; + + /// Array of results (up to MAXNOOFTABLES * DDS_STRAINS = 200). + public DdTableResultsArray Results; + + /// + /// Safe indexer with bounds checking against actual no_of_boards. + /// + public DdTableResults this[int index] + { + get + { + if (index < 0 || index >= no_of_boards) + throw new IndexOutOfRangeException($"Index {index} out of range [0, {no_of_boards - 1}]"); + + return Results[index]; + } + + set + { + if (index < 0 || index >= no_of_boards) + throw new IndexOutOfRangeException($"Index {index} out of range [0, {no_of_boards - 1}]"); + + Results[index] = value; + } + } + + #region Nested Types + [InlineArray(DdsConstants.MaxNumberOfTables * DdsConstants.DdsStrains)] + public struct DdTableResultsArray + { + private DdTableResults item; + + public static implicit operator DdTableResultsArray(DdTableResults[] array) + { + var result = new DdTableResultsArray(); + + if (array != null) + { + var span = result.AsSpan(); + + for (int i = 0; i < Math.Min(array.Length, span.Length); i++) + span[i] = array[i]; + } + + return result; + } + + // Implicit conversion from DdTableResultsArray to Span + private Span AsSpan() + { + return System.Runtime.InteropServices.MemoryMarshal.CreateSpan(ref item, DdsConstants.MaxNumberOfTables * DdsConstants.DdsStrains); + } + } + #endregion +} diff --git a/dotnet/DDS_Core/DataModel/Deal.cs b/dotnet/DDS_Core/DataModel/Deal.cs new file mode 100644 index 00000000..6ede0840 --- /dev/null +++ b/dotnet/DDS_Core/DataModel/Deal.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using DDS_Core.Helpers; + +namespace DDS_Core; + +/// +/// Represents a bridge Deal for double dummy analysis. +/// +[StructLayout(LayoutKind.Sequential)] +public struct Deal +{ + /// Trump suit (0 = NT, 1 = Spades, 2 = Hearts, 3 = Diamonds, 4 = Clubs). + public int Trump; + + /// Hand to play first (0 = N, 1 = E, 2 = S, 3 = W). + public int First; + + /// Suits of cards played in the current trick (3 entries). + public intArray3 CurrentTrickSuit; + + /// Ranks of cards played in the current trick (3 entries). + public intArray3 CurrentTrickRank; + + /// Remaining cards for each hand and suit (4 hands × 4 suits = 16 elements). + public FourHands RemainingCards; +} diff --git a/dotnet/DDS_Core/DataModel/DealPBN.cs b/dotnet/DDS_Core/DataModel/DealPBN.cs new file mode 100644 index 00000000..73841432 --- /dev/null +++ b/dotnet/DDS_Core/DataModel/DealPBN.cs @@ -0,0 +1,28 @@ +using System.Runtime.InteropServices; + +namespace DDS_Core; + +/// +/// Represents a bridge Deal in PBN (Portable Bridge Notation) format. +/// +/// PBN format is a standard text representation for bridge hands. +/// Example: "N:AKQ.K.AKQ.AKQ8 J976.QJT.J42.Q2 T842.A542.T83.T3 53.9876.965.J976" +/// +[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] +public struct DealPBN +{ + /// Trump suit (0 = NT, 1 = Spades, 2 = Hearts, 3 = Diamonds, 4 = Clubs). + public int Trump; + + /// Hand to play first (0 = N, 1 = E, 2 = S, 3 = W). + public int First; + + /// Suits of cards played in the current trick (3 entries). + public intArray3 CurrentTrickSuit; + + /// Ranks of cards played in the current trick (3 entries). + public intArray3 CurrentTrickRank; + + /// PBN string describing remaining cards (max 80 characters). + public string80 RemainingCards; +} diff --git a/dotnet/DDS_Core/DataModel/Enums and Constants/CardRanks.cs b/dotnet/DDS_Core/DataModel/Enums and Constants/CardRanks.cs new file mode 100644 index 00000000..bc9a9e67 --- /dev/null +++ b/dotnet/DDS_Core/DataModel/Enums and Constants/CardRanks.cs @@ -0,0 +1,21 @@ +namespace DDS_Core; + +public enum CardRanks : uint +{ + None = 0 + , rA = 0x00000001 <<14 + , rK = 0x00000001 <<13 + , rQ = 0x00000001 <<12 + , rJ = 0x00000001 <<11 + , rT = 0x00000001 <<10 + , r9 = 0x00000001 <<9 + , r8 = 0x00000001 <<8 + , r7 = 0x00000001 <<7 + , r6 = 0x00000001 <<6 + , r5 = 0x00000001 <<5 + , r4 = 0x00000001 <<4 + , r3 = 0x00000001 <<3 + , r2 = 0x00000001 <<2 + , All = 0x00007FFC // All ranks (except None) +}; + diff --git a/dotnet/DDS_Core/DataModel/Enums and Constants/DdsConstants.cs b/dotnet/DDS_Core/DataModel/Enums and Constants/DdsConstants.cs new file mode 100644 index 00000000..7fefc915 --- /dev/null +++ b/dotnet/DDS_Core/DataModel/Enums and Constants/DdsConstants.cs @@ -0,0 +1,28 @@ +using System; + +namespace DDS_Core +{ + /// + /// Constants for DDS bridge analysis. + /// + public static class DdsConstants + { + /// Number of bridge strains (4 suits + no trump). + public const int DdsStrains = 5; + + /// Number of hands (N/E/S/W). + public const int DdsHands = 4; + + /// Number of suits (S/H/D/C). + public const int DdsSuits = 4; + + /// No trump strain index. + public const int DdsNoTrump = 4; + + /// Maximum number of boards in batch operations. + public const int MaxNumberOfBoards = 200; + + /// Maximum number of DD tables. + public const int MaxNumberOfTables = 40; + } +} diff --git a/dotnet/DDS_Core/DataModel/Enums and Constants/Hand.cs b/dotnet/DDS_Core/DataModel/Enums and Constants/Hand.cs new file mode 100644 index 00000000..d276ad32 --- /dev/null +++ b/dotnet/DDS_Core/DataModel/Enums and Constants/Hand.cs @@ -0,0 +1,3 @@ +namespace DDS_Core; + +public enum Hand : int { North = 0, East = 1, South = 2, West = 3 } diff --git a/dotnet/DDS_Core/DataModel/Enums and Constants/SolveBoardResult.cs b/dotnet/DDS_Core/DataModel/Enums and Constants/SolveBoardResult.cs new file mode 100644 index 00000000..932365ab --- /dev/null +++ b/dotnet/DDS_Core/DataModel/Enums and Constants/SolveBoardResult.cs @@ -0,0 +1,129 @@ +namespace DDS_Core; + +/// +/// DDS return codes for SolveBoard() and related functions. +/// +public enum SolveBoardResult +{ + /// + /// Success - no fault detected. + /// + NoFault = 1, + + /// + /// General error. Currently happens when fopen() fails or when AnalyseAllPlaysBin() + /// gets a different number of Boards in its first two arguments. + /// + UnknownFault = -1, + + /// + /// Zero cards supplied. + /// + ZeroCards = -2, + + /// + /// Target exceeds number of tricks remaining. + /// + TargetTooHigh = -3, + + /// + /// Cards duplicated. + /// + DuplicateCards = -4, + + /// + /// Target is less than -1. + /// + TargetWrongLo = -5, + + /// + /// Target is higher than 13. + /// + TargetWrongHi = -7, + + /// + /// Solutions parameter is less than 1. + /// + SolutionsWrongLo = -8, + + /// + /// Solutions parameter is higher than 3. + /// + SolutionsWrongHi = -9, + + /// + /// Too many cards supplied. + /// + TooManyCards = -10, + + /// + /// currentTrickSuit or currentTrickRank has wrong data. + /// + SuitOrRank = -12, + + /// + /// Played card also remains in a hand. + /// + PlayedCard = -13, + + /// + /// Wrong number of remaining cards in a hand. + /// + CardCount = -14, + + /// + /// Thread index is not 0 .. maximum. + /// + ThreadIndex = -15, + + /// + /// Mode parameter is less than 0. + /// + ModeWrongLo = -16, + + /// + /// Mode parameter is higher than 2. + /// + ModeWrongHi = -17, + + /// + /// Trump is not in 0 .. 4. + /// + TrumpWrong = -18, + + /// + /// First is not in 0 .. 2. + /// + FirstWrong = -19, + + /// + /// AnalysePlay input error (less than 0 or more than 52 cards, + /// invalid suit or rank, or played card is not held by the right player). + /// + PlayFault = -98, + + /// + /// PBN string error. + /// + PbnFault = -99, + + /// + /// Too many Boards requested. + /// + TooManyBoards = -101, + + /// + /// Could not create threads. + /// + ThreadCreate = -102, + + /// + /// Something failed waiting for thread to end. + /// + ThreadWait = -103, + + /// + /// Tried to set a multi-threading system that is not present in DLL. + /// + ThreadMissing = -104 +} diff --git a/dotnet/DDS_Core/DataModel/Enums and Constants/SolvingMode.cs b/dotnet/DDS_Core/DataModel/Enums and Constants/SolvingMode.cs new file mode 100644 index 00000000..66d74e37 --- /dev/null +++ b/dotnet/DDS_Core/DataModel/Enums and Constants/SolvingMode.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices; + +namespace DDS_Core; + +public enum SolvingMode +{ + Auto = 0, + Par = 1, + SingleDummy = 2, + DoubleDummy = 3 +} diff --git a/dotnet/DDS_Core/DataModel/Enums and Constants/Suit.cs b/dotnet/DDS_Core/DataModel/Enums and Constants/Suit.cs new file mode 100644 index 00000000..ad23c32e --- /dev/null +++ b/dotnet/DDS_Core/DataModel/Enums and Constants/Suit.cs @@ -0,0 +1,5 @@ +using System.Runtime.InteropServices; + +namespace DDS_Core; + +public enum Suit : int { Spades = 0, Hearts = 1, Diamonds = 2, Clubs = 3 } diff --git a/dotnet/DDS_Core/DataModel/Enums and Constants/TTKind.cs b/dotnet/DDS_Core/DataModel/Enums and Constants/TTKind.cs new file mode 100644 index 00000000..80e6e0ba --- /dev/null +++ b/dotnet/DDS_Core/DataModel/Enums and Constants/TTKind.cs @@ -0,0 +1,7 @@ +namespace DDS_Core; + +public enum TTKind : int +{ + Small = 0, + Large = 1 +} diff --git a/dotnet/DDS_Core/DataModel/FutureTricks.cs b/dotnet/DDS_Core/DataModel/FutureTricks.cs new file mode 100644 index 00000000..bdadfa8d --- /dev/null +++ b/dotnet/DDS_Core/DataModel/FutureTricks.cs @@ -0,0 +1,32 @@ +using System; +using System.Runtime.InteropServices; + +namespace DDS_Core; + +/// +/// Stores the result of a double dummy analysis for a single position. +/// +/// Contains the number of nodes searched, the number of cards in the result, +/// and arrays for each card's suit, rank, equality group, and score. +/// +[StructLayout(LayoutKind.Sequential)] +public struct FutureTricks +{ + /// Number of nodes searched in the analysis. + public int Nodes; + + /// Number of cards in the results. + public int NumberOfCards; + + /// Suit of each card (13 entries). + public intArray13 Suit; + + /// Rank of each card (13 entries). + public intArray13 Ranks; + + /// Equality group for each card (13 entries). + public intArray13 EqualGroups; + + /// Score for each card (13 entries). + public intArray13 Score; +} diff --git a/dotnet/DDS_Core/DataModel/ParResultsDealer.cs b/dotnet/DDS_Core/DataModel/ParResultsDealer.cs new file mode 100644 index 00000000..e54e8334 --- /dev/null +++ b/dotnet/DDS_Core/DataModel/ParResultsDealer.cs @@ -0,0 +1,23 @@ +using System; +using System.Drawing; +using System.Runtime.InteropServices; + +namespace DDS_Core; + +/// +/// Par result for a specific dealer. +/// +/// Contains number of par contracts and their details. +/// +[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] +public struct ParResultsDealer +{ + /// Number of contracts yielding the par score. + public int NumberOfContracts; + + /// Par score for the specified dealer hand. + public int Score; + + /// Par contract text strings (10 entries, max 10 chars each). + public stringArray10x10 Contracts; +} diff --git a/dotnet/DDS_Core/DataModel/ParResultsDealers.cs b/dotnet/DDS_Core/DataModel/ParResultsDealers.cs new file mode 100644 index 00000000..519b9151 --- /dev/null +++ b/dotnet/DDS_Core/DataModel/ParResultsDealers.cs @@ -0,0 +1,43 @@ +using System; +using System.Runtime.InteropServices; + +namespace DDS_Core +{ + + /// + /// ParResultsDealer[2] + /// + /// Contains number of par contracts and their details. + /// + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] + public struct ParResultsDealers + { + private ParResultsDealer parResultsDealersNS; + private ParResultsDealer parResultsDealersEW; + + public ParResultsDealer this[int index] + { + get + { + if ((uint)index >= 2) + throw new IndexOutOfRangeException(); + + if (index == 0) + return parResultsDealersNS; + else + return parResultsDealersEW; + } + + set + { + if ((uint)index >= 2) + throw new IndexOutOfRangeException(); + + if (index == 0) + parResultsDealersNS = value; + else + parResultsDealersEW = value; + } + } + } +} diff --git a/dotnet/DDS_Core/DataModel/ParResultsMaster.cs b/dotnet/DDS_Core/DataModel/ParResultsMaster.cs new file mode 100644 index 00000000..bd44019e --- /dev/null +++ b/dotnet/DDS_Core/DataModel/ParResultsMaster.cs @@ -0,0 +1,29 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace DDS_Core; + +/// +/// Par contracts for both dealer and vulnerable combinations. +/// +[StructLayout(LayoutKind.Sequential)] +public struct ParResultsMaster +{ + /// Par score (sign according to NS view). + public int Score; + + /// Number of contracts giving the par score. + public int Number; + + /// InlineArray of 10 par contracts. + public ContractTypes Contracts; + + #region Nested Types + [InlineArray(10)] + public struct ContractTypes + { + private ContractType Contract; + } + #endregion +} diff --git a/dotnet/DDS_Core/DataModel/ParResultsMasters.cs b/dotnet/DDS_Core/DataModel/ParResultsMasters.cs new file mode 100644 index 00000000..98bec397 --- /dev/null +++ b/dotnet/DDS_Core/DataModel/ParResultsMasters.cs @@ -0,0 +1,43 @@ +using System; +using System.Runtime.InteropServices; + +namespace DDS_Core +{ + + /// + /// Par contracts for both dealer and vulnerable combinations. + /// + [StructLayout(LayoutKind.Sequential)] + + public struct ParResultsMasters + { + /// InlineArray of 10 par contracts. + private ParResultsMaster parResultsMaster0; + private ParResultsMaster parResultsMaster1; + + public ParResultsMaster this[int index] + { + get + { + if ((uint)index >= 2) + throw new IndexOutOfRangeException(); + + if (index == 0) + return parResultsMaster0; + else + return parResultsMaster1; + } + + set + { + if ((uint)index >= 2) + throw new IndexOutOfRangeException(); + + if (index == 0) + parResultsMaster0 = value; + else + parResultsMaster1 = value; + } + } + } +} diff --git a/dotnet/DDS_Core/DataModel/ParTextResults.cs b/dotnet/DDS_Core/DataModel/ParTextResults.cs new file mode 100644 index 00000000..c37e7671 --- /dev/null +++ b/dotnet/DDS_Core/DataModel/ParTextResults.cs @@ -0,0 +1,22 @@ +using System.Runtime.InteropServices; + +namespace DDS_Core; + +/// +/// Text representation of par results. +/// +/// Includes short text summary and information about equality. +/// +[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] +public struct ParTextResults +{ + /// + /// Short par text for NS and EW using 2D indexing (2 x 128 chars). + /// Access: par_text[side, index] where side=0(NS) or 1(EW) + /// + public stringArray2x128 ParTextStrings; + + /// True if equal (doesn't matter who starts bidding), false otherwise. + [MarshalAs(UnmanagedType.I1)] + public bool IsEqual; +} diff --git a/dotnet/DDS_Core/DataModel/PlayTraceBin.cs b/dotnet/DDS_Core/DataModel/PlayTraceBin.cs new file mode 100644 index 00000000..44a30efb --- /dev/null +++ b/dotnet/DDS_Core/DataModel/PlayTraceBin.cs @@ -0,0 +1,24 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace DDS_Core; + +/// +/// Binary play trace (cards played in sequence). +/// +/// Contains card suits and ranks for cards played during the hand. +/// +[StructLayout(LayoutKind.Sequential)] +public struct PlayTraceBin +{ + /// Number of cards played (1-52). + public int NumberOfCards; + + /// Suit of each card played (52 plays max). + public intArray52 Suits; + + /// Rank of each card played (52 plays max). + public intArray52 Ranks; + +} diff --git a/dotnet/DDS_Core/DataModel/PlayTracePBN.cs b/dotnet/DDS_Core/DataModel/PlayTracePBN.cs new file mode 100644 index 00000000..9a196627 --- /dev/null +++ b/dotnet/DDS_Core/DataModel/PlayTracePBN.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace DDS_Core; + +/// +/// PBN format play trace (cards played in sequence). +/// +/// Uses PBN string representation for played cards. +/// +[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] +public struct PlayTracePBN +{ + /// Number of cards played. + public int NumberOfPlayedCards; + + /// Cards in PBN format (max 106 characters). + public string106 Cards; + +} diff --git a/dotnet/DDS_Core/DataModel/PlayTracesBin.cs b/dotnet/DDS_Core/DataModel/PlayTracesBin.cs new file mode 100644 index 00000000..4652c8ea --- /dev/null +++ b/dotnet/DDS_Core/DataModel/PlayTracesBin.cs @@ -0,0 +1,24 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace DDS_Core; + +/// +/// Multiple binary play traces for batch analysis. +/// +[StructLayout(LayoutKind.Sequential)] +public struct PlayTracesBin +{ + /// Number of boards. + public int NumberOfBoards; + + /// Array of play traces (up to MAXNOOFBOARDS). + [MarshalAs(UnmanagedType.ByValArray, SizeConst = DdsConstants.MaxNumberOfBoards)] + public PlayTraceBin[] Plays; + + public PlayTracesBin() + { + Plays = new PlayTraceBin[DdsConstants.MaxNumberOfBoards]; + } +} diff --git a/dotnet/DDS_Core/DataModel/PlayTracesPBN.cs b/dotnet/DDS_Core/DataModel/PlayTracesPBN.cs new file mode 100644 index 00000000..daaaa7f3 --- /dev/null +++ b/dotnet/DDS_Core/DataModel/PlayTracesPBN.cs @@ -0,0 +1,23 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace DDS_Core; + +/// +/// Multiple PBN play traces for batch analysis. +/// +[StructLayout(LayoutKind.Sequential)] +public struct PlayTracesPBN +{ + /// Number of boards. + public int NumberOfBoards; + + /// Array of PBN play traces (up to MAXNOOFBOARDS). + [MarshalAs(UnmanagedType.ByValArray, SizeConst = DdsConstants.MaxNumberOfBoards)] + public PlayTracePBN[] Plays; + + public PlayTracesPBN() + { + Plays = new PlayTracePBN[DdsConstants.MaxNumberOfBoards]; + } +} diff --git a/dotnet/DDS_Core/DataModel/SolvedBoards.cs b/dotnet/DDS_Core/DataModel/SolvedBoards.cs new file mode 100644 index 00000000..096e2c8f --- /dev/null +++ b/dotnet/DDS_Core/DataModel/SolvedBoards.cs @@ -0,0 +1,49 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace DDS_Core; + +/// +/// Solutions for multiple boards. +/// +/// Container for results from batch board solving operations. +/// Each entry contains the complete future tricks analysis for one board. +/// +[StructLayout(LayoutKind.Sequential)] +public struct SolvedBoards +{ + /// Number of solved boards. + public int NumberOfBoards; + + /// Array of solutions (future tricks for each board). + public FutureTricksArray Tricks; + + #region Nested Types + [InlineArray(DdsConstants.MaxNumberOfBoards)] + public struct FutureTricksArray + { + private FutureTricks item; + + public static implicit operator FutureTricksArray(FutureTricks[] array) + { + var result = new FutureTricksArray(); + + if (array != null) + { + var span = result.AsSpan(); + + for (int i = 0; i < Math.Min(array.Length, span.Length); i++) + span[i] = array[i]; + } + + return result; + } + + // Implicit conversion from FutureTricksArray to Span + private Span AsSpan() + { + return System.Runtime.InteropServices.MemoryMarshal.CreateSpan(ref item, DdsConstants.MaxNumberOfBoards); + } + } + #endregion +} diff --git a/dotnet/DDS_Core/DataModel/SolvedPlay.cs b/dotnet/DDS_Core/DataModel/SolvedPlay.cs new file mode 100644 index 00000000..4fbc9045 --- /dev/null +++ b/dotnet/DDS_Core/DataModel/SolvedPlay.cs @@ -0,0 +1,20 @@ +using System; +using System.Runtime.InteropServices; + +namespace DDS_Core; + + +/// +/// Analyzed result of a play sequence. +/// +/// Contains tricks won for each possible play continuation. +/// +[StructLayout(LayoutKind.Sequential)] +public struct SolvedPlay +{ + /// Number of results. + public int NumberOfResults; + + /// Tricks possible after each play (53 entries). + public IntArray53 Tricks; +} diff --git a/dotnet/DDS_Core/DataModel/SolvedPlays.cs b/dotnet/DDS_Core/DataModel/SolvedPlays.cs new file mode 100644 index 00000000..42a42eb3 --- /dev/null +++ b/dotnet/DDS_Core/DataModel/SolvedPlays.cs @@ -0,0 +1,49 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace DDS_Core; + +/// +/// Analyzed results of multiple play sequences. +/// +[StructLayout(LayoutKind.Sequential)] +public struct SolvedPlays +{ + /// Number of solved plays. + public int NumberOfPlayedBoards; + + /// Array of solved play results (up to MAXNOOFBOARDS). + //[MarshalAs(UnmanagedType.ByValArray, SizeConst = DdsConstants.MaxNumberOfBoards)] + //public SolvedPlay[] Solved; + public SolvedPlayArray Solved; + + #region Nested Types + [InlineArray(DdsConstants.MaxNumberOfBoards)] + public struct SolvedPlayArray + { + private SolvedPlay item; + + public static implicit operator SolvedPlayArray(SolvedPlay[] array) + { + var result = new SolvedPlayArray(); + + if (array != null) + { + var span = result.AsSpan(); + + for (int i = 0; i < Math.Min(array.Length, span.Length); i++) + span[i] = array[i]; + } + + return result; + } + + // Implicit conversion from SolvedPlayArray to Span + private Span AsSpan() + { + return System.Runtime.InteropServices.MemoryMarshal.CreateSpan(ref item, DdsConstants.MaxNumberOfBoards); + } + } + #endregion +} diff --git a/dotnet/DDS_Core/DataModel/SolverConfig.cs b/dotnet/DDS_Core/DataModel/SolverConfig.cs new file mode 100644 index 00000000..1d05a509 --- /dev/null +++ b/dotnet/DDS_Core/DataModel/SolverConfig.cs @@ -0,0 +1,23 @@ +using System.Runtime.InteropServices; + +namespace DDS_Core; + +[StructLayout(LayoutKind.Sequential)] +public struct SolverConfig +{ + public TTKind TTKind; + public int DefaultMemoryMB; + public int MaximumMemoryMB; + + public SolverConfig() + { + TTKind = TTKind.Large; + } + + public SolverConfig(TTKind tTKind, int defaultMemoryMB, int maximumMemoryMB) + { + TTKind = tTKind; + DefaultMemoryMB = defaultMemoryMB; + MaximumMemoryMB = maximumMemoryMB; + } +} diff --git a/dotnet/DDS_Core/DataModel/SolverContext.cs b/dotnet/DDS_Core/DataModel/SolverContext.cs new file mode 100644 index 00000000..bdf29cc7 --- /dev/null +++ b/dotnet/DDS_Core/DataModel/SolverContext.cs @@ -0,0 +1,148 @@ +using System.Diagnostics; +using DDS_Core.Helpers; +using DDS_Core.Native; + +namespace DDS_Core; + +public sealed class SolverContext : IDisposable +{ + public SolverContextHandle Handle { get; } + + #region Constructors and destructors + public SolverContext() + { + Handle = DdsNative.dds_create_solvercontext_default() + ?? throw new InvalidOperationException("Failed to create SolverContext."); + } + + public SolverContext(SolverConfig config) + { + Handle = DdsNative.dds_create_solvercontext(config) + ?? throw new InvalidOperationException("Failed to create SolverContext."); + } + + public void Dispose() + { + Handle?.Dispose(); + GC.SuppressFinalize(this); + } + #endregion + + #region TT Management + public void ConfigureTT(TTKind kind, int defaultMb, int maxMb) => DdsNative.dds_configure_tt(Handle, kind, defaultMb, maxMb); + + public void ResizeTT(int defaultMb, int maxMb) => DdsNative.dds_resize_tt(Handle, defaultMb, maxMb); + + public void ClearTT() => DdsNative.dds_clear_tt(Handle); + + public void ResetForSolve() => DdsNative.dds_reset_for_solve(Handle); + + public void ResetBestMovesLite() => DdsNative.dds_reset_best_moves_lite(Handle); + #endregion + + #region Solving + public int SolveBoard( in Deal dl + , int target + , int solutions + , int mode + , out FutureTricks fut) + { + var rc = DdsNative.dds_solve_board( Handle + , dl + , target + , solutions + , mode + , out fut); + + ThrowIfError(rc, nameof(SolveBoard)); + return rc; + } + + #region CalcDdTable - Double Dummy Table and Par + public int CalcDdTable(in DdTableDeal table_deal, out DdTableResults table_results) + { + var rc = DdsNative.dds_calc_dd_table( Handle + , in table_deal + , out table_results); + + ThrowIfError(rc, nameof(CalcDdTable)); + return rc; + } + + public int CalcDdTable(in DdTableDealPBN table_deal_pbn, out DdTableResults table_results) + { + var rc = DdsNative.dds_calc_dd_table_pbn( Handle + , in table_deal_pbn + , out table_results); + + ThrowIfError(rc, nameof(CalcDdTable)); + return rc; + } + + /// + /// Calculates par score and contracts for a deal table. + /// + /// + /// + /// Computes the double dummy table for the given deal, then calculates par score + /// and contracts based on vulnerability. + /// + /// + /// This function is equivalent to calling CalcDDtable followed by + /// Par in the legacy C API. + /// + /// + /// + /// Deal represented as card holdings for each hand. + /// + /// + /// Vulnerability (0=None, 1=Both, 2=NS, 3=EW). + /// + /// + /// Output: double dummy table results. + /// + /// + /// Output: par score and contract strings. + /// + /// + /// Error code (RETURN_NO_FAULT on success). + /// + public int CalcPar( in DdTableDeal table_deal + , int vulnerable + , out DdTableResults table_results + , out ParResults par_results) + { + var rc = DdsNative.dds_calc_par( Handle + , in table_deal + , vulnerable + , out table_results + , out par_results); + ThrowIfError(rc, nameof(CalcPar)); + return rc; + } + #endregion + #endregion + + #region Logging + /// + /// Appends a message to the DDS log. + /// + /// The message to append to the log. + public void LogAppend(string message) => DdsNative.dds_log_append(Handle, message); + + /// + /// Clears the DDS log. + /// + public void LogClear() => DdsNative.dds_log_clear(Handle); + #endregion + + #region private methods + [Conditional("DEBUG")] + private static void ThrowIfError(int result, string functionName) + { + if (result != (int)SolveBoardResult.NoFault) + throw new InvalidOperationException($"{functionName} failed with code {result}: {result.GetRCErrorMessage()}"); + } + #endregion +} + diff --git a/dotnet/DDS_Core/DataModel/ddTableDeal.cs b/dotnet/DDS_Core/DataModel/ddTableDeal.cs new file mode 100644 index 00000000..d983e13f --- /dev/null +++ b/dotnet/DDS_Core/DataModel/ddTableDeal.cs @@ -0,0 +1,21 @@ +using System; +using System.Runtime.InteropServices; + +namespace DDS_Core; + +/// +/// A single deal for double dummy table calculation. +/// +/// Contains card distribution as bitmasks for efficient computation. +/// C++ type: unsigned int cards[DDS_HANDS][DDS_SUITS] = unsigned int cards[4][4] +/// +[StructLayout(LayoutKind.Sequential)] +public struct DdTableDeal +{ + /// + /// Cards for each hand and suit using 2D indexing. + /// Access: cards[hand, suit] where hand=0-3 (N/E/S/W), suit=0-3 (S/H/D/C) + /// Each uint is a bitmask of cards (bit 0 = Deuce, bit 12 = Ace) + /// + public FourHands Cards; +} diff --git a/dotnet/DDS_Core/DataModel/ddTableResults.cs b/dotnet/DDS_Core/DataModel/ddTableResults.cs new file mode 100644 index 00000000..7915ceca --- /dev/null +++ b/dotnet/DDS_Core/DataModel/ddTableResults.cs @@ -0,0 +1,19 @@ +using System.Runtime.InteropServices; + +namespace DDS_Core; + +/// +/// Double dummy results for all strains and hands. +/// +/// Results indexed by strain (0-4: S/H/D/C/NT) and hand (0-3: N/E/S/W). +/// Each entry contains the number of tricks that hand can take in that strain. +/// +[StructLayout(LayoutKind.Sequential)] +public struct DdTableResults +{ + /// + /// Tricks per strain and hand using 2D indexing. + /// Access: res_table[strain, hand] where strain=0-4, hand=0-3 + /// + public intArray5x4 ResultsTable; +} diff --git a/dotnet/DDS_Core/DataModel/parResults.cs b/dotnet/DDS_Core/DataModel/parResults.cs new file mode 100644 index 00000000..13bd5157 --- /dev/null +++ b/dotnet/DDS_Core/DataModel/parResults.cs @@ -0,0 +1,26 @@ +using System.Runtime.InteropServices; + +namespace DDS_Core; + +/// +/// Par score and contracts for a single declarer/strain combination. +/// +/// Includes both NS and EW perspectives. +/// Index 0 = NS view, Index 1 = EW view. +/// +[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] +public struct ParResults +{ + /// + /// Par score strings for NS and EW (2 entries, max 16 chars each). + /// Access: par_score[side, index] where side=0(NS) or 1(EW) + /// + public stringArray2x16 ParScores; + + + /// + /// Par contract strings for NS and EW (2 entries, max 128 chars each). + /// Access: par_contracts_string[side, index] where side=0(NS) or 1(EW) + /// + public stringArray2x128 ParContractStrings; +} diff --git a/dotnet/DDS_Core/Helpers/FourHands.cs b/dotnet/DDS_Core/Helpers/FourHands.cs new file mode 100644 index 00000000..3c2e18e6 --- /dev/null +++ b/dotnet/DDS_Core/Helpers/FourHands.cs @@ -0,0 +1,88 @@ +using System.Runtime.InteropServices; + +namespace DDS_Core; + +[StructLayout(LayoutKind.Sequential)] +public unsafe struct FourHands +{ + public const int ROWS = 4; + public const int COLS = 4; + public const int SIZE = ROWS * COLS; + + private fixed uint data[SIZE]; + + public FourHands() + { + } + + public FourHands(uint[][] src) + { + int k = 0; + + for (int r = 0; r < Math.Min(ROWS ,src.Length); r++) + for (int c = 0; c < Math.Min(COLS, src[r].Length); c++) + data[k++] = src[r][c]; + } + public Span AsSpan() + => MemoryMarshal.CreateSpan(ref data[0], SIZE); + + public Span RowAsSpan(int row) + { + if ((uint)row >= ROWS) + throw new ArgumentOutOfRangeException(nameof(row)); + + // ref to first byte in the array for the specified row + ref uint start = ref data[row * COLS]; + + return MemoryMarshal.CreateSpan(ref start, COLS); + } + + public Span this[int row] + { + get + { + if ((uint)row >= ROWS) + throw new IndexOutOfRangeException(); + + // ref to the first element in the specified row + ref uint start = ref data[row * COLS]; + + // correct way to create a span over the fixed buffer + return MemoryMarshal.CreateSpan(ref start, COLS); + } + } + + public ref uint this[int row, int col] + { + get + { + if ((uint)row >= ROWS || (uint)col >= COLS) + throw new IndexOutOfRangeException(); + + return ref data[row * COLS + col]; + } + } + + public static implicit operator FourHands(uint[] src) + { + var buf = new FourHands(); + + for (int c = 0; c < SIZE; c++) + buf.data[c] = src[c]; + + return buf; + } + + public static implicit operator FourHands(uint[][] src) + { + var buf = new FourHands(); + + int k = 0; + + for (int r = 0; r < Math.Min(ROWS ,src.Length); r++) + for (int c = 0; c < Math.Min(COLS, src[r].Length); c++) + buf.data[k++] = src[r][c]; + + return buf; + } +} diff --git a/dotnet/DDS_Core/Helpers/Global.cs b/dotnet/DDS_Core/Helpers/Global.cs new file mode 100644 index 00000000..57def295 --- /dev/null +++ b/dotnet/DDS_Core/Helpers/Global.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DDS_Core.Helpers +{ + internal static class Global + { + internal static string[] Denoms = ["S","H", "D", "C", "NT" ]; + internal static string[] Seats = ["N","E", "S", "W", "NS", "EW" ]; + } +} diff --git a/dotnet/DDS_Core/Helpers/IBuffer.cs b/dotnet/DDS_Core/Helpers/IBuffer.cs new file mode 100644 index 00000000..c363c8b7 --- /dev/null +++ b/dotnet/DDS_Core/Helpers/IBuffer.cs @@ -0,0 +1,7 @@ +namespace DDS_Core +{ +public interface IBuffer + { + public string GetString(int row); + } +} diff --git a/dotnet/DDS_Core/Helpers/SolveBoardResultExtensions.cs b/dotnet/DDS_Core/Helpers/SolveBoardResultExtensions.cs new file mode 100644 index 00000000..c8b9ffff --- /dev/null +++ b/dotnet/DDS_Core/Helpers/SolveBoardResultExtensions.cs @@ -0,0 +1,40 @@ +namespace DDS_Core.Helpers +{ + public static class SolveBoardResultExtensions + { + public static string GetRCErrorMessage(this int result) + { + return GetRcErrorMessage((SolveBoardResult)result); + } + + public static string GetRcErrorMessage(this SolveBoardResult result) + => result switch + { + SolveBoardResult.NoFault => "No fault", + SolveBoardResult.UnknownFault => "General error", + SolveBoardResult.ZeroCards => "Zero cards", + SolveBoardResult.TargetTooHigh => "Target exceeds number of tricks", + SolveBoardResult.DuplicateCards => "Cards duplicated", + SolveBoardResult.TargetWrongLo => "Target is less than -1", + SolveBoardResult.TargetWrongHi => "Target is higher than 13", + SolveBoardResult.SolutionsWrongLo => "Solutions parameter is less than 1", + SolveBoardResult.SolutionsWrongHi => "Solutions parameter is higher than 3", + SolveBoardResult.TooManyCards => "Too many cards", + SolveBoardResult.SuitOrRank => "currentTrickSuit or currentTrickRank has wrong data", + SolveBoardResult.PlayedCard => "Played card also remains in a hand", + SolveBoardResult.CardCount => "Wrong number of remaining cards in a hand", + SolveBoardResult.ThreadIndex => "Thread index is not 0 .. maximum", + SolveBoardResult.ModeWrongLo => "Mode parameter is less than 0", + SolveBoardResult.ModeWrongHi => "Mode parameter is higher than 2", + SolveBoardResult.TrumpWrong => "Trump is not in 0 .. 4", + SolveBoardResult.FirstWrong => "First is not in 0 .. 2", + SolveBoardResult.PlayFault => "AnalysePlay input error", + SolveBoardResult.PbnFault => "PBN string error", + SolveBoardResult.TooManyBoards => "Too many Boards requested", + SolveBoardResult.ThreadCreate => "Could not create threads", + SolveBoardResult.ThreadWait => "Something failed waiting for thread to end", + SolveBoardResult.ThreadMissing => "Tried to set a multi-threading system that is not present", + _ => "Unknown error" + }; + } +} diff --git a/dotnet/DDS_Core/Helpers/SolverContextHandle.cs b/dotnet/DDS_Core/Helpers/SolverContextHandle.cs new file mode 100644 index 00000000..f2f09dd5 --- /dev/null +++ b/dotnet/DDS_Core/Helpers/SolverContextHandle.cs @@ -0,0 +1,17 @@ +using System.Runtime.InteropServices; +using DDS_Core.Native; + +namespace DDS_Core; +public sealed class SolverContextHandle : SafeHandle +{ + // Required by marshaling infrastructure, not intended to be used directly. + private SolverContextHandle() : base(IntPtr.Zero, ownsHandle: true) { } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + DdsNative.dds_destroy_solvercontext(handle); + return true; + } +} diff --git a/dotnet/DDS_Core/Helpers/intArray13.cs b/dotnet/DDS_Core/Helpers/intArray13.cs new file mode 100644 index 00000000..7828d3a4 --- /dev/null +++ b/dotnet/DDS_Core/Helpers/intArray13.cs @@ -0,0 +1,31 @@ +using System; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace DDS_Core; + +[InlineArray(13)] +public struct intArray13 +{ + private int item; + + public static implicit operator intArray13(int[] src) + { + var buf = new intArray13(); + var span = buf.AsSpan(); + + int count = Math.Min(span.Length, src.Length); + + for (int i = 0; i < count; i++) + span[i] = src[i]; + + for (int i = count; i < span.Length; i++) + span[i] = 0; + + return buf; + } + + public Span AsSpan() + => System.Runtime.InteropServices.MemoryMarshal.CreateSpan(ref item, 13); + +} diff --git a/dotnet/DDS_Core/Helpers/intArray200.cs b/dotnet/DDS_Core/Helpers/intArray200.cs new file mode 100644 index 00000000..ea7310fe --- /dev/null +++ b/dotnet/DDS_Core/Helpers/intArray200.cs @@ -0,0 +1,31 @@ +using System; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace DDS_Core +{ + [InlineArray(DdsConstants.MaxNumberOfBoards)] + public struct intArray200 + { + private int item; + + public static implicit operator intArray200(int[] src) + { + var buf = new intArray200(); + var span = buf.AsSpan(); + + int count = Math.Min(span.Length, src.Length); + + for (int i = 0; i < count; i++) + span[i] = src[i]; + + for (int i = count; i < span.Length; i++) + span[i] = 0; + + return buf; + } + + public Span AsSpan() + => System.Runtime.InteropServices.MemoryMarshal.CreateSpan(ref item, DdsConstants.MaxNumberOfBoards); + } +} diff --git a/dotnet/DDS_Core/Helpers/intArray3.cs b/dotnet/DDS_Core/Helpers/intArray3.cs new file mode 100644 index 00000000..fe9ea9cc --- /dev/null +++ b/dotnet/DDS_Core/Helpers/intArray3.cs @@ -0,0 +1,31 @@ +using System; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace DDS_Core; + +[InlineArray(3)] +public struct intArray3 +{ + private int item; + + public static implicit operator intArray3(int[] src) + { + var buf = new intArray3(); + var span = buf.AsSpan(); + + int count = Math.Min(span.Length, src.Length); + + for (int i = 0; i < count; i++) + span[i] = src[i]; + + for (int i = count; i < span.Length; i++) + span[i] = 0; + + return buf; + } + + public Span AsSpan() + => System.Runtime.InteropServices.MemoryMarshal.CreateSpan(ref item, 3); + +} diff --git a/dotnet/DDS_Core/Helpers/intArray5.cs b/dotnet/DDS_Core/Helpers/intArray5.cs new file mode 100644 index 00000000..f2eaf9fa --- /dev/null +++ b/dotnet/DDS_Core/Helpers/intArray5.cs @@ -0,0 +1,32 @@ +using System; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace DDS_Core +{ + [InlineArray(5)] + public struct intArray5 + { + private int item; + + public static implicit operator intArray5(int[] src) + { + var buf = new intArray5(); + var span = buf.AsSpan(); + + int count = Math.Min(span.Length, src.Length); + + for (int i = 0; i < count; i++) + span[i] = src[i]; + + for (int i = count; i < span.Length; i++) + span[i] = 0; + + return buf; + } + + public Span AsSpan() + => System.Runtime.InteropServices.MemoryMarshal.CreateSpan(ref item, 5); + + } +} diff --git a/dotnet/DDS_Core/Helpers/intArray52.cs b/dotnet/DDS_Core/Helpers/intArray52.cs new file mode 100644 index 00000000..866ec967 --- /dev/null +++ b/dotnet/DDS_Core/Helpers/intArray52.cs @@ -0,0 +1,31 @@ +using System; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace DDS_Core; + +[InlineArray(52)] +public struct intArray52 +{ + private int item; + + public static implicit operator intArray52(int[] src) + { + var buf = new intArray52(); + var span = buf.AsSpan(); + + int count = Math.Min(span.Length, src.Length); + + for (int i = 0; i < count; i++) + span[i] = src[i]; + + for (int i = count; i < span.Length; i++) + span[i] = 0; + + return buf; + } + + public Span AsSpan() + => System.Runtime.InteropServices.MemoryMarshal.CreateSpan(ref item, 52); + +} diff --git a/dotnet/DDS_Core/Helpers/intArray53.cs b/dotnet/DDS_Core/Helpers/intArray53.cs new file mode 100644 index 00000000..3583146b --- /dev/null +++ b/dotnet/DDS_Core/Helpers/intArray53.cs @@ -0,0 +1,32 @@ +using System; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace DDS_Core +{ + [InlineArray(53)] + public struct IntArray53 + { + private int item; + + public static implicit operator IntArray53(int[] src) + { + var buf = new IntArray53(); + var span = buf.AsSpan(); + + int count = Math.Min(span.Length, src.Length); + + for (int i = 0; i < count; i++) + span[i] = src[i]; + + for (int i = count; i < span.Length; i++) + span[i] = 0; + + return buf; + } + + public Span AsSpan() + => System.Runtime.InteropServices.MemoryMarshal.CreateSpan(ref item, 53); + + } +} diff --git a/dotnet/DDS_Core/Helpers/intArray5x4.cs b/dotnet/DDS_Core/Helpers/intArray5x4.cs new file mode 100644 index 00000000..edbefdff --- /dev/null +++ b/dotnet/DDS_Core/Helpers/intArray5x4.cs @@ -0,0 +1,99 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; + +namespace DDS_Core; + +[DebuggerDisplay("{ToString(),nq}")] +[DebuggerTypeProxy(typeof(intArray5xDebugView))] +[StructLayout(LayoutKind.Sequential)] +public unsafe struct intArray5x4 : IBuffer +{ + public const int ROWS = 5; + public const int COLS = 4; + public const int SIZE = ROWS * COLS; + + private fixed int data[SIZE]; + + public Span AsSpan() + => MemoryMarshal.CreateSpan(ref data[0], SIZE); + + public Span RowAsSpan(int row) + { + if ((uint)row >= ROWS ) + throw new ArgumentOutOfRangeException(nameof(row)); + + // ref to first byte in the array for the specified row + ref int start = ref data[row * COLS]; + + return MemoryMarshal.CreateSpan(ref start, COLS); + } + + public Span this[int row] + { + get + { + if ((uint)row >= ROWS) + throw new IndexOutOfRangeException(); + + // ref to the first element in the specified row + ref int start = ref data[row * COLS]; + + // correct way to create a span over the fixed buffer + return MemoryMarshal.CreateSpan(ref start, COLS); + } + } + + public ref int this[int row, int col] + { + get + { + if ((uint)row >= ROWS || (uint)col >= COLS) + throw new IndexOutOfRangeException(); + + return ref data[row * COLS + col]; + } + } + + public static implicit operator intArray5x4(int[] src) + { + var buf = new intArray5x4(); + int count = Math.Min(src.Length, SIZE); + + for (int c = 0; c < count; c++) + buf.data[c] = src[c]; + + return buf; + } + + public static implicit operator intArray5x4(int[][] src) + { + var buf = new intArray5x4(); + + int rows = Math.Min(src.Length, ROWS); + int cols = src.Length > 0 ? Math.Min(src[0].Length, COLS) : 0; + + int k = 0; + for (int r = 0; r < rows; r++) + for (int c = 0; c < cols; c++) + buf.data[k++] = src[r][c]; + + return buf; + } + + public string GetString(int row) + => string.Join(",", RowAsSpan(row).ToArray()); + + public override string ToString() + { + var parts = new string[ROWS]; + + for (int r = 0; r < ROWS; r++) + { + var row = RowAsSpan(r); + parts[r] = "[" + string.Join(",", row.ToArray()) + "]"; + } + + return string.Join(" | ", parts); + } +} diff --git a/dotnet/DDS_Core/Helpers/intArray5xDebugView.cs b/dotnet/DDS_Core/Helpers/intArray5xDebugView.cs new file mode 100644 index 00000000..d3a50c7a --- /dev/null +++ b/dotnet/DDS_Core/Helpers/intArray5xDebugView.cs @@ -0,0 +1,22 @@ +using System; + +namespace DDS_Core +{ + internal sealed class intArray5xDebugView where T : IBuffer + { + private readonly T _buffer; + + public intArray5xDebugView(T buffer) + { + _buffer = buffer; + } + + public string Row0 => _buffer.GetString(0); + public string Row1 => _buffer.GetString(1); + public string Row2 => _buffer.GetString(2); + public string Row3 => _buffer.GetString(3); + public string Row4 => _buffer.GetString(4); + + } +} + diff --git a/dotnet/DDS_Core/Helpers/string106.cs b/dotnet/DDS_Core/Helpers/string106.cs new file mode 100644 index 00000000..534c66ef --- /dev/null +++ b/dotnet/DDS_Core/Helpers/string106.cs @@ -0,0 +1,86 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; + +namespace DDS_Core; + +/// +/// Byte buffer struct is suitable for interop with native code, but in c# it acts +/// like a simple string with fixed max length per string. Strings are ASCII-encoded +/// and padded with nulls if shorter than max length. +/// +[DebuggerDisplay("{ToString()}")] +[StructLayout(LayoutKind.Sequential)] +public unsafe struct string106 +{ + public const int SIZE = 106; + + private fixed byte data[SIZE]; + + // ------------------------------- + // Indexers + // ------------------------------- + public ref byte this[int index] + { + get + { + if ((uint)index >= SIZE) + throw new IndexOutOfRangeException(); + + return ref data[index]; + } + } + + public string Value + { + get => GetString(); + set => SetString(value); + } + + // ------------------------------- + // String assignment API + // ------------------------------- + public void SetString(string value) + { + if (value is null) + throw new ArgumentNullException(nameof(value)); + + if (value.Length > SIZE) + throw new ArgumentOutOfRangeException(nameof(value)); + + ref byte start = ref data[0]; + var span = MemoryMarshal.CreateSpan(ref start, SIZE); + + // ASCII encode + int written = Encoding.ASCII.GetBytes(value, span); + + if (written < SIZE) + span.Slice(written).Clear(); + } + + public string GetString() + { + ref byte start = ref data[0]; + var span = MemoryMarshal.CreateSpan(ref start, SIZE); + return Encoding.ASCII.GetString(span).TrimEnd('\0'); + } + + public Span AsSpan() + => MemoryMarshal.CreateSpan(ref data[0], SIZE); + + // ------------------------------- + // Implicit conversion + // ------------------------------- + public static implicit operator string106(string src) + { + var buf = new string106(); + buf.SetString(src); + return buf; + } + + // ------------------------------- + // Debugger / ToString + // ------------------------------- + public override string ToString() => GetString(); +} diff --git a/dotnet/DDS_Core/Helpers/string80.cs b/dotnet/DDS_Core/Helpers/string80.cs new file mode 100644 index 00000000..fd69871c --- /dev/null +++ b/dotnet/DDS_Core/Helpers/string80.cs @@ -0,0 +1,86 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; + +namespace DDS_Core; + +/// +/// Byte buffer struct is suitable for interop with native code, but in c# it acts +/// like a simple string with fixed max length per string. Strings are ASCII-encoded +/// and padded with nulls if shorter than max length. +/// +[DebuggerDisplay("{ToString()}")] +[StructLayout(LayoutKind.Sequential)] +public unsafe struct string80 +{ + public const int SIZE = 80; + + private fixed byte data[SIZE]; + + // ------------------------------- + // Indexers + // ------------------------------- + public ref byte this[int index] + { + get + { + if ((uint)index >= SIZE) + throw new IndexOutOfRangeException(); + + return ref data[index]; + } + } + + public string Value + { + get => GetString(); + set => SetString(value); + } + + // ------------------------------- + // String assignment API + // ------------------------------- + public void SetString(string value) + { + if (value is null) + throw new ArgumentNullException(nameof(value)); + + if (value.Length > SIZE) + throw new ArgumentOutOfRangeException(nameof(value)); + + ref byte start = ref data[0]; + var span = MemoryMarshal.CreateSpan(ref start, SIZE); + + // ASCII encode + int written = Encoding.ASCII.GetBytes(value, span); + + if (written < SIZE) + span.Slice(written).Clear(); + } + + public string GetString() + { + ref byte start = ref data[0]; + var span = MemoryMarshal.CreateSpan(ref start, SIZE); + return Encoding.ASCII.GetString(span).TrimEnd('\0'); + } + + public Span AsSpan() + => MemoryMarshal.CreateSpan(ref data[0], SIZE); + + // ------------------------------- + // Implicit conversion + // ------------------------------- + public static implicit operator string80(string src) + { + var buf = new string80(); + buf.SetString(src); + return buf; + } + + // ------------------------------- + // Debugger / ToString + // ------------------------------- + public override string ToString() => GetString(); +} diff --git a/dotnet/DDS_Core/Helpers/stringArray10x10.cs b/dotnet/DDS_Core/Helpers/stringArray10x10.cs new file mode 100644 index 00000000..ee8022eb --- /dev/null +++ b/dotnet/DDS_Core/Helpers/stringArray10x10.cs @@ -0,0 +1,92 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; + +namespace DDS_Core; + + +/// +/// Byte buffer struct is suitable for interop with native code, but in c# it acts +/// like a 2D string array with fixed max length per string. Strings are ASCII-encoded +/// and padded with nulls if shorter than max length. +/// +[DebuggerDisplay("{ToString()}")] +[DebuggerTypeProxy(typeof(stringArray10xDebugView))] +[StructLayout(LayoutKind.Sequential)] +public unsafe struct stringArray10x10 : IBuffer +{ + public const int ROWS = 10; + public const int COLS = 10; + public const int SIZE = ROWS * COLS; + + private fixed byte data[SIZE]; + + #region Indexers + public ref byte this[int row, int col] + { + get + { + if ((uint)row >= ROWS || (uint)col >= COLS) + throw new IndexOutOfRangeException(); + + return ref data[row * COLS + col]; + } + } + + public string this[int row] + { + get => GetString(row); + set => SetString(row, value); + } + #endregion + + #region String assignment API + public void SetString(int row, string value) + { + if ((uint)row >= ROWS) + throw new ArgumentOutOfRangeException(nameof(row)); + + if (value is null) + throw new ArgumentNullException(nameof(value)); + + ref byte start = ref data[row * COLS]; + var span = MemoryMarshal.CreateSpan(ref start, COLS); + + // encode to ASCII + int written = Encoding.ASCII.GetBytes(value, span); + + if (written < COLS) + span.Slice(written).Clear(); + } + + public string GetString(int row) + { + if ((uint)row >= ROWS) + throw new ArgumentOutOfRangeException(nameof(row)); + + ref byte start = ref data[row * COLS]; + var span = MemoryMarshal.CreateSpan(ref start, COLS); + return Encoding.ASCII.GetString(span).TrimEnd('\0'); + } + #endregion + + #region Spans + public Span AsSpan() => MemoryMarshal.CreateSpan(ref data[0], SIZE); + + public Span RowAsSpan(int row) + { + if ((uint)row >= ROWS) + throw new ArgumentOutOfRangeException(nameof(row)); + + // ref to first byte in the array for the specified row + ref byte start = ref data[row * COLS]; + + return MemoryMarshal.CreateSpan(ref start, COLS); + } + #endregion + + public override string ToString() + => "["+string.Join("],[", Enumerable.Range(0, ROWS).Select(GetString)) +"]"; + +} diff --git a/dotnet/DDS_Core/Helpers/stringArray10xDebugView.cs b/dotnet/DDS_Core/Helpers/stringArray10xDebugView.cs new file mode 100644 index 00000000..3ed078c0 --- /dev/null +++ b/dotnet/DDS_Core/Helpers/stringArray10xDebugView.cs @@ -0,0 +1,28 @@ +using System; + +namespace DDS_Core; + + internal sealed class stringArray10xDebugView where T : IBuffer + { + private readonly T _buffer; + + public stringArray10xDebugView(T buffer) + { + _buffer = buffer; + } + + public string Row0 => _buffer.GetString(0); + public string Row1 => _buffer.GetString(1); + public string Row2 => _buffer.GetString(2); + public string Row3 => _buffer.GetString(3); + + public string Row4 => _buffer.GetString(4); + public string Row5 => _buffer.GetString(5); + + public string Row6 => _buffer.GetString(6); + public string Row7 => _buffer.GetString(7); + + public string Row8 => _buffer.GetString(8); + public string Row9 => _buffer.GetString(9); + } + diff --git a/dotnet/DDS_Core/Helpers/stringArray2x128.cs b/dotnet/DDS_Core/Helpers/stringArray2x128.cs new file mode 100644 index 00000000..979da2f6 --- /dev/null +++ b/dotnet/DDS_Core/Helpers/stringArray2x128.cs @@ -0,0 +1,91 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; + +namespace DDS_Core; + + +/// +/// Byte buffer struct is suitable for interop with native code, but in c# it acts +/// like a 2D string array with fixed max length per string. Strings are ASCII-encoded +/// and padded with nulls if shorter than max length. +/// +[DebuggerDisplay("{ToString()}")] +[DebuggerTypeProxy(typeof(stringArray2xDebugView))] +[StructLayout(LayoutKind.Sequential)] +public unsafe struct stringArray2x128 : IBuffer +{ + public const int ROWS = 2; + public const int COLS = 128; + public const int SIZE = ROWS * COLS; + + private fixed byte data[SIZE]; + + #region Indexers + public ref byte this[int row, int col] + { + get + { + if ((uint)row >= ROWS || (uint)col >= COLS) + throw new IndexOutOfRangeException(); + + return ref data[row * COLS + col]; + } + } + + public string this[int row] + { + get => GetString(row); + set => SetString(row, value); + } + #endregion + + #region String assignment API + public void SetString(int row, string value) + { + if ((uint)row >= ROWS) + throw new ArgumentOutOfRangeException(nameof(row)); + + if (value is null) + throw new ArgumentNullException(nameof(value)); + + ref byte start = ref data[row * COLS]; + var span = MemoryMarshal.CreateSpan(ref start, COLS); + + // encode to ASCII + int written = Encoding.ASCII.GetBytes(value, span); + + if (written < COLS) + span.Slice(written).Clear(); + } + + public string GetString(int row) + { + if ((uint)row >= ROWS) + throw new ArgumentOutOfRangeException(nameof(row)); + + ref byte start = ref data[row * COLS]; + var span = MemoryMarshal.CreateSpan(ref start, COLS); + return Encoding.ASCII.GetString(span).TrimEnd('\0'); + } + #endregion + + #region Spans + public Span AsSpan() => MemoryMarshal.CreateSpan(ref data[0], SIZE); + + public Span RowAsSpan(int row) + { + if ((uint)row >= ROWS) + throw new ArgumentOutOfRangeException(nameof(row)); + + // ref to first byte in the array for the specified row + ref byte start = ref data[row * COLS]; + + return MemoryMarshal.CreateSpan(ref start, COLS); + } + #endregion + + public override string ToString() + => "["+string.Join("],[", Enumerable.Range(0, ROWS).Select(GetString)) +"]"; +} diff --git a/dotnet/DDS_Core/Helpers/stringArray2x16.cs b/dotnet/DDS_Core/Helpers/stringArray2x16.cs new file mode 100644 index 00000000..88eb1a50 --- /dev/null +++ b/dotnet/DDS_Core/Helpers/stringArray2x16.cs @@ -0,0 +1,92 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; + +namespace DDS_Core; + + +/// +/// Byte buffer struct is suitable for interop with native code, but in c# it acts +/// like a 2D string array with fixed max length per string. Strings are ASCII-encoded +/// and padded with nulls if shorter than max length. +/// +[DebuggerDisplay("{ToString()}")] +[DebuggerTypeProxy(typeof(stringArray2xDebugView))] +[StructLayout(LayoutKind.Sequential)] +public unsafe struct stringArray2x16 : IBuffer +{ + public const int ROWS = 2; + public const int COLS = 16; + public const int SIZE = ROWS * COLS; + + private fixed byte data[SIZE]; + + #region Indexers + public ref byte this[int row, int col] + { + get + { + if ((uint)row >= ROWS || (uint)col >= COLS) + throw new IndexOutOfRangeException(); + + return ref data[row * COLS + col]; + } + } + + public string this[int row] + { + get => GetString(row); + set => SetString(row, value); + } + #endregion + + #region String assignment API + public void SetString(int row, string value) + { + if ((uint)row >= ROWS) + throw new ArgumentOutOfRangeException(nameof(row)); + + if (value is null) + throw new ArgumentNullException(nameof(value)); + + ref byte start = ref data[row * COLS]; + var span = MemoryMarshal.CreateSpan(ref start, COLS); + + // encode to ASCII + int written = Encoding.ASCII.GetBytes(value, span); + + if (written < COLS) + span.Slice(written).Clear(); + } + + public string GetString(int row) + { + if ((uint)row >= ROWS) + throw new ArgumentOutOfRangeException(nameof(row)); + + ref byte start = ref data[row * COLS]; + var span = MemoryMarshal.CreateSpan(ref start, COLS); + return Encoding.ASCII.GetString(span).TrimEnd('\0'); + } + #endregion + + #region Spans + public Span AsSpan() => MemoryMarshal.CreateSpan(ref data[0], SIZE); + + public Span RowAsSpan(int row) + { + if ((uint)row >= ROWS) + throw new ArgumentOutOfRangeException(nameof(row)); + + // ref to first byte in the array for the specified row + ref byte start = ref data[row * COLS]; + + return MemoryMarshal.CreateSpan(ref start, COLS); + } + #endregion + + public override string ToString() + => "["+string.Join("],[", Enumerable.Range(0, ROWS).Select(GetString)) +"]"; + +} diff --git a/dotnet/DDS_Core/Helpers/stringArray2xDebugView.cs b/dotnet/DDS_Core/Helpers/stringArray2xDebugView.cs new file mode 100644 index 00000000..d2543245 --- /dev/null +++ b/dotnet/DDS_Core/Helpers/stringArray2xDebugView.cs @@ -0,0 +1,16 @@ +using System; + +namespace DDS_Core; + +internal sealed class stringArray2xDebugView where T : IBuffer +{ + private readonly T _buffer; + + public stringArray2xDebugView(T buffer) + { + _buffer = buffer; + } + + public string Row0 => _buffer.GetString(0); + public string Row1 => _buffer.GetString(1); +} diff --git a/dotnet/DDS_Core/Native/DdsNative.cs b/dotnet/DDS_Core/Native/DdsNative.cs new file mode 100644 index 00000000..8b9b7cf8 --- /dev/null +++ b/dotnet/DDS_Core/Native/DdsNative.cs @@ -0,0 +1,267 @@ +using System.Runtime.InteropServices; +using System.Text; + +namespace DDS_Core.Native; + +internal static class DdsNative +{ + private const string DllName = "dds_native"; + + #region ====== Version 3 specific methods ====== + #region ===== Solver Context Management ====== + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + + internal static extern SolverContextHandle dds_create_solvercontext_default(); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + internal static extern SolverContextHandle dds_create_solvercontext(SolverConfig cfg); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void dds_destroy_solvercontext(IntPtr ctx); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void dds_configure_tt( SolverContextHandle ctx + , TTKind kind + , int defMB + , int maxMB); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void dds_resize_tt( SolverContextHandle ctx + , int defMB + , int maxMB); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void dds_clear_tt(SolverContextHandle ctx); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void dds_reset_for_solve(SolverContextHandle ctx); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void dds_reset_best_moves_lite(SolverContextHandle ctx); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + internal static extern void dds_log_append( SolverContextHandle ctx, string msg); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void dds_log_clear( SolverContextHandle ctx); + #endregion + + #region ====== SolverContext methods ====== + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + public static extern int dds_solve_board( SolverContextHandle ctx + , in Deal dl + , int target + , int solutions + , int mode + , out FutureTricks fut); + + #region Call_dd + // [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + //public static extern int dds_calc_dd_table( in DdTableDeal table_deal + // , out DdTableResults table_results); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + public static extern int dds_calc_dd_table( SolverContextHandle ctx + , in DdTableDeal table_deal + , out DdTableResults table_results); + + // [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + //public static extern int dds_calc_dd_table_pbn( in DdTableDealPBN table_deal_pbn + // , out DdTableResults table_results); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + public static extern int dds_calc_dd_table_pbn( SolverContextHandle ctx + , in DdTableDealPBN table_deal_pbn + , out DdTableResults table_results); + #endregion + + #region Call_par + // [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + //public static extern int calc_par( in DdTableDeal table_deal + // , int vulnerable + // , out DdTableResults table_results + // , out ParResults par_results); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + public static extern int dds_calc_par( SolverContextHandle ctx + , in DdTableDeal table_deal + , int vulnerable + , out DdTableResults table_results + , out ParResults par_results); + + // [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + //public static extern int calc_par_from_table( in DdTableResults table_results + // , int vulnerable + // , out ParResults par_results); + #endregion + #endregion + #endregion + + #region ====== Configuration and Resource Management ====== + // The C symbol SetMaxThreads is deprecated and now a thin alias of + // InitializeStaticMemory; the userThreads argument is ignored. + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + public static extern void SetMaxThreads( int userThreads); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + public static extern int SetThreading(int code); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + public static extern void SetResources( int maxMemoryMB + , int maxThreads); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + public static extern void FreeMemory(); + #endregion + + #region ====== Single Board Solving ====== + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + public static extern int SolveBoard( in Deal dl + , int target + , int solutions + , int mode + , out FutureTricks fut + , int threadIndex); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + public static extern int SolveBoardPBN( in DealPBN dlpbn + , int target + , int solutions + , int mode + , out FutureTricks fut + , int thrId); + #endregion + + #region ====== Multiple Board Solving ====== + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + public static extern int SolveAllBoards( in BoardsPBN bop + , out SolvedBoards solved); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + public static extern int SolveAllBoardsBin( in Boards bop + , out SolvedBoards solved); + + //[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + //public static extern int SolveAllChunks( in BoardsPBN bop + // , out SolvedBoards solved + // , int chunkSize); + + //[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + //public static extern int SolveAllChunksBin( in Boards bop + // , out SolvedBoards solved + // , int chunkSize); + + //[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + //public static extern int SolveAllChunksPBN( in BoardsPBN bop + // , out SolvedBoards solved + // , int chunkSize); + #endregion + + #region ====== Double Dummy Table Calculation ====== + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + public static extern int CalcDDtable( in DdTableDeal tableDeal + , out DdTableResults table); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + public static extern int CalcDDtablePBN( in DdTableDealPBN tableDealPBN + , out DdTableResults table); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + public static extern int CalcAllTables( in DdTableDeals dealsp + , int mode + , intArray5 trumpFilter + , out DdTablesResult resp + , out AllParResults presp); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + public static extern int CalcAllTablesPBN( in DdTableDealsPBN dealsp + , int mode + , in intArray5 trumpFilter + , out DdTablesResult resp + , out AllParResults presp); + #endregion + + #region ====== Par Score Calculation ====== + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + public static extern int Par( in DdTableResults table + , out ParResults pres + , int vulnerable); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + public static extern int CalcPar( in DdTableDeal tableDeal + , int vulnerable + , out DdTableResults table + , out ParResults pres); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + public static extern int CalcParPBN( in DdTableDealPBN tableDealPBN + , out DdTableResults table + , int vulnerable + , out ParResults pres); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + public static extern int SidesPar( in DdTableResults table + , out ParResultsDealers sidesRes + , int vulnerable); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + public static extern int DealerPar( in DdTableResults table + , out ParResultsDealer pres + , int dealer + , int vulnerable); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + public static extern int DealerParBin( in DdTableResults table + , out ParResultsMaster pres + , int dealer + , int vulnerable); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + public static extern int SidesParBin( in DdTableResults table + , out ParResultsMasters sidesRes + , int vulnerable); + #endregion + + #region ====== Par Text Conversion ====== + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + public static extern int ConvertToDealerTextFormat( in ParResultsMaster pres + , StringBuilder resp); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + public static extern int ConvertToSidesTextFormat( in ParResultsMasters pres + , out ParTextResults resp); + #endregion + + #region ====== Play Analysis ====== + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + public static extern int AnalysePlayBin( in Deal dl + , in PlayTraceBin play + , out SolvedPlay solved + , int thrId); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + public static extern int AnalysePlayPBN( in DealPBN dlPBN + , in PlayTracePBN playPBN + , out SolvedPlay solved + , int thrId); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + public static extern int AnalyseAllPlaysBin( in Boards bop + , in PlayTracesBin plp + , out SolvedPlays solved + , int chunkSize); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + public static extern int AnalyseAllPlaysPBN( in BoardsPBN bopPBN + , in PlayTracesPBN plpPBN + , out SolvedPlays solved + , int chunkSize); + #endregion + + #region ====== Utility Functions ====== + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + public static extern void GetDDSInfo(out DdsInfo info); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + public static extern void ErrorMessage( int code + , StringBuilder line); + #endregion +} + diff --git a/dotnet/DDS_Core/Properties/launchSettings.json b/dotnet/DDS_Core/Properties/launchSettings.json new file mode 100644 index 00000000..03f7be23 --- /dev/null +++ b/dotnet/DDS_Core/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "DDS_Core": { + "commandName": "Project", + "debugEngines": "managed-dotnet,native" + } + } +} \ No newline at end of file diff --git a/dotnet/DDS_Core_Demo/DDS_Core_Demo.csproj b/dotnet/DDS_Core_Demo/DDS_Core_Demo.csproj new file mode 100644 index 00000000..ee83a962 --- /dev/null +++ b/dotnet/DDS_Core_Demo/DDS_Core_Demo.csproj @@ -0,0 +1,17 @@ + + + + Exe + net8.0 + enable + enable + x64 + false + ..\..\Build\bin\ + + + + + + + diff --git a/dotnet/DDS_Core_Demo/Program.cs b/dotnet/DDS_Core_Demo/Program.cs new file mode 100644 index 00000000..166091ce --- /dev/null +++ b/dotnet/DDS_Core_Demo/Program.cs @@ -0,0 +1,826 @@ +using System.Diagnostics; +using DDS_Core; + +namespace DDS_Core_Demo; + +internal class Program +{ + private static string[] contracts = ["spades","hearts", "diamonds", "clubs", "no trump" ]; + private static string[] c = ["S","H", "D", "C", "NT" ]; + private static int[,] tricks = new int[5, 4]; + private static bool isPerformanceTest ; + private static int iterations = 100; + + static void Main(string[] args) + { + var dds = new DDS(); + var tst =TestData.deals; // Only to initialize TestData - actually not needed! + + // All samples display the number of tricks for each contract + // and each declarer even though most of the dds methods returns + // the number of tricks for the hand at lead. + // + // PERFORMANCE TEST: Measure AnalyseAllPlaysBin P/Invoke performance + // Run in Release mode for accurate results + isPerformanceTest = args.Length > 0 && args[0] == "benchmark"; + + if (isPerformanceTest) + { + BenchmarkAnalyseAllPlaysBin(dds); + Console.WriteLine($"Press any key to continue..."); + Console.ReadKey(); + return; + } + + int vulnability =2; // 0=none, 1=both, 2=NS, 3=EW + int dealer = 1; // 0=N, 1=E, 2=S, 3=W + + // + doSolveBoard(dds, TestData.deals[0]); + doSolveBoard(dds, TestData.dealsPBN[0]); + + doSolveAllBoards(dds, TestData.boards); + doSolveAllBoards(dds, TestData.boardsPBN); + + // Obsolete methods - not recommended for use as they are not optimized and will be removed in future versions + //doSolveAllChunks(dds, TestData.boards, 10); + //doSolveAllChunks(dds, TestData.boardsPBN, 10); + //doSolveAllChunksPBN(dds, TestData.boardsPBN, 10); + //// + doCalcDdTable(dds, TestData.ddTableDeal); + doCalcDdTable(dds, TestData.ddTableDealPBN); + + doCalcAllTables(dds, TestData.ddTableDeals); + doCalcAllTables(dds, TestData.ddTableDealsPBN); + // + doCalcPar(dds, TestData.ddTableDeal, vulnability); + doCalcPar(dds, TestData.ddTableDealPBN, vulnability); + doPar(dds, TestData.ddTableResults, vulnability); + + doParDealer(dds, TestData.ddTableResults, dealer, vulnability); + doParDealerBothSides(dds, TestData.ddTableResults, dealer, vulnability); + + doParAll(dds, TestData.ddTableResults, vulnability); + doParSide(dds, TestData.ddTableResults, vulnability); + + doConvertToTextFormat(dds, TestData.parResultsMaster); + doConvertToTextFormat(dds, TestData.parResultsMasters); + + doAnalysePlay(dds, TestData.deals[0], TestData.parResultsMasters, TestData.playTraceBin); + doAnalysePlay(dds, TestData.dealsPBN[0], TestData.parResultsMasters, TestData.playTracePBN); + // + doAnalyseAllPlays(dds, TestData.boards, TestData.playTracesBin); + doAnalyseAllPlays(dds, TestData.boardsPBN, TestData.playTracesPBN); + // + doGetDDSInfo(dds); + doErrorMessage(dds, -14); + + #region Version 3.0.0 samples + var cfg = new SolverConfig() + { + TTKind = TTKind.Large + , DefaultMemoryMB = 256 + , MaximumMemoryMB = 1024 + }; + + using (var ctx = new SolverContext(cfg)) + { + ctx?.ConfigureTT(TTKind.Small, 256, 1024); + + doSolveBoardV3(dds, ctx, TestData.deals[0]); + doCalcDdTableV3(dds, ctx, TestData.ddTableDeal); + doCalcDdTableV3(dds, ctx, TestData.ddTableDealPBN); + doCalcParV3(dds, ctx, TestData.ddTableDeal, vulnability); + ctx.LogAppend("Completed V3.0.0 samples"); + //ctx.LogClear(); + } + #endregion + + Console.WriteLine($"Press any key to continue..."); + Console.ReadKey(); + return; + } + + private static void doSolveBoard(DDS dds, Deal deal) + { + Console.WriteLine($"SolveBoard"); + + tricks = new int[5, 4]; + + for (deal.Trump = 0; deal.Trump < 5; deal.Trump++) + for (deal.First = 0; deal.First < 4; deal.First++) + { + var decl =(deal.First + 3) & 3; + var rc = dds.SolveBoard(deal, -1, 1, 0, out FutureTricks fut); + + // record the number of tricks for declarer + tricks[deal.Trump, decl] = 13 - fut.Score[0]; + dds.FreeMemory(); + } + + DisplayTricks(); + } + + private static void doSolveBoardV3(DDS dds, SolverContext ctx, Deal deal) + { + Console.WriteLine($"SolveBoard v3"); + + tricks = new int[5, 4]; + + for (deal.Trump = 0; deal.Trump < 5; deal.Trump++) + for (deal.First = 0; deal.First < 4; deal.First++) + { + var decl =(deal.First + 3) & 3; + var rc = ctx.SolveBoard(deal, -1, 1, 0, out FutureTricks fut); + + // record the number of tricks for declarer + tricks[deal.Trump, decl] = 13 - fut.Score[0]; + ctx.ResetForSolve(); + } + + + DisplayTricks(); + } + + private static void doSolveBoard(DDS dds, DealPBN deal) + { + // SolveBoard: Loop through all possible contracts and first players + Console.WriteLine($"SolveBoard PBN"); + tricks = new int[5, 4]; + + for (deal.Trump = 0; deal.Trump < 5; deal.Trump++) + + for (deal.First = 0; deal.First < 4; deal.First++) + { + var decl =(deal.First + 3) & 3; + var rc = dds.SolveBoard(deal, -1, 1, 0, out FutureTricks fut); + + // record the number of tricks for declarer + tricks[deal.Trump, decl] = 13 - fut.Score[0]; + + dds.FreeMemory(); + } + + DisplayTricks(); + } + + private static void doSolveAllBoards(DDS dds, BoardsPBN boards) + { + Console.WriteLine($"SolveAllBoards PBN"); + tricks = new int[5, 4]; + + for (boards.Deals[0].Trump = 0; boards.Deals[0].Trump < 5; boards.Deals[0].Trump++) + for (boards.Deals[0].First = 0; boards.Deals[0].First < 4; boards.Deals[0].First++) + { + var decl =(boards.Deals[0].First + 3) & 3; + var rc = dds.SolveAllBoards(boards, out SolvedBoards solved); + + // record the number of tricks for declarer + tricks[boards.Deals[0].Trump, decl] = 13 - solved.Tricks[0].Score[0]; + + dds.FreeMemory(); + } + + DisplayTricks(); + } + + private static void doSolveAllBoards(DDS dds, Boards boards) + { + Console.WriteLine($"SolveAllBoards"); + tricks = new int[5, 4]; + + // Here we loop over all trump suits and dealers which is't the normal thing to do! + // This is done as we want to display the full tricks table + for (boards.Deals[0].Trump = 0; boards.Deals[0].Trump < 5; boards.Deals[0].Trump++) + for (boards.Deals[0].First = 0; boards.Deals[0].First < 4; boards.Deals[0].First++) + { + var decl =(boards.Deals[0].First + 3) & 3; + var rc = dds.SolveAllBoards(boards, out SolvedBoards solved); + + // record the number of tricks for declarer + tricks[boards.Deals[0].Trump, decl] = 13 - solved.Tricks[0].Score[0]; + + dds.FreeMemory(); + } + + DisplayTricks(); + } + + //private static void doSolveAllChunks(DDS dds, BoardsPBN boards, int chunkSize) + //{ + // Console.WriteLine($"SolveAllChunks PBN"); + // tricks = new int[5, 4]; + + // for (boards.Deals[0].Trump = 0; boards.Deals[0].Trump < 5; boards.Deals[0].Trump++) + // for (boards.Deals[0].First = 0; boards.Deals[0].First < 4; boards.Deals[0].First++) + // { + // var decl =(boards.Deals[0].First + 3) & 3; + // var rc = dds.SolveAllChunks(boards, out SolvedBoards solved, chunkSize); + + // // record the number of tricks for declarer + // tricks[boards.Deals[0].Trump, decl] = 13 - solved.Tricks[0].Score[0]; + + // dds.FreeMemory(); + // } + + // DisplayTricks(); + //} + + //private static void doSolveAllChunks(DDS dds, Boards boards, int chunkSize) + //{ + // Console.WriteLine($"SolveAllChunks"); + // tricks = new int[5, 4]; + + // for (boards.Deals[0].Trump = 0; boards.Deals[0].Trump < 5; boards.Deals[0].Trump++) + // for (boards.Deals[0].First = 0; boards.Deals[0].First < 4; boards.Deals[0].First++) + // { + // var decl =(boards.Deals[0].First + 3) & 3; + // var rc = dds.SolveAllChunksBin(boards, out SolvedBoards solved, chunkSize); + + // // record the number of tricks for declarer + // tricks[boards.Deals[0].Trump, decl] = 13 - solved.Tricks[0].Score[0]; + + // dds.FreeMemory(); + // } + + // DisplayTricks(); + //} + + //private static void doSolveAllChunksPBN(DDS dds, BoardsPBN boards, int chunkSize) + //{ + // Console.WriteLine($"SolveAllChunks PBN"); + // tricks = new int[5, 4]; + + // for (boards.Deals[0].Trump = 0; boards.Deals[0].Trump < 5; boards.Deals[0].Trump++) + // for (boards.Deals[0].First = 0; boards.Deals[0].First < 4; boards.Deals[0].First++) + // { + // var decl =(boards.Deals[0].First + 3) & 3; + // var rc = dds.SolveAllChunksPBN(boards, out SolvedBoards solved, chunkSize); + + // // record the number of tricks for declarer + // tricks[boards.Deals[0].Trump, decl] = 13 - solved.Tricks[0].Score[0]; + + // dds.FreeMemory(); + // } + + // DisplayTricks(); + //} + private static void doCalcDdTable(DDS dds, DdTableDeal ddTableDeal) + { + Console.WriteLine($"CalcDdTable"); + tricks = new int[5, 4]; + + var rc = dds.CalcDdTable(ddTableDeal, out DdTableResults results); + + for (var trump = 0; trump < 5; trump++) + + for (var first = 0; first < 4; first++) + { + var decl =(first + 3) & 3; + + // record the number of tricks for declarer + tricks[trump, decl] = results.ResultsTable[trump, decl]; + } + + dds.FreeMemory(); + + DisplayTricks(); + + TestData.ddTableResults = results; + } + + private static void doCalcDdTableV3(DDS dds, SolverContext ctx, DdTableDeal ddTableDeal) + { + Console.WriteLine($"CalcDdTable V3"); + tricks = new int[5, 4]; + + var rc = ctx.CalcDdTable(ddTableDeal, out DdTableResults results); + + for (var trump = 0; trump < 5; trump++) + + for (var first = 0; first < 4; first++) + { + var decl =(first + 3) & 3; + + // record the number of tricks for declarer + tricks[trump, decl] = results.ResultsTable[trump, decl]; + } + + ctx.ResetForSolve(); + DisplayTricks(); + } + + private static void doCalcDdTable( + DDS dds, DdTableDealPBN ddTableDeal) + { + Console.WriteLine($"CalcDdTable PBN"); + tricks = new int[5, 4]; + + var rc = dds.CalcDdTable(ddTableDeal, out DdTableResults results); + + for (var trump = 0; trump < 5; trump++) + + for (var first = 0; first < 4; first++) + { + var decl =(first + 3) & 3; + + // record the number of tricks for declarer + tricks[trump, decl] = results.ResultsTable[trump, decl]; + } + + dds.FreeMemory(); + + DisplayTricks(); + + TestData.ddTableResults = results; + } + + private static void doCalcDdTableV3(DDS dds, SolverContext ctx, DdTableDealPBN ddTableDeal) + { + Console.WriteLine($"CalcDdTable PBN V3"); + tricks = new int[5, 4]; + + var rc = ctx.CalcDdTable(ddTableDeal, out DdTableResults results); + + for (var trump = 0; trump < 5; trump++) + + for (var first = 0; first < 4; first++) + { + var decl =(first + 3) & 3; + + // record the number of tricks for declarer + tricks[trump, decl] = results.ResultsTable[trump, decl]; + } + + ctx.ResetForSolve(); + DisplayTricks(); + + TestData.ddTableResults = results; + } + + private static void doCalcAllTables(DDS dds, DdTableDeals ddTableDeals) + { + Console.WriteLine($"CalcAllTables"); + tricks = new int[5, 4]; + intArray5 trumpFilter = new(); + + var rc = dds.CalcAllTables( in ddTableDeals + , 0 + , trumpFilter + , out DdTablesResult results + , out AllParResults presp); + + for (var trump = 0; trump < 5; trump++) + + for (var first = 0; first < 4; first++) + { + var decl =(first + 3) & 3; + + // record the number of tricks for declarer + tricks[trump, decl] = results[0].ResultsTable[trump, decl]; + } + + dds.FreeMemory(); + + DisplayTricks(); + } + + private static void doCalcAllTables(DDS dds, DdTableDealsPBN ddTableDeals) + { + Console.WriteLine($"CalcAllTables PBN"); + tricks = new int[5, 4]; + intArray5 trumpFilter = new(); + + var rc = dds.CalcAllTables( in ddTableDeals + , 0 + , trumpFilter + , out DdTablesResult results + , out AllParResults presp); + + for (var trump = 0; trump < 5; trump++) + + for (var first = 0; first < 4; first++) + { + var decl =(first + 3) & 3; + + // record the number of tricks for declarer + tricks[trump, decl] = results[0].ResultsTable[trump, decl]; + } + + dds.FreeMemory(); + + DisplayTricks(); + } + + private static void doPar(DDS dds, DdTableResults tableResults, int vulnability) + { + Console.WriteLine($"Par"); + tricks = new int[5, 4]; + + var rc = dds.Par( in tableResults + , vulnability , out ParResults results +); + + Console.WriteLine(results.ParContractStrings); + Console.WriteLine(results.ParScores); + + dds.FreeMemory(); + Console.WriteLine(); + } + + private static void doCalcParV3(DDS dds, SolverContext ctx, DdTableDeal ddTableDeal, int vulnability) + { + Console.WriteLine($"CalcPar V3"); + tricks = new int[5, 4]; + + var rc = ctx.CalcPar( in ddTableDeal + , vulnability + , out DdTableResults tResults + , out ParResults results ); + + Console.WriteLine(results.ParContractStrings); + Console.WriteLine(results.ParScores); + + for (var trump = 0; trump < 5; trump++) + for (var first = 0; first < 4; first++) + { + var decl =(first + 3) & 3; + + // record the number of tricks for declarer + tricks[trump, decl] = tResults.ResultsTable[trump, decl]; + } + + ctx.ResetForSolve(); + DisplayTricks(); + } + + private static void doCalcPar(DDS dds, DdTableDeal ddTableDeal, int vulnability) + { + Console.WriteLine($"CalcPar"); + tricks = new int[5, 4]; + + var rc = dds.CalcPar( in ddTableDeal + , vulnability + , out DdTableResults tResults + , out ParResults results ); + + Console.WriteLine(results.ParContractStrings); + Console.WriteLine(results.ParScores); + + for (var trump = 0; trump < 5; trump++) + for (var first = 0; first < 4; first++) + { + var decl =(first + 3) & 3; + + // record the number of tricks for declarer + tricks[trump, decl] = tResults.ResultsTable[trump, decl]; + } + + dds.FreeMemory(); + + DisplayTricks(); + //Console.WriteLine(); + } + + + private static void doCalcPar(DDS dds, DdTableDealPBN ddTableDeal, int vulnability) + { + Console.WriteLine($"CalcPar PBN"); + tricks = new int[5, 4]; + + var rc = dds.CalcPar( in ddTableDeal + , vulnability + , out DdTableResults tResults + , out ParResults results ); + + Console.WriteLine(results.ParContractStrings); + Console.WriteLine(results.ParScores); + + for (var trump = 0; trump < 5; trump++) + for (var first = 0; first < 4; first++) + { + var decl =(first + 3) & 3; + + // record the number of tricks for declarer + tricks[trump, decl] = tResults.ResultsTable[trump, decl]; + } + + dds.FreeMemory(); + + DisplayTricks(); + //Console.WriteLine(); + } + + private static void doParSide(DDS dds, DdTableResults tResults, int vulnability) + { + Console.WriteLine($"ParSide -> ParResultsDealers"); + tricks = new int[5, 4]; + + var rc = dds.ParSide( in tResults + , vulnability , out ParResultsDealers results +); + + Console.WriteLine(results[0].NumberOfContracts); + Console.WriteLine(results[0].Score); + Console.WriteLine(results[0].Contracts); + Console.WriteLine(); + + Console.WriteLine(results[1].NumberOfContracts); + Console.WriteLine(results[1].Score); + Console.WriteLine(results[1].Contracts); + Console.WriteLine(); + + dds.FreeMemory(); + } + + private static void doParAll(DDS dds, DdTableResults tResults, int vulnability) + { + Console.WriteLine($"ParAll -> ParResultsMasters "); + tricks = new int[5, 4]; + + var rc = dds.ParAll( in tResults + , vulnability , out ParResultsMasters results +); + + for (int s = 0; s < 2; s++) + { + Console.WriteLine($"Side {(s == 0 ? "N/S" : "E/W")}"); + Console.WriteLine(results[s].Number); + Console.WriteLine(results[s].Score); + + for (int i = 0; i < results[s].Number; i++) + { + var contract =results[s].Contracts[i]; + var d = c[contract.Denomination]; + + if (contract.UnderTricks > 0) + Console.WriteLine($"{contract.Level}{d}(-{contract.UnderTricks})"); + else + if (contract.OverTricks > 0) + Console.WriteLine($"{contract.Level}{d}(+{contract.OverTricks})"); + else + Console.WriteLine($"{contract.Level}{d}(=)"); + } + + Console.WriteLine(""); + } + + TestData.parResultsMasters = results; + dds.FreeMemory(); + } + + private static void doParDealer(DDS dds, DdTableResults tResults, int dealer, int vulnability) + { + Console.WriteLine($"ParDealer"); + tricks = new int[5, 4]; + + var rc = dds.ParDealer( in tResults + , dealer + , vulnability + , out ParResultsDealer results + ); + + Console.WriteLine(results.NumberOfContracts); + Console.WriteLine(results.Score); + Console.WriteLine(results.Contracts); + Console.WriteLine(""); + + dds.FreeMemory(); + } + + private static void doParDealerBothSides( DDS dds, DdTableResults tResults, int dealer + , int vulnability) + { + Console.WriteLine($"DealerParBothSides"); + tricks = new int[5, 4]; + + var rc = dds.DealerParBothSides( in tResults + , dealer + , vulnability , out ParResultsMaster results +); + + Console.WriteLine(results.Number); + Console.WriteLine(results.Score); + + for (int i = 0; i < results.Number; i++) + Console.WriteLine(results.Contracts[i]); + + Console.WriteLine(""); + TestData.parResultsMaster = results; + dds.FreeMemory(); + } + + private static void doConvertToTextFormat(DDS dds, ParResultsMaster tResults) + { + Console.WriteLine($"ConvertToTextFormat ParResultsMaster"); + + var rc = dds.ConvertToTextFormat( in tResults + , out string str ); + + Console.WriteLine(str); + Console.WriteLine(""); + dds.FreeMemory(); + } + + private static void doConvertToTextFormat(DDS dds, ParResultsMasters tResults) + { + Console.WriteLine($"ConvertToTextFormat ParResultsMasters"); + + var rc = dds.ConvertToTextFormat( in tResults + , out ParTextResults str ); + + Console.WriteLine(str.ParTextStrings); + Console.WriteLine(""); + dds.FreeMemory(); + } + + private static void doAnalysePlay(DDS dds, Deal deal, ParResultsMasters tResults, PlayTraceBin ptrace) + { + Console.WriteLine($"AnalysePlay PlayTracesBin"); + + var rc = dds.AnalysePlay( in deal + , in ptrace + , 0 , out SolvedPlay solved +); + + for (int i = 0; i <= ptrace.NumberOfCards; i++) + Console.WriteLine($"{i,2}: {solved.Tricks[i]}"); + + Console.WriteLine(""); + dds.FreeMemory(); + } + + private static void doAnalysePlay(DDS dds, DealPBN deal, ParResultsMasters tResults, PlayTracePBN ptrace) + { + Console.WriteLine($"AnalysePlay PlayTracePBN"); + + var rc = dds.AnalysePlay( in deal + , in ptrace + , 0 , out SolvedPlay solved +); + + for (int i = 0; i <= ptrace.NumberOfPlayedCards; i++) + Console.WriteLine($"{i,2}: {solved.Tricks[i]}"); + + Console.WriteLine(""); + dds.FreeMemory(); + } + + private static void doAnalyseAllPlays(DDS dds, Boards boards, PlayTracesBin ptrace) + { + if (!isPerformanceTest) + Console.WriteLine($"AnalysePlay PlayTracesBin"); + + var rc = dds.AnalyseAllPlays( in boards + , in ptrace + , 0 , out SolvedPlays solved +); + + if (isPerformanceTest) + { + for (int i = 0; i <= ptrace.Plays[0].NumberOfCards; i++) + Console.WriteLine($"{i,2}: {solved.Solved[0].Tricks[i]}"); + + Console.WriteLine(""); + } + + dds.FreeMemory(); + } + + private static void doAnalyseAllPlays(DDS dds, BoardsPBN boards, PlayTracesPBN ptrace) + { + Console.WriteLine($"AnalyseAllPlay PlayTracesPBN"); + + var rc = dds.AnalyseAllPlays( in boards + , in ptrace + , 0 , out SolvedPlays solved +); + + for (int i = 0; i <= ptrace.Plays[0].NumberOfPlayedCards; i++) + Console.WriteLine($"{i,2}: {solved.Solved[0].Tricks[i]}"); + + Console.WriteLine(""); + dds.FreeMemory(); + } + + private static void doGetDDSInfo(DDS dds) + { + Console.WriteLine($"GetDDSInfo"); + + dds.GetDDSInfo(out DdsInfo info); + + Console.WriteLine($"DDS Info:"); + Console.WriteLine($" Version: {info.VersionString}"); + Console.WriteLine($" System: {info.System}"); + Console.WriteLine($" Bits: {info.NumberOfBits}"); + Console.WriteLine($" Threading: {info.Threading}"); + Console.WriteLine($" Threads: {info.NumberOfThreads}"); + Console.WriteLine(""); + } + + private static void doErrorMessage(DDS dds, int rc) + { + Console.WriteLine($"Error Message"); + + dds.ErrorMessage(rc, out string error); + + Console.WriteLine($"Error: {error}"); + Console.WriteLine(""); + } + + private static void DisplayTricks() + { + Console.WriteLine($" N E S W"); + Console.WriteLine($" __ __ __ __"); + + for (var denonination = 0; denonination < 5; denonination++) + { + Console.Write($"Tricks in {contracts[denonination],-10}: "); + + for (var declarer = 0; declarer < 4; declarer++) + + Console.Write($"{tricks[denonination, declarer],2} "); + + Console.WriteLine(""); + } + + Console.WriteLine(); + } + + private static void BenchmarkAnalyseAllPlaysBin(DDS dds) + { + Console.WriteLine("=== AnalyseAllPlaysBin P/Invoke Performance Benchmark ==="); + Console.WriteLine("(Run this in Release configuration for accurate results)\n"); + + var boards = TestData.boards; + var playTracesBin = TestData.playTracesBin; + try + { + var time1 = 0d; + + // Variant 1: Baseline - Cdecl with 'in' parameters (read-only reference) + time1+= BenchmarkVariant1(dds, boards, playTracesBin); + + Console.WriteLine("\n=== Benchmark Complete ==="); + Console.Out.Flush(); + } + + catch (Exception ex) + { + Console.WriteLine($"\nERROR: {ex.GetType().Name}: {ex.Message}"); + Console.WriteLine($"Stack trace: {ex.StackTrace}"); + Console.Out.Flush(); + } + } + + private static double BenchmarkVariant1(DDS dds, Boards boards, PlayTracesBin playTracesBin) + { + try + { + // Warmup iterations + Console.WriteLine("\n--- Variant 1: in parameters all the way ---"); + Console.WriteLine("Warming up..."); + Console.Out.Flush(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + for (int i = 0; i < 5; i++) + dds.AnalyseAllPlays(in boards, in playTracesBin, 0, out SolvedPlays solved); + + dds.FreeMemory(); + + // Measure iterations + Console.WriteLine($"Running {iterations} iterations..."); + + var sw = Stopwatch.StartNew(); + + for (int i = 0; i < iterations; i++) + { + dds.AnalyseAllPlays(in boards, in playTracesBin, 0, out SolvedPlays solved); + dds.FreeMemory(); + } + + sw.Stop(); + + double avgMs = sw.Elapsed.TotalMilliseconds / iterations; + double opsPerSecond = 1000.0 / avgMs; + + Console.WriteLine($"Total time: {sw.Elapsed.TotalMilliseconds:F2} ms"); + Console.WriteLine($"Average time: {avgMs:F4} ms per call"); + Console.WriteLine($"Throughput: {opsPerSecond:F2} calls/second"); + + return opsPerSecond; + } + catch (Exception ex) + { + Console.WriteLine($"ERROR in Variant1: {ex.Message}"); + + if (ex.InnerException != null) + Console.WriteLine($"Inner: {ex.InnerException.Message}"); + } + + return 0d; + } +} + diff --git a/dotnet/DDS_Core_Demo/Properties/launchSettings.json b/dotnet/DDS_Core_Demo/Properties/launchSettings.json new file mode 100644 index 00000000..1779d31d --- /dev/null +++ b/dotnet/DDS_Core_Demo/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "DDS_Core_Demo": { + "commandName": "Project", + "debugEngines": "managed-dotnet,native" + } + } +} \ No newline at end of file diff --git a/dotnet/DDS_Core_Demo/TestData.cs b/dotnet/DDS_Core_Demo/TestData.cs new file mode 100644 index 00000000..103d4796 --- /dev/null +++ b/dotnet/DDS_Core_Demo/TestData.cs @@ -0,0 +1,303 @@ +using System.Numerics; +using System.Runtime.Intrinsics.X86; +using System.Text; +using DDS_Core; +using static DDS_Core.CardRanks; + +namespace DDS_Core_Demo +{ + public static class TestData + { + public static string[] pbn; + public static uint[][][] hands; + public static Deal[] deals; + public static DealPBN[] dealsPBN; + public static BoardsPBN boardsPBN; + public static Boards boards; + public static DdTableDeal ddTableDeal; + public static DdTableDealPBN ddTableDealPBN; + public static DdTableDeals ddTableDeals; + public static DdTableDealsPBN ddTableDealsPBN; + public static DdTableResults ddTableResults; + public static ParResultsMaster parResultsMaster; + public static ParResultsMasters parResultsMasters; + public static PlayTraceBin playTraceBin; + public static PlayTracePBN playTracePBN; + public static PlayTracesBin playTracesBin; + public static PlayTracesPBN playTracesPBN; + + static TestData() + { + hands = new uint[3][][]; + + hands[0] = [ + [ (uint)(rT|r8|r5) + , (uint)(rA|rT|r7|r2) + , (uint)(rK|rQ|r8) + , (uint)(rA|r3|r2) + ] + , [ (uint)(rJ|r2) + , (uint)(rK|r9) + , (uint)(rA|rJ|r7|r4|r3|r2) + , (uint)(rJ|r8|r7) + ] + , [ (uint)(rK|rQ|r3) + , (uint)(rJ|r8|r6|r5|r4|r3) + , (uint)(r5) + , (uint)(rK|r9|r6) + ] + , [ (uint)(rA|r9|r7|r6|r4) + , (uint)(rQ) + , (uint)(rT|r9|r6) + , (uint)(rQ|rT|r5|r4) + ] + ]; + + hands[1] = [ + [ (uint)(rA|rK|r9|r6) + , (uint)(rK|rQ|r8) + , (uint)(rA|r9|r8) + , (uint)(rK|r6|r3) + ] + + , [ (uint)(rQ|rJ|rT|r5|r4|r3|r2) + , (uint)(rT) + , (uint)(r6) + , (uint)(rQ|rJ|r8|r2) + ] + + , [ (uint)None + , (uint)(rJ|r9|r7|r5|r4|r3) + , (uint)(rK|r7|r5|r3|r2) + , (uint)(r9|r4) + ] + + , [ (uint)(r8|r7) + , (uint)(rA|r6|r2) + , (uint)(rQ|rJ|rT|r4) + , (uint)(rA|rT|r7|r5) + ] + ]; + + hands[2] = [ + + [ (uint)(r7|r3) + , (uint)(rQ|rJ|rT) + , (uint)(rA|rQ|r5|r4) + , (uint)(rT|r7|r5|r2) + ] + , [ (uint)(rQ|rT|r6) + , (uint)(r8|r7|r6) + , (uint)(rK|rJ|r9) + , (uint)(rA|rQ|r8|r4) + ] + , [ (uint)r5 + , (uint)(rA|r9|r5|r4|r3|r2) + , (uint)(r7|r6|r3|r2) + , (uint)(rK|r6) + ] + , [ (uint)(rA|rK|rJ|r9|r8|r4|r2) + , (uint)(rK) + , (uint)(rT|r8) + , (uint)(rJ|r9|r3) + ] + ]; + + // "N:J652.A74..KT9642 AT9873.KJ5.T82.8 4.QT982.AKQ93.AJ KQ.63.J7654.Q753" + pbn = [ "N:T85.AT72.KQ8.A32 J2.K9.AJ7432.J87 KQ3.J86543.5.K96 A9764.Q.T96.QT54" + , "E:QJT5432.T.6.QJ82 .J97543.K7532.94 87.A62.QJT4.AT75 AK96.KQ8.A98.K63" + , "N:73.QJT.AQ54.T752 QT6.876.KJ9.AQ84 5.A95432.7632.K6 AKJ9842.K.T8.J93" + ]; + + deals = new Deal[3]; + + for (int i = 0; i < deals.Length; i++) + deals[i] = new Deal + { + Trump = (int)Suit.Hearts + , First = 0 + , CurrentTrickSuit = new int[3] {0, 0, 0 } + , CurrentTrickRank = new int[3] {0, 0, 0 } + , RemainingCards = TestData.hands[i] + }; + + // dealPBN + dealsPBN = new DealPBN[3]; + + for (int i = 0; i < dealsPBN.Length; i++) + dealsPBN[i] = new DealPBN + { + Trump = (int)Suit.Hearts + , First = 0 + , CurrentTrickSuit = new int[3] {0, 0, 0 } + , CurrentTrickRank = new int[3] {0, 0, 0 } + , RemainingCards = TestData.pbn[i] + }; + + // boards and boardsPBN + boards = new Boards { NumberOfBoards = 3 }; + boardsPBN = new BoardsPBN {NumberOfBoards = 3 }; + + for (int i = 0; i < boardsPBN.NumberOfBoards; i++) + { + boards.Deals[i] = deals[i]; + boards.Target[i] = -1; + boards.Solutions[i] = 1; + boards.Modes[i] = 0; + + boardsPBN.Deals[i] = dealsPBN[i]; + boardsPBN.Target[i] = -1; + boardsPBN.Solutions[i] = 1; + boardsPBN.Modes[i] = 0; + } + + // ddTableDeal(PBN) + ddTableDeal = new() { Cards = hands[0] }; + ddTableDealPBN = new() {Cards = pbn[0] }; + + // ddTableDeals + ddTableDeals = new DdTableDeals + { + NumberOfTables = 3 + , Deals = new DdTableDeal[200] + }; + + for (int i = 0; i < ddTableDeals.NumberOfTables; i++) + ddTableDeals.Deals[i] = new DdTableDeal { Cards = hands[i] }; + + // ddTableDealsPBN + ddTableDealsPBN = new DdTableDealsPBN + { + NumberOfTables = 3 + , Deals = new DdTableDealPBN[200] + }; + + for (int i = 0; i < ddTableDealsPBN.NumberOfTables; i++) + ddTableDealsPBN.Deals[i] = new DdTableDealPBN { Cards = pbn[i] }; + + // playTraceBin and PBN + playTraceBin = new(); + playTraceBin.NumberOfCards = 3; + playTraceBin.Suits[0] = 3; + playTraceBin.Suits[1] = 3; + playTraceBin.Suits[2] = 3; + + playTraceBin.Ranks[0] = 2; + playTraceBin.Ranks[1] = 7; + playTraceBin.Ranks[2] = 6; + + var playTraceBin1 = new PlayTraceBin(); + playTraceBin1.NumberOfCards = 1; + playTraceBin1.Suits[0] = 0; + playTraceBin1.Ranks[0] = 14; + + var playTraceBin2 = new PlayTraceBin(); + playTraceBin2.NumberOfCards = 4; + playTraceBin2.Suits[0] = 1; + playTraceBin2.Suits[1] = 1; + playTraceBin2.Suits[2] = 1; + playTraceBin2.Suits[3] = 1; + + playTraceBin2.Ranks[0] = 12; + playTraceBin2.Ranks[1] = 8; + playTraceBin2.Ranks[2] = 2; + playTraceBin2.Ranks[3] = 13; + + playTracePBN = new PlayTracePBN { NumberOfPlayedCards = 3, Cards = "C2C7C6" }; + var playTracePBN2 = new PlayTracePBN {NumberOfPlayedCards = 1, Cards = "SA" }; + var playTracePBN3 = new PlayTracePBN {NumberOfPlayedCards = 4, Cards = "HQH8H2HK" }; + + // playTracesBin and PBN + playTracesBin = new(); + playTracesBin.NumberOfBoards = 3; + playTracesBin.Plays[0] = playTraceBin; + playTracesBin.Plays[1] = playTraceBin1; + playTracesBin.Plays[2] = playTraceBin2; + + playTracesPBN = new(); + + playTracesPBN.NumberOfBoards = 3; + playTracesPBN.Plays[0] = playTracePBN; + playTracesPBN.Plays[1] = playTracePBN2; + playTracesPBN.Plays[2] = playTracePBN3; + + validate(); + } + + private static void validate() + { + // Validation + Console.WriteLine("Validating TestData..."); + var err =false; + + for (int i = 0; i < hands.Count(); i++) + { + //var arr = pbn[i][2..].Split(' ','.'); + for (int s = 0; s < 4; s++) + { + uint ranks =0; + string str = ""; + + for (int p = 0; p < 4; p++) + { + var r = hands[i][p][s]>>2; + var d = ranks & r; + + if (d == 0) + { + ranks |= r; + //Console.WriteLine($"TestData: valid rank(s) in hands[{i}] suit[{s}] player: {p} rank(s):{Convert.ToString(r , 2).PadLeft(13,'0')}"); + } + else + { + err = true; + Console.WriteLine($"TestData: valid rank(s) in prior hands - rank(s):{Convert.ToString(ranks, 2).PadLeft(13, '0')}"); + Console.WriteLine($"TestData: Duplicate rank(s) in hands[{i}] suit[{s}] player: {p} rank(s):{Convert.ToString(d, 2).PadLeft(13, '0')}"); + continue; + } + } + } + } + + for (int i = 0; i < hands.Count(); i++) + for (int p = 0; p < 4; p++) + { + var cnt = BitOperations.PopCount(hands[i][p][0]) + + BitOperations.PopCount(hands[i][p][1]) + + BitOperations.PopCount(hands[i][p][2]) + + BitOperations.PopCount(hands[i][p][3]); + + //int cnt = 0; + if (cnt > 13 || cnt < 13) + { + err = true; + Console.WriteLine($"TestData: hands[{i}] player:{p} :contains {cnt} cards"); + continue; + } + } + + for (int i = 0; i < hands.Count(); i++) + for (int s = 0; s < 4; s++) + { + var cnt = BitOperations.PopCount(hands[i][0][s]) + + BitOperations.PopCount(hands[i][1][s]) + + BitOperations.PopCount(hands[i][2][s]) + + BitOperations.PopCount(hands[i][3][s]); + + if (cnt > 13 || cnt < 13) + { + err = true; + Console.WriteLine($"TestData: hands[{i}] suit:{s} contains {cnt} cards"); + continue; + } + } + + if (err) + { + Console.WriteLine("Validation failed."); + Console.ReadKey(); + Environment.Exit(0); + } + } + } +} diff --git a/dotnet/Directory.Build.props b/dotnet/Directory.Build.props new file mode 100644 index 00000000..eaaf5149 --- /dev/null +++ b/dotnet/Directory.Build.props @@ -0,0 +1,21 @@ + + + latest + 4 + ..\..\Build\bin\$(platform)\ + ..\..\Build\int\$(platform)\$(MSBuildProjectName)\ + + + + $(DefineConstants);DEBUG; + false + full + + + + + $(DefineConstants); RELEASE; + true + none + + \ No newline at end of file diff --git a/examples/BUILD.bazel b/examples/BUILD.bazel index 8b0b67d3..e425c145 100644 --- a/examples/BUILD.bazel +++ b/examples/BUILD.bazel @@ -147,6 +147,19 @@ cc_binary( ], ) +cc_binary( + name = "dd_table_for_deal", + srcs = ["dd_table_for_deal.cpp"], + copts = EXAMPLES_CPPOPTS, + linkopts = DDS_LINKOPTS, + local_defines = EXAMPLES_LOCAL_DEFINES, + deps = [ + ":hands", + "//library/src:dds", + "//library/src/api:api_definitions", + ], +) + cc_binary( name = "dealer_par", srcs = ["dealer_par.cpp"], @@ -251,6 +264,7 @@ filegroup( ":calc_all_tables_pbn", ":calc_dd_table", ":calc_dd_table_pbn", + ":dd_table_for_deal", ":calc_par_context_example", ":dealer_par", ":par", diff --git a/examples/README b/examples/README index 9541664d..c1c472bb 100644 --- a/examples/README +++ b/examples/README @@ -11,10 +11,14 @@ Build a specific example: Run an example: bazel run //examples:calc_dd_table +Python `dd_table_for_deal` (see `python/examples/`): + bazel run //python/examples:dd_table_for_deal -- hands/example.pbn + Available examples: - analyse_all_plays_bin, analyse_all_plays_pbn - analyse_play_bin, analyse_play_pbn - calc_all_tables, calc_all_tables_pbn - calc_dd_table, calc_dd_table_pbn +- dd_table_for_deal (C++; Python: //python/examples:dd_table_for_deal) - dealer_par, par - solve_all_boards, solve_board, solve_board_pbn diff --git a/examples/analyse_all_plays_bin.cpp b/examples/analyse_all_plays_bin.cpp index f3013714..4593ee24 100644 --- a/examples/analyse_all_plays_bin.cpp +++ b/examples/analyse_all_plays_bin.cpp @@ -30,10 +30,6 @@ auto main() -> int char line[80]; bool match; -#if defined(__linux) || defined(__APPLE__) - SetMaxThreads(0); -#endif - bo.no_of_boards = 3; DDplays.no_of_boards = 3; diff --git a/examples/analyse_all_plays_pbn.cpp b/examples/analyse_all_plays_pbn.cpp index b293387b..d13884a4 100644 --- a/examples/analyse_all_plays_pbn.cpp +++ b/examples/analyse_all_plays_pbn.cpp @@ -30,10 +30,6 @@ auto main() -> int char line[80]; bool match; -#if defined(__linux) || defined(__APPLE__) - SetMaxThreads(0); -#endif - bo.no_of_boards = 3; DDplays.no_of_boards = 3; diff --git a/examples/analyse_play_bin.cpp b/examples/analyse_play_bin.cpp index 5b97cd68..985b4438 100644 --- a/examples/analyse_play_bin.cpp +++ b/examples/analyse_play_bin.cpp @@ -30,10 +30,6 @@ auto main() -> int char line[80]; bool match; -#if defined(__linux) || defined(__APPLE__) || defined(__WASM__) - SetMaxThreads(0); -#endif - for (int handno = 0; handno < 3; handno++) { dl.trump = trump_suit_[handno]; @@ -62,12 +58,7 @@ auto main() -> int if (res != RETURN_NO_FAULT) { - #ifdef __WASM__ - // For WASM, we can't use ErrorMessage, so we'll just print the error code - snprintf(line, sizeof(line), "error code %d", res); - #else ErrorMessage(res, line); - #endif printf("DDS error: %s\n", line); } diff --git a/examples/analyse_play_pbn.cpp b/examples/analyse_play_pbn.cpp index 5a94ae92..35197edd 100644 --- a/examples/analyse_play_pbn.cpp +++ b/examples/analyse_play_pbn.cpp @@ -28,10 +28,6 @@ auto main() -> int char line[80]; bool match; -#if defined(__linux) || defined(__APPLE__) - SetMaxThreads(0); -#endif - for (int handno = 0; handno < 3; handno++) { dlPBN.trump = trump_suit_[handno]; diff --git a/examples/calc_all_tables.cpp b/examples/calc_all_tables.cpp index 260ff452..b7b7f7c5 100644 --- a/examples/calc_all_tables.cpp +++ b/examples/calc_all_tables.cpp @@ -30,10 +30,6 @@ auto main() -> int char line[80]; bool match; -#if defined(__linux) || defined(__APPLE__) - SetMaxThreads(0); -#endif - DDdeals.no_of_tables = 3; for (int handno = 0; handno < 3; handno++) diff --git a/examples/calc_all_tables_pbn.cpp b/examples/calc_all_tables_pbn.cpp index 408a52ff..c304d976 100644 --- a/examples/calc_all_tables_pbn.cpp +++ b/examples/calc_all_tables_pbn.cpp @@ -30,10 +30,6 @@ auto main() -> int char line[80]; bool match; -#if defined(__linux) || defined(__APPLE__) - SetMaxThreads(0); -#endif - DDdealsPBN.no_of_tables = 3; for (int handno = 0; handno < 3; handno++) diff --git a/examples/calc_dd_table.cpp b/examples/calc_dd_table.cpp index 93825c29..0c1a53c8 100644 --- a/examples/calc_dd_table.cpp +++ b/examples/calc_dd_table.cpp @@ -27,10 +27,6 @@ auto main() -> int char line[80]; bool match; -#if defined(__linux) || defined(__APPLE__) - SetMaxThreads(0); -#endif - for (int handno = 0; handno < 3; handno++) { diff --git a/examples/calc_dd_table_pbn.cpp b/examples/calc_dd_table_pbn.cpp index 7ca7fba7..a3b4ceab 100644 --- a/examples/calc_dd_table_pbn.cpp +++ b/examples/calc_dd_table_pbn.cpp @@ -27,10 +27,6 @@ auto main() -> int char line[80]; bool match; -#if defined(__linux) || defined(__APPLE__) || defined(__WASM__) - SetMaxThreads(0); -#endif - for (int handno = 0; handno < 3; handno++) { strcpy(tableDealPBN.cards, pbn_hands_[handno]); diff --git a/examples/calc_par_context_example.cpp b/examples/calc_par_context_example.cpp index 3605d93d..ffd0d22a 100644 --- a/examples/calc_par_context_example.cpp +++ b/examples/calc_par_context_example.cpp @@ -170,10 +170,6 @@ auto main() -> int printf("DDS Examples: Par Calculation with SolverContext\n"); printf("================================================\n"); -#if defined(__linux) || defined(__APPLE__) - SetMaxThreads(0); -#endif - // Run examples example_without_context(); example_with_context(); diff --git a/examples/dd_table_for_deal.cpp b/examples/dd_table_for_deal.cpp new file mode 100644 index 00000000..48814f1a --- /dev/null +++ b/examples/dd_table_for_deal.cpp @@ -0,0 +1,242 @@ +/* + DDS, a bridge double dummy solver. + + Copyright (C) 2006-2014 by Bo Haglund / + 2014-2016 by Bo Haglund & Soren Hein. + + See LICENSE and README. +*/ + + +// Print the double-dummy table for a deal from the command line or a PBN file. + +// Coded by Cursor, based on calc_dd_table.cpp + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if defined(_WIN32) +#include +#else +#include +#endif +#include +#include "hands.hpp" + + +namespace { + +constexpr std::size_t PBN_FILE_MAX = 8192; +constexpr std::size_t PBN_DEAL_MAX = sizeof(DdTableDealPBN::cards); + +const std::regex DEAL_TAG_RE{ + R"re(\[Deal\s*"([^"]*)")re", + std::regex::icase}; + + +static auto stdin_is_tty() -> bool +{ +#if defined(_WIN32) + return _isatty(_fileno(stdin)) != 0; +#else + return isatty(STDIN_FILENO) != 0; +#endif +} + + +auto read_pbn_stream(std::istream& in) -> std::optional +{ + std::string text(PBN_FILE_MAX - 1, '\0'); + in.read(text.data(), static_cast(PBN_FILE_MAX - 1)); + const auto n = in.gcount(); + if (n <= 0) + { + return std::nullopt; + } + + text.resize(static_cast(n)); + return text; +} + + +auto read_pbn_file(const std::filesystem::path& path) -> std::optional +{ + std::ifstream file(path, std::ios::binary); + if (!file) + { + return std::nullopt; + } + + return read_pbn_stream(file); +} + + +auto read_pbn_file_workspace_relative(std::string_view path) + -> std::optional +{ + if (auto text = read_pbn_file(std::filesystem::path(path))) + { + return text; + } + + // bazel run uses a runfiles cwd; BUILD_WORKSPACE_DIRECTORY is the repo root. + if (const char* workspace = std::getenv("BUILD_WORKSPACE_DIRECTORY")) + { + return read_pbn_file(std::filesystem::path(workspace) / path); + } + + return std::nullopt; +} + + +auto extract_deal_tag(std::string_view text) -> std::optional +{ + std::match_results match; + if (std::regex_search(text.cbegin(), text.cend(), match, DEAL_TAG_RE) + && match.size() > 1) + { + return std::string(match[1].first, match[1].second); + } + + return std::nullopt; +} + + +auto load_deal(std::string_view arg) -> std::optional +{ + if (arg == "-") + { + const auto text = read_pbn_stream(std::cin); + if (!text) + { + std::cerr << "No PBN input on stdin\n"; + return std::nullopt; + } + + const auto deal = extract_deal_tag(*text); + if (!deal) + { + std::cerr << "No [Deal \"...\"] tag found in stdin\n"; + return std::nullopt; + } + + return deal; + } + + if (const auto text = read_pbn_file_workspace_relative(arg)) + { + const auto deal = extract_deal_tag(*text); + if (!deal) + { + std::cerr << "No [Deal \"...\"] tag found in " << arg << "\n"; + return std::nullopt; + } + + return deal; + } + + if (arg.size() >= PBN_DEAL_MAX) + { + std::cerr << "PBN deal too long (max " << (PBN_DEAL_MAX - 1) + << " characters)\n"; + return std::nullopt; + } + + return std::string(arg); +} + +} // namespace + + +static auto print_usage(const char * prog) -> void +{ + fprintf(stderr, + "Usage: %s \n" + " %s -h | --help\n" + "\n" + "Calculate double-dummy tricks for all strains and leads.\n" + "\n" + "Arguments:\n" + " DDS PBN deal string, or path to a .pbn file\n" + "\n" + "If stdin is not a terminal, PBN is read from stdin (uses [Deal \"...\"]).\n" + "\n" + "Examples:\n" + " %s \"N:73.QJT.AQ54.T752 QT6.876.KJ9.AQ84 " + "5.A95432.7632.K6 AKJ9842.K.T8.J93\"\n" + " %s hands/example.pbn\n" + " %s < hands/example.pbn\n", + prog, + prog, + prog, + prog, + prog); +} + + +auto main(int argc, char * argv[]) -> int +{ + const char * input; + + if (argc == 2) + { + if (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0) + { + print_usage(argv[0]); + return 0; + } + input = argv[1]; + } + else if (argc == 1 && !stdin_is_tty()) + { + input = "-"; + } + else + { + print_usage(argv[0]); + return 1; + } + + const auto deal = load_deal(input); + if (!deal) + { + return 1; + } + + DdTableDealPBN tableDealPBN{}; + if (deal->size() >= sizeof(tableDealPBN.cards)) + { + fprintf(stderr, + "PBN deal too long (max %zu characters)\n", + sizeof(tableDealPBN.cards) - 1); + return 1; + } + + std::copy_n(deal->begin(), deal->size(), tableDealPBN.cards); + tableDealPBN.cards[deal->size()] = '\0'; + + DdTableResults table; + char line[80]; + + const int res = CalcDDtablePBN(tableDealPBN, &table); + if (res != RETURN_NO_FAULT) + { + ErrorMessage(res, line); + fprintf(stderr, "DDS error: %s\n", line); + return 1; + } + + sprintf(line, "dd_table_for_deal:\n"); + print_pbn_hand(line, tableDealPBN.cards); + print_table(&table); + + return 0; +} diff --git a/examples/dealer_par.cpp b/examples/dealer_par.cpp index 789cf87e..72a408a7 100644 --- a/examples/dealer_par.cpp +++ b/examples/dealer_par.cpp @@ -27,10 +27,6 @@ auto main() -> int char line[80]; bool match; -#if defined(__linux) || defined(__APPLE__) - SetMaxThreads(0); -#endif - for (int handno = 0; handno < 3; handno++) { set_table(&DDtable, handno); diff --git a/examples/migration_example.cpp b/examples/migration_example.cpp index e9e40088..7114c1c2 100644 --- a/examples/migration_example.cpp +++ b/examples/migration_example.cpp @@ -17,7 +17,7 @@ void solve_legacy(const Deal& deal) { - SetMaxThreads(4); + InitializeStaticMemory(); SetResources(2000, 4); FutureTricks fut; diff --git a/examples/par.cpp b/examples/par.cpp index cff51da8..eaf65f32 100644 --- a/examples/par.cpp +++ b/examples/par.cpp @@ -27,10 +27,6 @@ auto main() -> int char line[80]; bool match; -#if defined(__linux) || defined(__APPLE__) - SetMaxThreads(0); -#endif - for (int handno = 0; handno < 3; handno++) { set_table(&DDtable, handno); diff --git a/examples/solve_all_boards.cpp b/examples/solve_all_boards.cpp index 6c987ac3..5bd0b7fb 100644 --- a/examples/solve_all_boards.cpp +++ b/examples/solve_all_boards.cpp @@ -27,10 +27,6 @@ auto main() -> int char line[80]; bool match; -#if defined(__linux) || defined(__APPLE__) - SetMaxThreads(0); -#endif - bo.no_of_boards = 3; for (int handno = 0; handno < 3; handno++) { diff --git a/examples/solve_board.cpp b/examples/solve_board.cpp index b42bffb2..196cb3c3 100644 --- a/examples/solve_board.cpp +++ b/examples/solve_board.cpp @@ -33,10 +33,6 @@ auto main() -> int bool match2; bool match3; -#if defined(__linux) || defined(__APPLE__) || defined(__WASM__) - SetMaxThreads(0); -#endif - for (int handno = 0; handno < 3; handno++) { dl.trump = trump_suit_[handno]; diff --git a/examples/solve_board_pbn.cpp b/examples/solve_board_pbn.cpp index a24f5c7e..b42b432c 100644 --- a/examples/solve_board_pbn.cpp +++ b/examples/solve_board_pbn.cpp @@ -32,10 +32,6 @@ auto main() -> int bool match2, match3; -#if defined(__linux) || defined(__APPLE__) || defined(__WASM__) - SetMaxThreads(0); -#endif - for (int handno = 0; handno < 3; handno++) { dlPBN.trump = trump_suit_[handno]; diff --git a/examples/wasm/calc_dd_table_pbn_test.cpp b/examples/wasm/calc_dd_table_pbn_test.cpp index 01a900c9..0dfdc6e9 100644 --- a/examples/wasm/calc_dd_table_pbn_test.cpp +++ b/examples/wasm/calc_dd_table_pbn_test.cpp @@ -13,9 +13,6 @@ #include "hands.hpp" TEST(CalcDdTablePbnWasmTest, MatchesReferenceTables) { -#if defined(__linux) || defined(__APPLE__) || defined(__WASM__) - SetMaxThreads(0); -#endif for (int handno = 0; handno < 3; ++handno) { DdTableDealPBN deal{}; diff --git a/hands/12_cards.pbn b/hands/12_cards.pbn new file mode 100644 index 00000000..07f4d8d0 --- /dev/null +++ b/hands/12_cards.pbn @@ -0,0 +1 @@ +[Deal "N:KQJT98765432... .KQJT98765432.. ..KQJT98765432. ...KQJT98765432"] diff --git a/hands/all_solid.pbn b/hands/all_solid.pbn new file mode 100644 index 00000000..a70fc311 --- /dev/null +++ b/hands/all_solid.pbn @@ -0,0 +1 @@ +[Deal "N:AKQJT98765432... .AKQJT98765432.. ..AKQJT98765432. ...AKQJT98765432"] diff --git a/hands/everyone_makes_3N.pbn b/hands/everyone_makes_3N.pbn new file mode 100644 index 00000000..91aa97eb --- /dev/null +++ b/hands/everyone_makes_3N.pbn @@ -0,0 +1,2 @@ +{Every direction can make 3N - a deal constructed by Richard Pavlicek} +[Deal "N:QT9.A8765432.KJ. KJ..A8765432.QT9 A8765432.QT9..KJ .KJ.QT9.A8765432"] diff --git a/hands/example.pbn b/hands/example.pbn new file mode 100644 index 00000000..ae636b3c --- /dev/null +++ b/hands/example.pbn @@ -0,0 +1 @@ +[Deal "N:73.QJT.AQ54.T752 QT6.876.KJ9.AQ84 5.A95432.7632.K6 AKJ9842.K.T8.J93"] diff --git a/hands/thomas1.pbn b/hands/thomas1.pbn new file mode 100644 index 00000000..0b7db8df --- /dev/null +++ b/hands/thomas1.pbn @@ -0,0 +1,2 @@ +{Thomas 1 - time consuming to solve} +[Deal "S:Q853.AJ962.KT74. AJ962.KT74..Q853 KT74..Q853.AJ962 .Q853.AJ962.KT74"] diff --git a/hands/thomas2.pbn b/hands/thomas2.pbn new file mode 100644 index 00000000..3c7f5f66 --- /dev/null +++ b/hands/thomas2.pbn @@ -0,0 +1,2 @@ +{Thomas 2 - time consuming to solve} +[Deal "S:AQT8642.KJ9753.. KJ9753..AQT8642. ..KJ9753.AQT8642 .AQT8642..KJ9753"] diff --git a/library/src/ab_search.cpp b/library/src/ab_search.cpp index cb4d65bc..c5b8ee96 100644 --- a/library/src/ab_search.cpp +++ b/library/src/ab_search.cpp @@ -90,7 +90,7 @@ bool ab_search( the value of the subtree is returned. This is a specialized AB function for hand_rel_first == 0. */ - auto thrp = ctx.thread(); + ThreadData* thrp = ctx.thread_ptr(); int hand = posPoint->first[depth]; int tricks = depth >> 2; bool success = (ctx.search().node_type_store(hand) == MAXNODE ? true : false); @@ -192,7 +192,7 @@ static bool ab_search_0_ctx( the value of the subtree is returned. This is a specialized AB function for hand_rel_first == 0. */ - auto thrp = ctx.thread(); + ThreadData* thrp = ctx.thread_ptr(); int trump = thrp->trump; int hand = posPoint->first[depth]; int tricks = depth >> 2; @@ -225,7 +225,7 @@ static bool ab_search_0_ctx( { #ifdef DDS_AB_HITS DumpRetrieved(thrp->fileRetrieved.GetStream(), - * posPoint, cardsP, target, depth); + * posPoint, *cardsP, target, depth); #endif for (int ss = 0; ss < DDS_SUITS; ss++) @@ -493,7 +493,7 @@ static bool ab_search_1_ctx( const int depth, SolverContext& ctx) { - auto thrp = ctx.thread(); + ThreadData* thrp = ctx.thread_ptr(); int trump = thrp->trump; int hand = HAND_ID(posPoint->first[depth], 1); bool success = (ctx.search().node_type_store(hand) == MAXNODE ? true : false); @@ -589,7 +589,9 @@ static bool ab_search_2_ctx( const int depth, SolverContext& ctx) { - auto thrp = ctx.thread(); +#ifdef DDS_AB_STATS + ThreadData* thrp = ctx.thread_ptr(); +#endif int hand = HAND_ID(posPoint->first[depth], 2); bool success = (ctx.search().node_type_store(hand) == MAXNODE ? true : false); bool value = ! success; @@ -680,7 +682,9 @@ static bool ab_search_3_ctx( unsigned short int makeWinRank[DDS_SUITS]; - auto thrp = ctx.thread(); +#ifdef DDS_AB_STATS + ThreadData* thrp = ctx.thread_ptr(); +#endif int hand = HAND_ID(posPoint->first[depth], 3); bool success = (ctx.search().node_type_store(hand) == MAXNODE ? true : false); bool value = ! success; @@ -826,7 +830,7 @@ void make_3( MoveType const * mply, SolverContext& ctx) { - auto thrp = ctx.thread(); + ThreadData* thrp = ctx.thread_ptr(); int firstHand = posPoint->first[depth]; const TrickDataType& data = ctx.move_gen().get_trick_data((depth + 3) >> 2); @@ -874,10 +878,10 @@ void make_3( int aggr = posPoint->aggr[st]; - posPoint->winner[st].rank = static_cast(thrp->rel[aggr].abs_rank[1][st].rank); - posPoint->winner[st].hand = static_cast(thrp->rel[aggr].abs_rank[1][st].hand); - posPoint->second_best[st].rank = static_cast(thrp->rel[aggr].abs_rank[2][st].rank); - posPoint->second_best[st].hand = static_cast(thrp->rel[aggr].abs_rank[2][st].hand); + posPoint->winner[st].rank = thrp->rel[aggr].abs_rank[1][st].rank; + posPoint->winner[st].hand = thrp->rel[aggr].abs_rank[1][st].hand; + posPoint->second_best[st].rank = thrp->rel[aggr].abs_rank[2][st].rank; + posPoint->second_best[st].hand = thrp->rel[aggr].abs_rank[2][st].hand; } } @@ -892,7 +896,7 @@ static void make_3_ctx( MoveType const * mply, SolverContext& ctx) { - auto thrp = ctx.thread(); + ThreadData* thrp = ctx.thread_ptr(); int firstHand = posPoint->first[depth]; const TrickDataType& data = ctx.move_gen().get_trick_data((depth + 3) >> 2); @@ -940,10 +944,10 @@ static void make_3_ctx( int aggr = posPoint->aggr[st]; - posPoint->winner[st].rank = static_cast(thrp->rel[aggr].abs_rank[1][st].rank); - posPoint->winner[st].hand = static_cast(thrp->rel[aggr].abs_rank[1][st].hand); - posPoint->second_best[st].rank = static_cast(thrp->rel[aggr].abs_rank[2][st].rank); - posPoint->second_best[st].hand = static_cast(thrp->rel[aggr].abs_rank[2][st].hand); + posPoint->winner[st].rank = thrp->rel[aggr].abs_rank[1][st].rank; + posPoint->winner[st].hand = thrp->rel[aggr].abs_rank[1][st].hand; + posPoint->second_best[st].rank = thrp->rel[aggr].abs_rank[2][st].rank; + posPoint->second_best[st].hand = thrp->rel[aggr].abs_rank[2][st].hand; } } @@ -1111,7 +1115,6 @@ EvalType evaluate_with_context( const int trump, SolverContext& ctx) { - auto thrp = ctx.thread(); int s, h, hmax = 0, count = 0, k = 0; unsigned short rmax = 0; EvalType eval; diff --git a/library/src/api/BUILD.bazel b/library/src/api/BUILD.bazel index 268a0b74..b3aacc6b 100644 --- a/library/src/api/BUILD.bazel +++ b/library/src/api/BUILD.bazel @@ -8,6 +8,7 @@ cc_library( "calc_par.hpp", "dds.h", "dll.h", + "dds_api.hpp", "portab.h", "PBN.h", # Case-sensitive on consumers; cannot add pbn.h on macOS CI "solve_board.hpp", diff --git a/library/src/api/calc_dd_table.hpp b/library/src/api/calc_dd_table.hpp index 9782f259..51e1cb73 100644 --- a/library/src/api/calc_dd_table.hpp +++ b/library/src/api/calc_dd_table.hpp @@ -80,7 +80,7 @@ auto calc_dd_table_pbn( * @param table_results Output: double dummy table results * @return Error code (RETURN_NO_FAULT on success, RETURN_PBN_FAULT on parse error) */ -auto calc_dd_table_pbn( + auto calc_dd_table_pbn( SolverContext& ctx, const DdTableDealPBN& table_deal_pbn, DdTableResults* table_results) -> int; diff --git a/library/src/api/calc_par.hpp b/library/src/api/calc_par.hpp index faeca2c9..5dc3ebb5 100644 --- a/library/src/api/calc_par.hpp +++ b/library/src/api/calc_par.hpp @@ -59,7 +59,7 @@ auto calc_par( * @param par_results Output: par score and contract strings * @return Error code (RETURN_NO_FAULT on success) */ -auto calc_par( + auto calc_par( SolverContext& ctx, const DdTableDeal& table_deal, int vulnerable, diff --git a/library/src/api/dds_api.hpp b/library/src/api/dds_api.hpp new file mode 100644 index 00000000..92c6589f --- /dev/null +++ b/library/src/api/dds_api.hpp @@ -0,0 +1,68 @@ +// File: dds_api.hpp +#pragma once +#include +#include +#include + +extern "C" { + + // Opaque handle type for C#/PInvoke + typedef SolverContext* DDS_SOLVER_CTX; + + // Creation + EXTERN_C DLLEXPORT DDS_SOLVER_CTX dds_create_solvercontext_default(); + + EXTERN_C DLLEXPORT DDS_SOLVER_CTX dds_create_solvercontext(SolverConfig cfg); + + // SolverContext Destruction + EXTERN_C DLLEXPORT void dds_destroy_solvercontext(DDS_SOLVER_CTX ctx); + + // TT Configuration + EXTERN_C DLLEXPORT void dds_configure_tt(DDS_SOLVER_CTX ctx, + TTKind kind, + int defMB, int maxMB); + + EXTERN_C DLLEXPORT void dds_resize_tt(DDS_SOLVER_CTX ctx, + int defMB, + int maxMB); + + EXTERN_C DLLEXPORT void dds_clear_tt(DDS_SOLVER_CTX ctx); + + // Resets + EXTERN_C DLLEXPORT void dds_reset_for_solve(DDS_SOLVER_CTX ctx); + + EXTERN_C DLLEXPORT void dds_reset_best_moves_lite(DDS_SOLVER_CTX ctx); + + // Utilities – simple logging passthrough + EXTERN_C DLLEXPORT void dds_log_append(DDS_SOLVER_CTX ctx, + const char* msg); + + EXTERN_C DLLEXPORT void dds_log_clear(DDS_SOLVER_CTX ctx); + + EXTERN_C DLLEXPORT auto dds_solve_board(DDS_SOLVER_CTX ctx, + const Deal& dl, + int target, + int solutions, + int mode, + FutureTricks* futp) -> int; + + EXTERN_C DLLEXPORT auto dds_calc_dd_table( + DDS_SOLVER_CTX ctx, + const DdTableDeal& table_deal, + DdTableResults* table_results) -> int; + + + EXTERN_C DLLEXPORT auto dds_calc_dd_table_pbn( + DDS_SOLVER_CTX ctx, + const DdTableDealPBN& table_deal, + DdTableResults* table_results) -> int; + + EXTERN_C DLLEXPORT auto dds_calc_par( + DDS_SOLVER_CTX ctx, + const DdTableDeal& table_deal, + int vulnerable, + DdTableResults* table_results, + ParResults* par_results) -> int; + + +} diff --git a/library/src/api/dll.h b/library/src/api/dll.h index 7fbbdd44..e1c3aa9c 100644 --- a/library/src/api/dll.h +++ b/library/src/api/dll.h @@ -429,20 +429,29 @@ struct DDSInfo /** - * @brief Set the maximum number of threads used by the solver. + * @brief Initialise the solver's static memory. * - * @deprecated In the modern C++ API, thread count is controlled by the - * embedding application (typically one SolverContext per worker - * thread). New code should create/destroy SolverContext instances - * in the application rather than calling this function. + * Allocates the transposition-table memory pools, registers scheduler and + * thread-manager state, and performs one-time lookup-table initialisation. + * This does NOT control the number of worker threads — use the + * SolveAllBoardsN / CalcAllTablesN family for per-call thread caps. + */ +EXTERN_C DLLEXPORT auto STDCALL InitializeStaticMemory() -> void; + +/** + * @brief Deprecated alias of InitializeStaticMemory(). + * + * @deprecated Use InitializeStaticMemory(); the thread count argument is + * ignored (internal batch threading was removed). In the modern + * C++ API, thread count is controlled by the embedding application + * (typically one SolverContext per worker thread), or per call via + * the SolveAllBoardsN / CalcAllTablesN family. * See docs/api_migration.md for modern C++ API examples. * - * @param userThreads Maximum number of threads to use + * @param userThreads Ignored; retained for backward compatibility. * * This function is part of the legacy C API and is maintained for backward - * compatibility. It has no direct equivalent in the modern API, where both - * threading and TT memory limits are configured via SolverContext and - * SolverConfig on a per-instance basis. + * compatibility. It simply forwards to InitializeStaticMemory(). */ EXTERN_C DLLEXPORT auto STDCALL SetMaxThreads( int userThreads) -> void; @@ -542,6 +551,17 @@ EXTERN_C DLLEXPORT auto STDCALL CalcDDtable( struct DdTableDeal tableDeal, struct DdTableResults * tablep) -> int; +/** + * @brief CalcDDtable with an explicit worker-thread cap. + * + * @param maxThreads Maximum worker threads; <= 0 selects the automatic + * (hardware_concurrency) default. + */ +EXTERN_C DLLEXPORT auto STDCALL CalcDDtableN( + struct DdTableDeal tableDeal, + struct DdTableResults * tablep, + int maxThreads) -> int; + /** * @brief Calculate the double dummy table for a PBN Deal. * @@ -553,6 +573,17 @@ EXTERN_C DLLEXPORT auto STDCALL CalcDDtablePBN( struct DdTableDealPBN tableDealPBN, struct DdTableResults * tablep) -> int; +/** + * @brief CalcDDtablePBN with an explicit worker-thread cap. + * + * @param maxThreads Maximum worker threads; <= 0 selects the automatic + * (hardware_concurrency) default. + */ +EXTERN_C DLLEXPORT auto STDCALL CalcDDtablePBNN( + struct DdTableDealPBN tableDealPBN, + struct DdTableResults * tablep, + int maxThreads) -> int; + /** * @brief Calculate double dummy tables for multiple deals. * @@ -570,6 +601,20 @@ EXTERN_C DLLEXPORT auto STDCALL CalcAllTables( struct DdTablesRes * resp, struct AllParResults * presp) -> int; +/** + * @brief CalcAllTables with an explicit worker-thread cap. + * + * @param maxThreads Maximum worker threads; <= 0 selects the automatic + * (hardware_concurrency) default. + */ +EXTERN_C DLLEXPORT auto STDCALL CalcAllTablesN( + struct DdTableDeals const * dealsp, + int mode, + int const trumpFilter[DDS_STRAINS], + struct DdTablesRes * resp, + struct AllParResults * presp, + int maxThreads) -> int; + /** * @brief Calculate double dummy tables for multiple PBN deals. * @@ -587,6 +632,20 @@ EXTERN_C DLLEXPORT auto STDCALL CalcAllTablesPBN( struct DdTablesRes * resp, struct AllParResults * presp) -> int; +/** + * @brief CalcAllTablesPBN with an explicit worker-thread cap. + * + * @param maxThreads Maximum worker threads; <= 0 selects the automatic + * (hardware_concurrency) default. + */ +EXTERN_C DLLEXPORT auto STDCALL CalcAllTablesPBNN( + struct DdTableDealsPBN const * dealsp, + int mode, + int const trumpFilter[DDS_STRAINS], + struct DdTablesRes * resp, + struct AllParResults * presp, + int maxThreads) -> int; + /** * @brief Solve multiple bridge deals in PBN format. * @@ -598,10 +657,32 @@ EXTERN_C DLLEXPORT auto STDCALL SolveAllBoards( struct BoardsPBN const * bop, struct SolvedBoards * solvedp) -> int; +/** + * @brief SolveAllBoards with an explicit worker-thread cap. + * + * @param maxThreads Maximum worker threads; <= 0 selects the automatic + * (hardware_concurrency) default. + */ +EXTERN_C DLLEXPORT auto STDCALL SolveAllBoardsN( + struct BoardsPBN const * bop, + struct SolvedBoards * solvedp, + int maxThreads) -> int; + EXTERN_C DLLEXPORT auto STDCALL SolveAllBoardsBin( struct Boards const * bop, struct SolvedBoards * solvedp) -> int; +/** + * @brief SolveAllBoardsBin with an explicit worker-thread cap. + * + * @param maxThreads Maximum worker threads; <= 0 selects the automatic + * (hardware_concurrency) default. + */ +EXTERN_C DLLEXPORT auto STDCALL SolveAllBoardsBinN( + struct Boards const * bop, + struct SolvedBoards * solvedp, + int maxThreads) -> int; + EXTERN_C DLLEXPORT auto STDCALL SolveAllBoardsSeq( struct BoardsPBN const * bop, struct SolvedBoards * solvedp) -> int; diff --git a/library/src/calc_tables.cpp b/library/src/calc_tables.cpp index 96555a82..4ff9fc45 100644 --- a/library/src/calc_tables.cpp +++ b/library/src/calc_tables.cpp @@ -8,6 +8,12 @@ */ #include "calc_tables.hpp" +#include +#include +#include +#include +#include + #include #include #include @@ -19,6 +25,133 @@ extern Memory memory; extern Scheduler scheduler; +extern System sysdep; + +namespace { + +struct CalcRunParam +{ + Boards* bop = nullptr; + SolvedBoards* solvedp = nullptr; + std::atomic error{RETURN_NO_FAULT}; +}; + +struct CalcThreadPool +{ + std::mutex mu; + std::vector> slots; +}; + +CalcThreadPool& calc_thread_pool() +{ + static CalcThreadPool pool; + return pool; +} + +auto solver_config_for_thread(const unsigned thr_id) -> SolverConfig +{ + SolverConfig cfg; + if (thr_id < memory.NumThreads() && memory.ThreadSize(thr_id) == "S") + cfg.tt_kind_ = TTKind::Small; + return cfg; +} + +void ensure_calc_contexts(const int num_threads) +{ + if (num_threads <= 0) + return; + + auto& pool = calc_thread_pool(); + std::lock_guard lock(pool.mu); + const unsigned n = static_cast(num_threads); + if (pool.slots.size() < n) + pool.slots.resize(n); + for (unsigned k = 0; k < n; ++k) + { + if (!pool.slots[k]) + pool.slots[k] = std::make_unique(solver_config_for_thread(k)); + } +} + +void calc_chunk_common(const int thr_id, CalcRunParam& param) +{ + SolverContext& ctx = *calc_thread_pool().slots[static_cast(thr_id)]; + + while (true) + { + const schedType st = scheduler.GetNumber(thr_id); + const int index = st.number; + if (index == -1) + break; + + if (st.repeatOf != -1) + { + START_THREAD_TIMER(thr_id); + for (int k = 0; k < DDS_HANDS; k++) + { + param.bop->deals[index].first = k; + param.solvedp->solved_board[index].score[k] = + param.solvedp->solved_board[st.repeatOf].score[k]; + } + END_THREAD_TIMER(thr_id); + continue; + } + + START_THREAD_TIMER(thr_id); + const int res = calc_single_common_internal( + ctx, *param.bop, *param.solvedp, index); + END_THREAD_TIMER(thr_id); + + if (res != RETURN_NO_FAULT) + { + int expected = RETURN_NO_FAULT; + param.error.compare_exchange_strong( + expected, res, std::memory_order_relaxed); + break; + } + } +} + +auto run_calc_threads(CalcRunParam& param) -> int +{ + const int num_threads = sysdep.get_num_threads(); + ensure_calc_contexts(num_threads); + if (num_threads <= 1) + { + calc_chunk_common(0, param); + return RETURN_NO_FAULT; + } + + std::vector threads; + threads.reserve(static_cast(num_threads)); + try + { + for (int k = 0; k < num_threads; ++k) + threads.emplace_back(calc_chunk_common, k, std::ref(param)); + } + catch (...) + { + for (auto& th : threads) + { + if (th.joinable()) + th.join(); + } + throw; + } + + for (auto& th : threads) + th.join(); + + return RETURN_NO_FAULT; +} + +} // namespace + +auto clear_calc_thread_contexts() -> void +{ + std::lock_guard lock(calc_thread_pool().mu); + calc_thread_pool().slots.clear(); +} // Legacy overload (creates temporary context) auto calc_all_boards_n( @@ -102,13 +235,42 @@ auto calc_all_boards_n( return RETURN_NO_FAULT; } -// Legacy overload: creates temporary context +// Legacy overload: parallel across boards via scheduler + persistent contexts. auto calc_all_boards_n( Boards * bop, SolvedBoards * solvedp) -> int { - SolverContext ctx; - return calc_all_boards_n(ctx, bop, solvedp); + const int n = bop->no_of_boards; + if (n > MAXNOOFBOARDS) + return RETURN_TOO_MANY_BOARDS; + + CalcRunParam param; + param.bop = bop; + param.solvedp = solvedp; + param.error.store(RETURN_NO_FAULT, std::memory_order_relaxed); + + scheduler.RegisterRun(RunMode::DDS_RUN_CALC, *bop); + + for (int k = 0; k < MAXNOOFBOARDS; k++) + solvedp->solved_board[k].cards = 0; + + START_BLOCK_TIMER; + + const int ret_run = run_calc_threads(param); + + END_BLOCK_TIMER; + + if (ret_run != RETURN_NO_FAULT) + return ret_run; + + solvedp->no_of_boards = n; + +#ifdef DDS_SCHEDULER + scheduler.PrintTiming(); +#endif + + const int err = param.error.load(std::memory_order_relaxed); + return err != RETURN_NO_FAULT ? err : RETURN_NO_FAULT; } diff --git a/library/src/dds.cpp b/library/src/dds.cpp index 5c4c7f3d..64991270 100644 --- a/library/src/dds.cpp +++ b/library/src/dds.cpp @@ -36,10 +36,9 @@ extern "C" BOOL APIENTRY DllMain( { if (ul_reason_for_call == DLL_PROCESS_ATTACH) - SetMaxThreads(0); + InitializeStaticMemory(); else if (ul_reason_for_call == DLL_PROCESS_DETACH) { - CloseDebugFiles(); FreeMemory(); #ifdef DDS_MEMORY_LEAKS_WIN32 _CrtDumpMemoryLeaks(); @@ -62,42 +61,50 @@ void DDSInitialize(), DDSFinalize(); /** * @brief Initialize the DDS library. */ -void DDSInitialize(void) +void DDSInitialize(void) { - SetMaxThreads(0); + InitializeStaticMemory(); } /** * @brief Finalize and clean up the DDS library. */ -void DDSFinalize(void) +void DDSFinalize(void) { - CloseDebugFiles(); FreeMemory(); } -#elif defined(USES_CONSTRUCTOR) /** - * @brief Library constructor for platforms supporting constructor attribute. + * @brief Library constructor/destructor for Apple platforms. * - * This function is called when the library is loaded. + * Register DDSInitialize/DDSFinalize so the library's static memory is set + * up automatically when the library is loaded, matching the behaviour of the + * Windows (DllMain) and USES_CONSTRUCTOR paths. This frees callers from having + * to call InitializeStaticMemory() themselves. */ static void __attribute__ ((constructor)) libInit(void) { - SetMaxThreads(0); + DDSInitialize(); } +static void __attribute__ ((destructor)) libFini(void) +{ + DDSFinalize(); +} + +#elif defined(USES_CONSTRUCTOR) + /** - * @brief Library destructor for platforms supporting destructor attribute. + * @brief Library constructor for platforms supporting constructor attribute. * - * This function is called when the library is unloaded. + * This function is called when the library is loaded. */ -static void __attribute__ ((destructor)) libEnd(void) +static void __attribute__ ((constructor)) libInit(void) { - CloseDebugFiles(); + InitializeStaticMemory(); } #endif diff --git a/library/src/dds_api.cpp b/library/src/dds_api.cpp new file mode 100644 index 00000000..a10973a6 --- /dev/null +++ b/library/src/dds_api.cpp @@ -0,0 +1,97 @@ +/* + DDS, a bridge double dummy solver. + + Version 3 API implementation - Solver Context Management + This file ensures the dds_api.hpp functions are compiled into the DLL. +*/ + +#include +#include + +// Creation +DLLEXPORT DDS_SOLVER_CTX dds_create_solvercontext_default() +{ + SolverConfig cfg{}; + return new SolverContext(cfg); +} + +DLLEXPORT DDS_SOLVER_CTX dds_create_solvercontext(SolverConfig cfg) +{ + return new SolverContext(cfg); +} + +// SolverContext Destruction +DLLEXPORT void dds_destroy_solvercontext(DDS_SOLVER_CTX ctx) +{ + delete ctx; +} + +// TT Configuration +DLLEXPORT void dds_configure_tt(DDS_SOLVER_CTX ctx, TTKind kind, int defMB, int maxMB) +{ + ctx->configure_tt(kind, defMB, maxMB); +} + +DLLEXPORT void dds_resize_tt(DDS_SOLVER_CTX ctx, int defMB, int maxMB) +{ + ctx->resize_tt(defMB, maxMB); +} + +DLLEXPORT void dds_clear_tt(DDS_SOLVER_CTX ctx) +{ + ctx->clear_tt(); +} + +// Resets +DLLEXPORT void dds_reset_for_solve(DDS_SOLVER_CTX ctx) +{ + ctx->reset_for_solve(); +} + +DLLEXPORT void dds_reset_best_moves_lite(DDS_SOLVER_CTX ctx) +{ + ctx->reset_best_moves_lite(); +} + +// Utilities – simple logging passthrough +DLLEXPORT void dds_log_append(DDS_SOLVER_CTX ctx, const char* msg) +{ + ctx->utilities().log_append(std::string(msg ? msg : "")); +} + +DLLEXPORT void dds_log_clear(DDS_SOLVER_CTX ctx) +{ + ctx->utilities().log_clear(); +} + +DLLEXPORT auto dds_solve_board(DDS_SOLVER_CTX ctx, const Deal& dl, int target, int solutions, int mode, FutureTricks* futp) -> int +{ + return SolveBoard(*ctx, + dl, + target, + solutions, + mode, + futp); +} + +DLLEXPORT auto dds_calc_dd_table(DDS_SOLVER_CTX ctx, const DdTableDeal& table_deal, DdTableResults* table_results) -> int +{ + return calc_dd_table(*ctx, table_deal, table_results); +} + +DLLEXPORT auto dds_calc_dd_table_pbn(DDS_SOLVER_CTX ctx, const DdTableDealPBN& table_deal, DdTableResults* table_results) -> int +{ + + return calc_dd_table_pbn(*ctx, table_deal, table_results); +} + +DLLEXPORT auto dds_calc_par( + DDS_SOLVER_CTX ctx, + const DdTableDeal& table_deal, + int vulnerable, + DdTableResults* table_results, + ParResults* par_results) -> int +{ + return calc_par(*ctx, table_deal, vulnerable, table_results, par_results); +} + diff --git a/library/src/dump.cpp b/library/src/dump.cpp index 860333ed..eb7419c7 100644 --- a/library/src/dump.cpp +++ b/library/src/dump.cpp @@ -15,6 +15,7 @@ #include "dump.hpp" #include #include +#include std::string PrintSuit(const unsigned short suitCode); @@ -335,3 +336,56 @@ void DumpTopLevel( ctx.search().trick_nodes() << " trick nodes\n\n"; } + +#ifdef DDS_AB_HITS + +void DumpRetrieved( + std::ofstream& fout, + const Pos& tpos, + const NodeCards& node, + const int target, + const int depth) +{ + fout << "Retrieved entry\n"; + fout << std::string(15, '-') << "\n"; + fout << PosToText(tpos, target, depth) << "\n"; + fout << FullNodeToText(node) << "\n"; + fout << RankToDiagrams(tpos.rank_in_suit, node) << "\n"; +} + + +void DumpStored( + std::ofstream& fout, + const Pos& tpos, + const Moves& moves, + const NodeCards& node, + const int target, + const int depth) +{ + fout << "Stored entry\n"; + fout << std::string(12, '-') << "\n"; + fout << PosToText(tpos, target, depth) << "\n"; + fout << NodeToText(node); + fout << moves.TrickToText((depth >> 2) + 1) << "\n"; + fout << PrintDeal(tpos.rank_in_suit, 16); +} + + +void DumpStored( + std::ofstream& fout, + const Pos& tpos, + SolverContext& ctx, + const NodeCards& node, + const int target, + const int depth) +{ + fout << "Stored entry\n"; + fout << std::string(12, '-') << "\n"; + fout << PosToText(tpos, target, depth) << "\n"; + fout << NodeToText(node); + fout << ctx.move_gen().trick_to_text((depth >> 2) + 1) << "\n"; + fout << PrintDeal(tpos.rank_in_suit, 16); +} + +#endif // DDS_AB_HITS + diff --git a/library/src/heuristic_sorting/heuristic_sorting.cpp b/library/src/heuristic_sorting/heuristic_sorting.cpp index 97bbe15f..a9ab8980 100644 --- a/library/src/heuristic_sorting/heuristic_sorting.cpp +++ b/library/src/heuristic_sorting/heuristic_sorting.cpp @@ -672,53 +672,21 @@ void weight_alloc_trump_void1(HeuristicContext& ctx) const int partner_lh = partner[lead_hand]; const int rho_lh = rho[lead_hand]; - + unsigned short suitCount = tpos.length[curr_hand][suit]; int suitAdd; - if (suit == trump) + if (lead_suit == trump) // We pitch { - // We trump a non-trump card. - - if (tpos.length[partner_lh][lead_suit] != 0) - { - // 3rd hand will follow. - if ((tpos.rank_in_suit[rho_lh][lead_suit] > - (tpos.rank_in_suit[partner_lh][lead_suit] | - bit_map_rank[ctx.lead0_rank])) || - ((tpos.length[rho_lh][lead_suit] == 0) && - (tpos.length[rho_lh][trump] != 0))) - { - // Partner can win with a card or by ruffing. - suitAdd = 60 + (suitCount << 6) / 44; - } - else - { - suitAdd = -2 + (suitCount << 6) / 36; - // Don't ruff from Kx. - if ((suitCount == 2) && - (tpos.second_best[suit].hand == curr_hand)) - suitAdd += -4; - } - } - else if ((tpos.length[rho_lh][lead_suit] == 0) && - (tpos.rank_in_suit[rho_lh][trump] > - tpos.rank_in_suit[partner_lh][trump])) - { - // Partner can overruff 3rd hand. - suitAdd = 60 + (suitCount << 6) / 44; - } - else if ((tpos.length[partner_lh][trump] == 0) && - (tpos.rank_in_suit[rho_lh][lead_suit] > - bit_map_rank[ctx.lead0_rank])) - { - // 3rd hand has no trumps, and partner has suit winner. - suitAdd = 60 + (suitCount << 6) / 44; - } + if (tpos.rank_in_suit[rho_lh][lead_suit] > + (tpos.rank_in_suit[partner_lh][lead_suit] | + bit_map_rank[ctx.lead0_rank])) + // RHO can win. + suitAdd = (suitCount << 6) / 44; else { - suitAdd = -2 + (suitCount << 6) / 36; - // Don't ruff from Kx. + // Don't pitch from Kx. + suitAdd = (suitCount << 6) / 36; if ((suitCount == 2) && (tpos.second_best[suit].hand == curr_hand)) suitAdd += -4; @@ -734,9 +702,9 @@ void weight_alloc_trump_void1(HeuristicContext& ctx) if (tpos.length[partner_lh][lead_suit] != 0) { // 3rd hand will follow. - if (tpos.rank_in_suit[rho_lh][lead_suit] > - (tpos.rank_in_suit[partner_lh][lead_suit] | - bit_map_rank[ctx.lead0_rank])) + if (tpos.rank_in_suit[rho_lh][lead_suit] > + (tpos.rank_in_suit[partner_lh][lead_suit] | + bit_map_rank[ctx.lead0_rank])) // Partner has winning card. suitAdd = 60 + (suitCount << 6) / 44; else if ((tpos.length[rho_lh][lead_suit] == 0) @@ -758,9 +726,9 @@ void weight_alloc_trump_void1(HeuristicContext& ctx) tpos.rank_in_suit[partner_lh][trump])) // Partner can overruff 3rd hand. suitAdd = 60 + (suitCount << 6) / 44; - else if ((tpos.length[partner_lh][trump] == 0) - && (tpos.rank_in_suit[rho_lh][lead_suit] > - bit_map_rank[ctx.lead0_rank])) + else if ((tpos.length[partner_lh][trump] == 0) + && (tpos.rank_in_suit[rho_lh][lead_suit] > + bit_map_rank[ctx.lead0_rank])) // 3rd hand has no trumps, and partner has suit winner. suitAdd = 60 + (suitCount << 6) / 44; else @@ -883,7 +851,7 @@ int rank_forces_ace(const HeuristicContext& ctx, const int cards4th) while (g >= 1 && ((mp.gap_[g] & removed) == mp.gap_[g])) g--; - if (! g) + if (g <= 0) return -1; // RHO's second-highest rank. @@ -1213,7 +1181,7 @@ void weight_alloc_trump_void2(HeuristicContext& ctx) MoveType* mply = ctx.mply; const int rho_lh = rho[lead_hand]; - + int suitAdd; const unsigned short suitCount = tpos.length[curr_hand][suit]; const int max4th = highest_rank[tpos.rank_in_suit[rho_lh][lead_suit]]; @@ -1241,19 +1209,16 @@ void weight_alloc_trump_void2(HeuristicContext& ctx) for (int k = last_num_moves; k < num_moves; k++) { - if (ctx.move1_suit == trump && - mply[k].rank < ctx.move1_rank) + if (ctx.move1_suit == trump && + mply[k].rank < ctx.move1_rank) { // Don't underruff. - unsigned char aggrSuit = static_cast(tpos.aggr[suit]); - unsigned char moveRank = static_cast(mply[k].rank); - unsigned char relRankValue = static_cast(rel_rank[aggrSuit][moveRank]); - int r_rank = static_cast(relRankValue); + int r_rank = rel_rank[tpos.aggr[suit]][mply[k].rank]; suitAdd = (suitCount << 6) / 40; mply[k].weight = -32 + r_rank + suitAdd; } - else if (ctx.high1 == 0) + else if (ctx.high1 == 0) { // We ruff partner's winner over 2nd hand. if (max4th != 0) @@ -1418,10 +1383,7 @@ void weight_alloc_trump_void3(HeuristicContext& ctx) { for (int k = last_num_moves; k < num_moves; k++) { - int r_rank = static_cast( - static_cast( - rel_rank[static_cast(tpos.aggr[suit])] - [static_cast(mply[k].rank)])); + int r_rank = rel_rank[tpos.aggr[suit]][mply[k].rank]; if (mply[k].rank > ctx.move2_rank) mply[k].weight = 33 + r_rank; // Overruff else @@ -1436,10 +1398,7 @@ void weight_alloc_trump_void3(HeuristicContext& ctx) { for (int k = last_num_moves; k < num_moves; k++) { - int r_rank = static_cast( - static_cast( - rel_rank[static_cast(tpos.aggr[suit])] - [static_cast(mply[k].rank)])); + int r_rank = rel_rank[tpos.aggr[suit]][mply[k].rank]; mply[k].weight = 33 + r_rank; } } diff --git a/library/src/init.cpp b/library/src/init.cpp index 27243514..6404f79d 100644 --- a/library/src/init.cpp +++ b/library/src/init.cpp @@ -12,7 +12,7 @@ #include #include -#include +#include #include "init.hpp" #include #include @@ -74,17 +74,14 @@ void STDCALL SetResources( int memMaxMB = min(memMaxGivenMB, memMaxFreeMB); memMaxMB = min(memMaxMB, memMax32bMB); - // Internal parallel execution has been removed. - // Legacy API calls execute sequentially, so use a single internal thread. - if (maxThreadsIn > 1) { - std::fprintf( - stderr, - "DDS warning: SetResources maxThreadsIn=%d requested, but internal batch threading is disabled; using 1 thread.\n", - maxThreadsIn); - } - (void) maxThreadsIn; - (void) ncores; - const int thrMax = 1; + // Limit worker count by memory budget and hardware. + int thrMax; + if (maxThreadsIn <= 0) + thrMax = ncores; + else + thrMax = std::min(maxThreadsIn, ncores); + if (thrMax < 1) + thrMax = 1; // For simplicity we won't vary the amount of memory per thread // in the small and large versions. @@ -398,6 +395,7 @@ void STDCALL GetDDSInfo(DDSInfo * info) */ void STDCALL FreeMemory() { + clear_worker_contexts(); for (unsigned thrId = 0; thrId < memory.NumThreads(); thrId++) memory.ReturnThread(thrId); } diff --git a/library/src/init.hpp b/library/src/init.hpp index d5c20e39..0c84da69 100644 --- a/library/src/init.hpp +++ b/library/src/init.hpp @@ -23,5 +23,3 @@ void InitWinners( const Deal& dl, Pos& posPoint, const std::shared_ptr& thrp); - -void CloseDebugFiles(); diff --git a/library/src/later_tricks.cpp b/library/src/later_tricks.cpp index bc8e08ca..98102dcb 100644 --- a/library/src/later_tricks.cpp +++ b/library/src/later_tricks.cpp @@ -163,7 +163,7 @@ bool LaterTricksMIN( fprintf(stderr, "LaterTricksMIN: invalid aggr=%u (depth=%d)", aggr, depth); return true; // conservative fallback } - int h = ctx.thread()->rel[aggr].abs_rank[3][trump].hand; + int h = ctx.thread_ptr()->rel[aggr].abs_rank[3][trump].hand; if (h == -1) return true; @@ -176,7 +176,7 @@ bool LaterTricksMIN( for (int ss = 0; ss < DDS_SUITS; ss++) if (depth_ok) tpos.win_ranks[depth][ss] = 0; if (depth_ok) tpos.win_ranks[depth][trump] = bit_map_rank[ - static_cast(static_cast(ctx.thread()->rel[aggr].abs_rank[3][trump].rank)) ]; + static_cast(static_cast(ctx.thread_ptr()->rel[aggr].abs_rank[3][trump].rank)) ]; return false; } } @@ -338,7 +338,7 @@ bool LaterTricksMAX( fprintf(stderr, "LaterTricksMAX: invalid aggr=%u (depth=%d)\n", aggr, depth); return false; // conservative fallback for MAX } - int h = ctx.thread()->rel[aggr].abs_rank[3][trump].hand; + int h = ctx.thread_ptr()->rel[aggr].abs_rank[3][trump].hand; if (h == -1) return false; @@ -351,7 +351,7 @@ bool LaterTricksMAX( for (int ss = 0; ss < DDS_SUITS; ss++) if (depth_ok) tpos.win_ranks[depth][ss] = 0; if (depth_ok) tpos.win_ranks[depth][trump] = bit_map_rank[ - static_cast(static_cast(ctx.thread()->rel[aggr].abs_rank[3][trump].rank)) ]; + static_cast(static_cast(ctx.thread_ptr()->rel[aggr].abs_rank[3][trump].rank)) ]; return true; } } diff --git a/library/src/moves/moves.cpp b/library/src/moves/moves.cpp index b4a99587..00a42101 100644 --- a/library/src/moves/moves.cpp +++ b/library/src/moves/moves.cpp @@ -72,6 +72,8 @@ Moves::Moves() { for (int h = 0; h < DDS_HANDS; h++) { lastCall[t][h] = MgType::SIZE; + moveList[t][h] = {}; + trickTable[t][h].count = 0; trickSuitTable[t][h].count = 0; diff --git a/library/src/quick_tricks.cpp b/library/src/quick_tricks.cpp index ac1417b1..48f37fe5 100644 --- a/library/src/quick_tricks.cpp +++ b/library/src/quick_tricks.cpp @@ -997,10 +997,10 @@ int QuickTricksPartnerHandTrump( for (int h = 0; h < DDS_HANDS; h++) ranks |= tpos.rank_in_suit[h][suit]; - if (ctx.thread()->rel[ranks].abs_rank[3][suit].hand == partner[hand]) + if (ctx.thread_ptr()->rel[ranks].abs_rank[3][suit].hand == partner[hand]) { tpos.win_ranks[depth][suit] |= bit_map_rank[ - static_cast(static_cast(ctx.thread()->rel[ranks].abs_rank[3][suit].rank)) ]; + static_cast(ctx.thread_ptr()->rel[ranks].abs_rank[3][suit].rank) ]; tpos.win_ranks[depth][commSuit] |= bit_map_rank[commRank]; @@ -1107,17 +1107,15 @@ int QuickTricksPartnerHandNT( for (int h = 0; h < DDS_HANDS; h++) ranks |= tpos.rank_in_suit[h][suit]; - if (ctx.thread()->rel[ranks].abs_rank[3][suit].hand == partner[hand]) + if (ctx.thread_ptr()->rel[ranks].abs_rank[3][suit].hand == partner[hand]) { tpos.win_ranks[depth][suit] |= bit_map_rank[ - static_cast(static_cast(ctx.thread()->rel[ranks].abs_rank[3][suit].rank)) ]; + static_cast(ctx.thread_ptr()->rel[ranks].abs_rank[3][suit].rank) ]; qt++; if (qt >= cutoff) return qt; if ((countOwn <= 2) && (countLho <= 2) && (countRho <= 2)) { - // TODO: Is the fix to qt correct? - // qtricks += countPart - 2; qt += countPart - 2; if (qt >= cutoff) return qt; diff --git a/library/src/solve_board.cpp b/library/src/solve_board.cpp index 82d76b8d..49fc7ef2 100644 --- a/library/src/solve_board.cpp +++ b/library/src/solve_board.cpp @@ -7,22 +7,18 @@ See LICENSE and README. */ -#include -#include #include -#include -#include #include "solve_board.hpp" #include #include #include +#include #include #include #include -extern Memory memory; extern Scheduler scheduler; auto same_board( @@ -31,9 +27,41 @@ auto same_board( const unsigned index2) -> bool; +static auto boards_from_pbn( + BoardsPBN const& bop, + Boards& bo) -> int +{ + bo.no_of_boards = bop.no_of_boards; + if (bo.no_of_boards > MAXNOOFBOARDS) + return RETURN_TOO_MANY_BOARDS; + + for (int k = 0; k < bop.no_of_boards; k++) + { + bo.mode[k] = bop.mode[k]; + bo.solutions[k] = bop.solutions[k]; + bo.target[k] = bop.target[k]; + bo.deals[k].first = bop.deals[k].first; + bo.deals[k].trump = bop.deals[k].trump; + + for (int i = 0; i <= 2; i++) + { + bo.deals[k].currentTrickSuit[i] = bop.deals[k].currentTrickSuit[i]; + bo.deals[k].currentTrickRank[i] = bop.deals[k].currentTrickRank[i]; + } + + if (convert_from_pbn(bop.deals[k].remainCards, bo.deals[k].remainCards) + != RETURN_NO_FAULT) + return RETURN_PBN_FAULT; + } + + return RETURN_NO_FAULT; +} + + auto solve_all_boards_n( Boards const& bds, - SolvedBoards& solved) -> int + SolvedBoards& solved, + int max_threads) -> int { const int n = bds.no_of_boards; if (n > MAXNOOFBOARDS) @@ -44,17 +72,11 @@ auto solve_all_boards_n( scheduler.RegisterRun(RunMode::DDS_RUN_SOLVE, bds); - const int nthreads = std::max(1, - std::min(static_cast(std::thread::hardware_concurrency()), n)); - - std::atomic next_board{0}; - std::atomic first_error{0}; + START_BLOCK_TIMER; - auto worker = [&] { - for (;;) { - const int bno = next_board.fetch_add(1, std::memory_order_relaxed); - if (bno >= n || first_error.load(std::memory_order_relaxed) != 0) - break; + const int err = parallel_all_boards_n(n, max_threads, + [&](const int worker_id, const int bno) -> int { + (void)worker_id; FutureTricks fut; const auto t0 = std::chrono::steady_clock::now(); @@ -66,37 +88,14 @@ auto solve_all_boards_n( if (dur < 0) dur = 0; scheduler.SetBoardTime(bno, static_cast(dur)); - if (res == 1) + if (res == RETURN_NO_FAULT) solved.solved_board[bno] = fut; - else { - int expected = 0; - first_error.compare_exchange_strong( - expected, res, std::memory_order_relaxed); - } - } - }; + return res; + }); - START_BLOCK_TIMER; - { - // Avoid std::jthread here: Emscripten's libc++ on Windows does not - // provide it yet, while std::thread is widely available. - std::vector threads; - threads.reserve(static_cast(nthreads)); - try { - for (int i = 0; i < nthreads; ++i) - threads.emplace_back(worker); - } catch (...) { - for (auto& t : threads) - if (t.joinable()) - t.join(); - throw; - } - for (auto& t : threads) - t.join(); - } END_BLOCK_TIMER; - if (const int err = first_error.load(); err != 0) + if (err != RETURN_NO_FAULT) return err; solved.no_of_boards = n; @@ -109,6 +108,19 @@ auto solve_all_boards_n( } +auto solve_all_boards_pbn_n( + BoardsPBN const& bop, + SolvedBoards& solved, + const int max_threads) -> int +{ + Boards bo; + const int rc = boards_from_pbn(bop, bo); + if (rc != RETURN_NO_FAULT) + return rc; + return solve_all_boards_n(bo, solved, max_threads); +} + + /* * Solve a single bridge Deal in PBN format. * @@ -148,36 +160,29 @@ int STDCALL SolveBoardPBN( * @param solvedp Pointer to results for solved Boards * @return 1 on success, error code otherwise */ -int STDCALL SolveAllBoards( +int STDCALL SolveAllBoardsN( BoardsPBN const * bop, - SolvedBoards * solvedp) + SolvedBoards * solvedp, + int maxThreads) { - Boards bo; - bo.no_of_boards = bop->no_of_boards; - if (bo.no_of_boards > MAXNOOFBOARDS) - return RETURN_TOO_MANY_BOARDS; + return solve_all_boards_pbn_n(* bop, * solvedp, maxThreads); +} - for (int k = 0; k < bop->no_of_boards; k++) - { - bo.mode[k] = bop->mode[k]; - bo.solutions[k] = bop->solutions[k]; - bo.target[k] = bop->target[k]; - bo.deals[k].first = bop->deals[k].first; - bo.deals[k].trump = bop->deals[k].trump; - for (int i = 0; i <= 2; i++) - { - bo.deals[k].currentTrickSuit[i] = bop->deals[k].currentTrickSuit[i]; - bo.deals[k].currentTrickRank[i] = bop->deals[k].currentTrickRank[i]; - } +int STDCALL SolveAllBoards( + BoardsPBN const * bop, + SolvedBoards * solvedp) +{ + return SolveAllBoardsN(bop, solvedp, 0); +} - if (convert_from_pbn(bop->deals[k].remainCards, bo.deals[k].remainCards) - != 1) - return RETURN_PBN_FAULT; - } - int res = solve_all_boards_n(bo, * solvedp); - return res; +int STDCALL SolveAllBoardsBinN( + Boards const * bop, + SolvedBoards * solvedp, + int maxThreads) +{ + return solve_all_boards_n(* bop, * solvedp, maxThreads); } @@ -185,7 +190,7 @@ int STDCALL SolveAllBoardsBin( Boards const * bop, SolvedBoards * solvedp) { - return solve_all_boards_n(* bop, * solvedp); + return SolveAllBoardsBinN(bop, solvedp, 0); } @@ -194,29 +199,9 @@ int STDCALL SolveAllBoardsSeq( SolvedBoards * solvedp) { Boards bo; - bo.no_of_boards = bop->no_of_boards; - if (bo.no_of_boards > MAXNOOFBOARDS) - return RETURN_TOO_MANY_BOARDS; - - for (int k = 0; k < bop->no_of_boards; k++) - { - bo.mode[k] = bop->mode[k]; - bo.solutions[k] = bop->solutions[k]; - bo.target[k] = bop->target[k]; - bo.deals[k].first = bop->deals[k].first; - bo.deals[k].trump = bop->deals[k].trump; - - for (int i = 0; i <= 2; i++) - { - bo.deals[k].currentTrickSuit[i] = bop->deals[k].currentTrickSuit[i]; - bo.deals[k].currentTrickRank[i] = bop->deals[k].currentTrickRank[i]; - } - - if (convert_from_pbn(bop->deals[k].remainCards, bo.deals[k].remainCards) - != 1) - return RETURN_PBN_FAULT; - } - + const int rc = boards_from_pbn(*bop, bo); + if (rc != RETURN_NO_FAULT) + return rc; return solve_all_boards_n_seq(bo, * solvedp); } diff --git a/library/src/solve_board.hpp b/library/src/solve_board.hpp index 2ce53185..5032aea0 100644 --- a/library/src/solve_board.hpp +++ b/library/src/solve_board.hpp @@ -14,6 +14,16 @@ #include +auto solve_all_boards_n( + Boards const& bds, + SolvedBoards& solved, + int max_threads = 0) -> int; + +auto solve_all_boards_pbn_n( + BoardsPBN const& bop, + SolvedBoards& solved, + int max_threads = 0) -> int; + auto solve_all_boards_n_seq( Boards const& bds, SolvedBoards& solved) -> int; diff --git a/library/src/solver_context/BUILD.bazel b/library/src/solver_context/BUILD.bazel index a566211b..f6dcb422 100644 --- a/library/src/solver_context/BUILD.bazel +++ b/library/src/solver_context/BUILD.bazel @@ -26,7 +26,7 @@ cc_library( includes = ["."], include_prefix = "solver_context", deps = [ - "//library/src/system", + "//library/src/system:system_util_log", "//library/src/api:api_definitions", "//library/src/trans_table", ], @@ -43,7 +43,7 @@ cc_library( includes = ["."], include_prefix = "solver_context", deps = [ - "//library/src/system", + "//library/src/system:system_util_stats", "//library/src/api:api_definitions", "//library/src/trans_table", ], diff --git a/library/src/solver_context/solver_context.cpp b/library/src/solver_context/solver_context.cpp index ceca3280..4b947e37 100644 --- a/library/src/solver_context/solver_context.cpp +++ b/library/src/solver_context/solver_context.cpp @@ -1,24 +1,57 @@ #include "solver_context.hpp" +#include +#include +#include #include #include #include +#include #include +//#include #include #include +#include + +namespace { + +#if defined(DDS_TOP_LEVEL) || defined(DDS_AB_STATS) || defined(DDS_AB_HITS) || \ + defined(DDS_TT_STATS) || defined(DDS_TIMING) || defined(DDS_MOVES) +std::string next_debug_file_suffix() +{ + static std::atomic serial{0}; + return std::to_string(serial.fetch_add(1, std::memory_order_relaxed)) + + DDS_DEBUG_SUFFIX; +} +#endif + +} // namespace + +void SolverContext::bind_thread_data() +{ + // Ensure persistent facades like SearchContext see the bound ThreadData. + search_.set_thread(thr_); + search_.set_owner(this); + if (!thr_) return; + + if (owns_thread_data_) { +#if defined(DDS_TOP_LEVEL) || defined(DDS_AB_STATS) || defined(DDS_AB_HITS) || \ + defined(DDS_TT_STATS) || defined(DDS_TIMING) || defined(DDS_MOVES) + thr_->init_debug_files(next_debug_file_suffix()); +#endif + } +} // Owned-ThreadData constructor: allocate ThreadData as a member of the // SolverContext so callers can create a context at the top of the stack // and pass it down without a separate per-thread lookup. SolverContext::SolverContext(SolverConfig cfg) - : thr_(nullptr), cfg_(cfg) + : cfg_(cfg), owns_thread_data_(true) { // Create an owned ThreadData instance and keep it in thr_. thr_ = std::make_shared(); - // Ensure persistent facades like SearchContext see the bound ThreadData. - search_.set_thread(thr_); - search_.set_owner(this); + bind_thread_data(); } auto SolverContext::trans_table() const -> TransTable* @@ -116,7 +149,14 @@ auto SolverContext::dispose_trans_table() const -> void // Defaulted destructor defined out-of-line so destruction of the // owned std::shared_ptr happens where ThreadData is a // complete type. -SolverContext::~SolverContext() = default; +SolverContext::~SolverContext() +{ + // Close debug files only when this context holds the last shared_ptr to + // ThreadData. close_debug_files() is a no-op when files were never opened + // (e.g. non-owning wrappers over stack ThreadData). + if (thr_ && thr_.use_count() == 1) + thr_->close_debug_files(); +} auto SolverContext::reset_for_solve() const -> void { @@ -233,7 +273,8 @@ auto SolverContext::reset_best_moves_lite() const -> void auto ThreadMemoryUsed() -> double { - // TODO: Only needed because SolverIF wants to set it. Avoid? + // Fixed per-thread lookup-table memory (RelRanksType) included in memUsed + // reporting; legacy SolverIF uses the same accounting. double memUsed = 8192 * sizeof(RelRanksType) / static_cast(1024.); diff --git a/library/src/solver_context/solver_context.hpp b/library/src/solver_context/solver_context.hpp index 47344033..7ba130e6 100644 --- a/library/src/solver_context/solver_context.hpp +++ b/library/src/solver_context/solver_context.hpp @@ -47,12 +47,13 @@ struct SolverConfig class SolverContext { public: + // Wrap existing ThreadData (helper/sub-context). Does not initialize debug + // files; ~SolverContext() closes them only when this context is the last + // shared_ptr holder of that ThreadData. explicit SolverContext(std::shared_ptr thread, SolverConfig cfg = {}) : thr_(std::move(thread)), cfg_(cfg) { - // Bind the persistent facades to the underlying ThreadData. - search_.set_thread(thr_); - search_.set_owner(this); + bind_thread_data(); } // NOTE: constructors that accepted raw ThreadData* were removed as part @@ -77,6 +78,17 @@ class SolverContext return thr_; } + /** + * @brief Non-owning raw access to the underlying ThreadData. + * + * Avoids the atomic reference-count traffic of copying the shared_ptr in + * hot search paths. The pointer is valid for the lifetime of the context. + */ + auto thread_ptr() const -> ThreadData* + { + return thr_.get(); + } + /** * @brief Access the current configuration snapshot. * @@ -431,6 +443,11 @@ class SolverContext // Transposition table is now owned per SearchContext and created lazily. // // See the developer note above for details on TT lifecycle and resets. + + // True when this context created thr_ via SolverContext(SolverConfig). + bool owns_thread_data_ = false; + + void bind_thread_data(); }; auto ThreadMemoryUsed() -> double; diff --git a/library/src/solver_if.cpp b/library/src/solver_if.cpp index 1d100852..d9910a8c 100644 --- a/library/src/solver_if.cpp +++ b/library/src/solver_if.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -21,6 +22,7 @@ extern Memory memory; extern Scheduler scheduler; +extern System sysdep; auto board_range_checks( @@ -72,10 +74,13 @@ int STDCALL SolveBoard( int solutions, int mode, FutureTricks * futp, - [[maybe_unused]] int thrId) + int thrId) { - SolverContext outer_ctx; - return solve_board(outer_ctx, dl, target, solutions, mode, futp); + if (thrId < 0 || thrId >= sysdep.get_num_threads()) + return RETURN_THREAD_INDEX; + + ensure_worker_contexts(sysdep.get_num_threads()); + return solve_board(worker_context_for(thrId), dl, target, solutions, mode, futp); } auto solve_board_internal( diff --git a/library/src/system/BUILD.bazel b/library/src/system/BUILD.bazel index fb05075a..a647463c 100644 --- a/library/src/system/BUILD.bazel +++ b/library/src/system/BUILD.bazel @@ -1,6 +1,8 @@ load("//:CPPVARIABLES.bzl", "DDS_CPPOPTS", "DDS_LINKOPTS", "DDS_LOCAL_DEFINES", "DDS_SCHEDULER_DEFINE") load("@rules_cc//cc:defs.bzl", "cc_library") +# system, system_util_log, and system_util_stats compile the same sources with +# different feature defines. Link at most one variant into a binary. cc_library( name = "system", srcs = glob(["*.cpp"]), @@ -13,6 +15,7 @@ cc_library( "//library/src/api:api_definitions", "//library/src/trans_table", "//library/src/moves:moves", + "//library/src:ab_stats", # Utilities lives under system/util (header-only for now) ], include_prefix = "system", @@ -34,6 +37,7 @@ cc_library( "//library/src/api:api_definitions", "//library/src/trans_table", "//library/src/moves:moves", + "//library/src:ab_stats", ], include_prefix = "system", copts = DDS_CPPOPTS, @@ -53,6 +57,7 @@ cc_library( "//library/src/api:api_definitions", "//library/src/trans_table", "//library/src/moves:moves", + "//library/src:ab_stats", ], include_prefix = "system", copts = DDS_CPPOPTS, diff --git a/library/src/system/file.cpp b/library/src/system/file.cpp new file mode 100644 index 00000000..52a146ed --- /dev/null +++ b/library/src/system/file.cpp @@ -0,0 +1,46 @@ +/* + DDS, a bridge double dummy solver. + + Copyright (C) 2006-2014 by Bo Haglund / + 2014-2018 by Bo Haglund & Soren Hein. + + See LICENSE and README. +*/ + +#include "file.hpp" + +File::~File() +{ + Close(); +} + +void File::Reset() +{ + fname_.clear(); + file_open_ = false; +} + +void File::SetName(const std::string& fname_in) +{ + fname_ = fname_in; +} + +std::ofstream& File::GetStream() +{ + if (!file_open_) + { + fout_.open(fname_); + file_open_ = true; + } + + return fout_; +} + +void File::Close() +{ + if (file_open_) + { + fout_.close(); + file_open_ = false; + } +} diff --git a/library/src/system/file.hpp b/library/src/system/file.hpp new file mode 100644 index 00000000..9e32be83 --- /dev/null +++ b/library/src/system/file.hpp @@ -0,0 +1,42 @@ +/* + DDS, a bridge double dummy solver. + + Copyright (C) 2006-2014 by Bo Haglund / + 2014-2018 by Bo Haglund & Soren Hein. + + See LICENSE and README. +*/ + +#pragma once + +#include +#include + +namespace dds { + +/** + * @brief Lazy-opening output file for debug/statistics logging. + */ +class File +{ + private: + + std::string fname_; + std::ofstream fout_; + + public: + + File() = default; + + ~File(); + + void Reset(); + + void SetName(const std::string& fname_in); + + std::ofstream& GetStream(); + + void Close(); +}; + +} // namespace dds diff --git a/library/src/system/parallel_boards.cpp b/library/src/system/parallel_boards.cpp new file mode 100644 index 00000000..750041e7 --- /dev/null +++ b/library/src/system/parallel_boards.cpp @@ -0,0 +1,110 @@ +/* + DDS, a bridge double dummy solver. + + Copyright (C) 2006-2014 by Bo Haglund / + 2014-2018 by Bo Haglund & Soren Hein. + + See LICENSE and README. +*/ + +#include "parallel_boards.hpp" + +#include +#include +#include +#include + +#include + + +auto resolve_worker_count( + const int max_threads, + const int count) -> int +{ + int workers = max_threads; + if (workers <= 0) + { + const unsigned hw = std::thread::hardware_concurrency(); + workers = hw > 0 ? static_cast(hw) : 1; + } + return std::max(1, std::min(workers, count)); +} + + +auto parallel_all_boards_n( + const int count, + const int worker_cap, + const std::function& process_board) -> int +{ + if (count <= 0) + { + return RETURN_NO_FAULT; + } + + const int workers = resolve_worker_count(worker_cap, count); + + if (workers == 1) + { + for (int bno = 0; bno < count; ++bno) + { + const int rc = process_board(0, bno); + if (rc != RETURN_NO_FAULT) + { + return rc; + } + } + return RETURN_NO_FAULT; + } + + std::atomic next{0}; + std::atomic first_error{RETURN_NO_FAULT}; + + auto worker = [&](const int worker_id) { + for (;;) + { + const int bno = next.fetch_add(1, std::memory_order_relaxed); + if (bno >= count || first_error.load(std::memory_order_relaxed) != RETURN_NO_FAULT) + { + break; + } + + const int rc = process_board(worker_id, bno); + if (rc != RETURN_NO_FAULT) + { + int expected = RETURN_NO_FAULT; + first_error.compare_exchange_strong( + expected, rc, std::memory_order_relaxed); + break; + } + } + }; + + std::vector threads; + threads.reserve(static_cast(workers)); + try + { + for (int t = 0; t < workers; ++t) + { + threads.emplace_back(worker, t); + } + } + catch (...) + { + for (auto & th : threads) + { + if (th.joinable()) + { + th.join(); + } + } + throw; + } + + for (auto & th : threads) + { + th.join(); + } + + const int err = first_error.load(std::memory_order_relaxed); + return err != RETURN_NO_FAULT ? err : RETURN_NO_FAULT; +} diff --git a/library/src/system/parallel_boards.hpp b/library/src/system/parallel_boards.hpp new file mode 100644 index 00000000..2f19b6de --- /dev/null +++ b/library/src/system/parallel_boards.hpp @@ -0,0 +1,36 @@ +/* + DDS, a bridge double dummy solver. + + Copyright (C) 2006-2014 by Bo Haglund / + 2014-2018 by Bo Haglund & Soren Hein. + + See LICENSE and README. +*/ + +#pragma once + +#include + + +/** + * @brief Resolve the number of worker threads to use. + * + * @param max_threads Requested cap; <= 0 means "auto" (use hardware concurrency). + * @param count Number of work items; the result is clamped to [1, count] when count > 0 and to 1 when count <= 0. + * @return The worker count to use. + */ +auto resolve_worker_count(int max_threads, int count) -> int; + +/** + * @brief Process boards [0, count) with work-stealing parallelism. + * + * @param count Number of board indices to process. + * @param worker_cap Maximum worker threads; <= 0 uses hardware concurrency. + * @param process_board Called for each board; must return RETURN_NO_FAULT (1) + * on success. Receives the worker's thread index and board number. + * @return First non-success code from @p process_board, or RETURN_NO_FAULT. + */ +auto parallel_all_boards_n( + int count, + int worker_cap, + const std::function& process_board) -> int; diff --git a/library/src/system/scheduler.cpp b/library/src/system/scheduler.cpp index fffcb180..1cefb850 100644 --- a/library/src/system/scheduler.cpp +++ b/library/src/system/scheduler.cpp @@ -135,7 +135,24 @@ void Scheduler::ClearTiming() void Scheduler::Reset() { for (int b = 0; b < MAXNOOFBOARDS; b++) + { hands[b].next = -1; + hands[b].repeatNo = 0; + hands[b].depth = 0; + hands[b].strength = 0; + hands[b].fanout = 0; + hands[b].thread = 0; + hands[b].selectFlag = 0; + hands[b].time = 0; + } + + for (int g = 0; g < MAXNOOFBOARDS; g++) + { + group[g].head = -1; + group[g].actual = 0; + group[g].repeatNo = 0; + group[g].pred = 0; + } numGroups = 0; extraGroups = 0; @@ -266,6 +283,9 @@ void Scheduler::MakeGroups(const Boards& bds) group[numGroups].strain = strain; group[numGroups].hash = key; + group[numGroups].head = -1; + group[numGroups].actual = 0; + group[numGroups].repeatNo = 0; numGroups++; } else @@ -332,6 +352,9 @@ void Scheduler::FinetuneGroups() group[numGroups].strain = 5; group[numGroups].hash = extraGroups; + group[numGroups].head = -1; + group[numGroups].actual = 0; + group[numGroups].repeatNo = 0; numGroups++; extraGroups++; @@ -422,6 +445,9 @@ void Scheduler::FinetuneGroups() group[numGroups].strain = 5; group[numGroups].hash = extraGroups; + group[numGroups].head = -1; + group[numGroups].actual = 0; + group[numGroups].repeatNo = 0; numGroups++; extraGroups++; @@ -450,24 +476,24 @@ bool Scheduler::SameHand( // that they scale somewhat proportionally to other cases. // The strength parameter is currently not used. -int SORT_SOLVE_TIMES[2][8] = +static int SORT_SOLVE_TIMES[2][8] = { { 284000, 91000, 37000, 23000, 17000, 15000, 13000, 4000 }, { 388000, 140000, 60000, 40000, 30000, 23000, 18000, 6000 }, }; -#define SORT_SOLVE_STRENGTH_CUTOFF 0 - -double SORT_SOLVE_STRENGTH[2][3] = -{ - { 1.525, 1.810, 0.0285 }, - { 1.585, 1.940, 0.0354 } -}; +// #define SORT_SOLVE_STRENGTH_CUTOFF 0 +// +// static double SORT_SOLVE_STRENGTH[2][3] = +// { +// { 1.525, 1.810, 0.0285 }, +// { 1.585, 1.940, 0.0354 } +// }; // Lower end of linear, upper end of linear, slope of linear, // exponential start, coefficient. -double SORT_SOLVE_FANOUT[2][5] = +static double SORT_SOLVE_FANOUT[2][5] = { { 30., 50., 0.07577, 1.515, 12. }, { 30., 50., 0.08144, 1.629, 12. } @@ -543,7 +569,7 @@ void Scheduler::SortSolve() // Lower end of linear, upper end of linear, slope of linear, // exponential start, coefficient. -double SORT_CALC_FANOUT[2][5] = +static double SORT_CALC_FANOUT[2][5] = { { 30., 50., 0.07812, 1.563, 13. }, { 30., 50., 0.07739, 1.548, 12. } @@ -599,7 +625,7 @@ void Scheduler::SortCalc() // These are specific times from a 12-core PC. The hope is // that they scale somewhat proportionally to other cases. -int SORT_TRACE_TIMES[2][8] = +static int SORT_TRACE_TIMES[2][8] = { { 157000, 47000, 26000, 18000, 16000, 14000, 10000, 6000 }, { 205000, 87000, 45000, 36000, 32000, 28000, 24000, 20000 }, @@ -610,7 +636,7 @@ int SORT_TRACE_TIMES[2][8] = // Slope between 16 and 48 incl // Average for 49-52 -double SORT_TRACE_DEPTH[2][4] = +static double SORT_TRACE_DEPTH[2][4] = { { 0.742, 0.411, 0.0414, 1.820 }, { 0.669, 0.428, 0.0346, 1.606 } @@ -619,7 +645,7 @@ double SORT_TRACE_DEPTH[2][4] = // Lower end of linear, upper end of linear, slope of linear, // exponential start, coefficient. -double SORT_TRACE_FANOUT[2][5] = +static double SORT_TRACE_FANOUT[2][5] = { { 30., 50., 0.07577, 1.515, 12. }, { 30., 50., 0.08166, 1.633, 13. } @@ -903,7 +929,7 @@ void Scheduler::EndBlockTimer() if (timeUser > blockMax) blockMax = timeUser; - if (hp->repeatNo == 0) + if (hp->repeatNo == 0 && timeUser > 0) { int bin = timeUser / 1000; timeHist[bin]++; @@ -916,8 +942,11 @@ void Scheduler::EndBlockTimer() for (int g = 0; g < numGroups; g++) { - int head = group[g].head; - int NTflag = (hands[head].strain == 4 ? 1 : 0); + const int head = group[g].head; + if (head < 0 || head >= numHands) + continue; + + const int NTflag = (hands[head].strain == 4 ? 1 : 0); TimeStat ts; diff --git a/library/src/system/system.cpp b/library/src/system/system.cpp index b6965cf3..ac8710c5 100644 --- a/library/src/system/system.cpp +++ b/library/src/system/system.cpp @@ -8,6 +8,7 @@ */ #include +#include #if defined(__linux__) || defined(__APPLE__) || defined(__unix__) #include @@ -388,18 +389,19 @@ string System::get_constructor(int& cons) const int System::get_cores() const { + const unsigned int hw = std::thread::hardware_concurrency(); + if (hw > 0) + return static_cast(hw); + int cores = 0; #if defined(_WIN32) || defined(__CYGWIN__) SYSTEM_INFO sysinfo; GetSystemInfo(&sysinfo); cores = static_cast(sysinfo.dwNumberOfProcessors); #elif defined(__APPLE__) || defined(__linux__) - cores = sysconf(_SC_NPROCESSORS_ONLN); + cores = static_cast(sysconf(_SC_NPROCESSORS_ONLN)); #endif - // TODO Think about thread::hardware_concurrency(). - // This should be standard in C++11. - return cores; } diff --git a/library/src/system/thread_data.cpp b/library/src/system/thread_data.cpp index 9a2b277b..f788e569 100644 --- a/library/src/system/thread_data.cpp +++ b/library/src/system/thread_data.cpp @@ -6,6 +6,9 @@ using std::string; void ThreadData::init_debug_files([[maybe_unused]] const string& suffix) { + if (debug_files_initialized_) + return; + #ifdef DDS_TOP_LEVEL fileTopLevel.SetName(DDS_TOP_LEVEL_PREFIX + suffix); #endif @@ -30,10 +33,15 @@ void ThreadData::init_debug_files([[maybe_unused]] const string& suffix) #ifdef DDS_MOVES fileMoves.SetName(DDS_MOVES_PREFIX + suffix); #endif + + debug_files_initialized_ = true; } void ThreadData::close_debug_files() { + if (!debug_files_initialized_) + return; + #ifdef DDS_TOP_LEVEL fileTopLevel.Close(); #endif @@ -58,4 +66,6 @@ void ThreadData::close_debug_files() #ifdef DDS_MOVES fileMoves.Close(); #endif -} \ No newline at end of file + + debug_files_initialized_ = false; +} diff --git a/library/src/system/thread_data.hpp b/library/src/system/thread_data.hpp index e642b707..19d4646b 100644 --- a/library/src/system/thread_data.hpp +++ b/library/src/system/thread_data.hpp @@ -1,13 +1,19 @@ #ifndef DDS_THREAD_DATA_H #define DDS_THREAD_DATA_H +#include + #include #include #include - #ifdef DDS_AB_STATS - #include "ab_stats.hpp" +#include "ab_stats.hpp" +#endif + +#if defined(DDS_TOP_LEVEL) || defined(DDS_AB_STATS) || defined(DDS_AB_HITS) || \ + defined(DDS_TT_STATS) || defined(DDS_TIMING) || defined(DDS_MOVES) +#include "file.hpp" #endif #ifdef DDS_TIMING @@ -67,37 +73,46 @@ struct ThreadData Moves moves; #ifdef DDS_TOP_LEVEL - File fileTopLevel; + dds::File fileTopLevel; #endif #ifdef DDS_AB_STATS ABstats ABStats; - File fileABstats; + dds::File fileABstats; #endif #ifdef DDS_AB_HITS - File fileRetrieved; - File fileStored; + dds::File fileRetrieved; + dds::File fileStored; #endif #ifdef DDS_TT_STATS - File fileTTstats; + dds::File fileTTstats; #endif #ifdef DDS_TIMING TimerList timerList; - File fileTimerList; + dds::File fileTimerList; #endif #ifdef DDS_MOVES - File fileMoves; + dds::File fileMoves; #endif - // Initialize per-thread debug/stat files with a suffix (e.g., "_suffix"). + // True after init_debug_files(); cleared by close_debug_files(). + bool debug_files_initialized_ = false; + + // Initialize per-thread debug/stat files. suffix is appended to each debug + // prefix (e.g. "0.txt" from SolverContext serial + DDS_DEBUG_SUFFIX). void init_debug_files([[maybe_unused]] const std::string& suffix); // Close any open per-thread debug/stat files. void close_debug_files(); + + auto debug_files_initialized() const -> bool + { + return debug_files_initialized_; + } }; diff --git a/library/src/system/thread_mgr.cpp b/library/src/system/thread_mgr.cpp index 0b3a9b39..a5a1d6b3 100644 --- a/library/src/system/thread_mgr.cpp +++ b/library/src/system/thread_mgr.cpp @@ -17,8 +17,12 @@ #include "thread_mgr.hpp" -mutex mtx; -mutex mtxPrint; +namespace { + +std::mutex mtx; +std::mutex mtxPrint; + +} // namespace ThreadMgr ThreadMgr::single_instance; diff --git a/library/src/utility/debug.h b/library/src/utility/debug.h index 93f66400..b0635547 100644 --- a/library/src/utility/debug.h +++ b/library/src/utility/debug.h @@ -53,6 +53,7 @@ /// @brief Enable AB search statistics (node counts, timing, etc.). /// Records alpha-beta search performance metrics for optimization analysis. +/// Enable via Bazel: --define=ab_stats=true // #define DDS_AB_STATS #define DDS_AB_STATS_PREFIX "ABstats" @@ -123,18 +124,4 @@ #endif #endif -/// @name Performance Counters -/// @brief Statistics counters for profiling and analysis. -/// @{ - -/// @brief Number of available counter slots for performance tracking. -constexpr int COUNTER_SLOTS = 200; - -/// @brief Global array of performance counters. -/// Each thread may use these counters for statistics collection. -/// Size: COUNTER_SLOTS (200) entries. -extern long long counter[COUNTER_SLOTS]; - -/// @} - /// @} diff --git a/library/tests/BUILD.bazel b/library/tests/BUILD.bazel index 0781deee..af1d80a7 100644 --- a/library/tests/BUILD.bazel +++ b/library/tests/BUILD.bazel @@ -7,7 +7,6 @@ filegroup( srcs = glob( ["*.cpp", "*.hpp"], exclude = [ - "itest.cpp", "context_equivalence.cpp", "calc_par_test.cpp", # Uses GoogleTest, compiled separately ], diff --git a/library/tests/args.cpp b/library/tests/args.cpp index 8c4c6c44..e83e4635 100644 --- a/library/tests/args.cpp +++ b/library/tests/args.cpp @@ -89,10 +89,9 @@ void usage( "-s, --solver One of: solve, calc, play, par, dealerpar.\n" << " (Default: solve)\n" << "\n" << - "-n, --numthr n Maximum number of threads (legacy option).\n" << - " (Default: 0 uses DDS/library defaults; when using\n" << - " the modern SolverContext API, prefer configuring\n" << - " threads via SolverConfig instead of this option.)\n" << + "-n, --numthr n Worker threads for solve/calc/play batches.\n" << + " 0 = auto (hardware concurrency), 1 = sequential.\n" << + " (Default: 0)\n" << "\n" << "-m, --memory n Total DDS memory size in MB (legacy option).\n" << " (Default: 0 uses DDS/library defaults; when using\n" << diff --git a/library/tests/args.hpp b/library/tests/args.hpp index d8eeea68..fa341a1f 100644 --- a/library/tests/args.hpp +++ b/library/tests/args.hpp @@ -13,7 +13,7 @@ /// @brief Command-line argument parsing for test utilities. /// /// Provides functions to parse and validate command-line options -/// for test driver programs (dtest, itest). Options include: +/// for the dtest driver program. Options include: /// - Input file specification /// - Solver type selection (solve, calc, play, par, dealer_par) /// - Number of threads diff --git a/library/tests/dtest.cpp b/library/tests/dtest.cpp index d4640cdd..d26abdfa 100644 --- a/library/tests/dtest.cpp +++ b/library/tests/dtest.cpp @@ -28,11 +28,15 @@ int main(int argc, char * argv[]) { read_args(argc, argv); - SetResources(options.memory_mb_, options.num_threads_); + SetResources(options.memory_mb_, 0); DDSInfo info; GetDDSInfo(&info); cout << info.systemString << endl; + if (options.num_threads_ == 0) + cout << "dtest worker threads: auto\n"; + else + cout << "dtest worker threads: " << options.num_threads_ << "\n"; real_main(argc, argv); diff --git a/library/tests/dtest_parallel.cpp b/library/tests/dtest_parallel.cpp new file mode 100644 index 00000000..778bf26f --- /dev/null +++ b/library/tests/dtest_parallel.cpp @@ -0,0 +1,95 @@ +/* + DDS, a bridge double dummy solver. + + Copyright (C) 2006-2014 by Bo Haglund / + 2014-2018 by Bo Haglund & Soren Hein. + + See LICENSE and README. +*/ + +#include "dtest_parallel.hpp" + +#include +#include +#include +#include + +#include + + +int dtest_effective_threads(const int requested, const int workload) +{ + if (workload <= 1) + return 1; + + const unsigned hw = std::thread::hardware_concurrency(); + const int auto_count = hw > 0 ? static_cast(hw) : 1; + + int n = requested > 0 ? requested : auto_count; + n = std::max(1, std::min(n, workload)); + return n; +} + + +int dtest_run_parallel( + const int count, + const int requested_threads, + const std::function & body) +{ + if (count <= 0) + return RETURN_NO_FAULT; + + const int nthreads = dtest_effective_threads(requested_threads, count); + if (nthreads <= 1) + { + for (int i = 0; i < count; ++i) + { + const int rc = body(i); + if (rc != RETURN_NO_FAULT) + return rc; + } + return RETURN_NO_FAULT; + } + + std::atomic next{0}; + std::atomic first_error{0}; + + auto worker = [&] { + for (;;) + { + const int i = next.fetch_add(1, std::memory_order_relaxed); + if (i >= count || first_error.load(std::memory_order_relaxed) != 0) + break; + + const int rc = body(i); + if (rc != RETURN_NO_FAULT) + { + int expected = 0; + first_error.compare_exchange_strong( + expected, rc, std::memory_order_relaxed); + break; + } + } + }; + + std::vector threads; + threads.reserve(static_cast(nthreads)); + try + { + for (int t = 0; t < nthreads; ++t) + threads.emplace_back(worker); + } + catch (...) + { + for (auto & th : threads) + if (th.joinable()) + th.join(); + throw; + } + + for (auto & th : threads) + th.join(); + + const int err = first_error.load(std::memory_order_relaxed); + return err != 0 ? err : RETURN_NO_FAULT; +} diff --git a/library/tests/dtest_parallel.hpp b/library/tests/dtest_parallel.hpp new file mode 100644 index 00000000..0e88414a --- /dev/null +++ b/library/tests/dtest_parallel.hpp @@ -0,0 +1,28 @@ +/* + DDS, a bridge double dummy solver. + + Copyright (C) 2006-2014 by Bo Haglund / + 2014-2018 by Bo Haglund & Soren Hein. + + See LICENSE and README. +*/ + +#pragma once + +#include + +/// Resolve the worker thread count for a dtest batch. +/// +/// @param requested Thread count from -n (0 = auto from hardware). +/// @param workload Number of independent items in the batch. +/// @return Thread count in [1, workload]. +int dtest_effective_threads(int requested, int workload); + +/// Run @p body for each index in [0, count) using up to @p requested_threads workers. +/// +/// @p body must return RETURN_NO_FAULT (1) on success. +/// @return First non-success code from @p body, or RETURN_NO_FAULT. +int dtest_run_parallel( + int count, + int requested_threads, + const std::function & body); diff --git a/library/tests/heuristic_sorting/minimal_weight_test.cpp b/library/tests/heuristic_sorting/minimal_weight_test.cpp index 465d0297..76ecb6bc 100644 --- a/library/tests/heuristic_sorting/minimal_weight_test.cpp +++ b/library/tests/heuristic_sorting/minimal_weight_test.cpp @@ -17,12 +17,6 @@ */ class MinimalWeightTest : public ::testing::Test { -protected: - void SetUp() override - { - // Initialize the DDS system - SetMaxThreads(0); - } }; TEST_F(MinimalWeightTest, BasicWeightAllocCall) { diff --git a/library/tests/itest.cpp b/library/tests/itest.cpp deleted file mode 100644 index 8e63b9ec..00000000 --- a/library/tests/itest.cpp +++ /dev/null @@ -1,47 +0,0 @@ -/* - DDS, a bridge double dummy solver. - - Copyright (C) 2006-2014 by Bo Haglund / - 2014-2018 by Bo Haglund & Soren Hein. - - See LICENSE and README. -*/ - - -#include -#include -#include - -#include -#include "testcommon.hpp" -#include "args.hpp" -#include "cst.hpp" - -using std::cout; -using std::endl; -using std::setw; - -OptionsType options; - - -int main(int argc, char * argv[]) -{ - read_args(argc, argv); - - SetResources(options.memory_mb_, options.num_threads_); - - DDSInfo info; - GetDDSInfo(&info); - cout << info.systemString << endl; - - realMain(argc, argv); - -#ifdef DDS_SCHEDULER - scheduler.PrintTiming(); -#endif - - FreeMemory(); - - exit(0); -} - diff --git a/library/tests/loop.cpp b/library/tests/loop.cpp index 10a75fcc..ecc6b4eb 100644 --- a/library/tests/loop.cpp +++ b/library/tests/loop.cpp @@ -16,6 +16,10 @@ #include "TestTimer.hpp" #include "compare.hpp" #include "print.hpp" +#include + +#include "cst.hpp" +#include "dtest_parallel.hpp" using std::cout; using std::endl; @@ -26,6 +30,7 @@ using std::right; #define BATCHTIMES extern TestTimer timer; +extern OptionsType options; void loop_solve( @@ -57,7 +62,28 @@ void loop_solve( timer.start(count); int ret; - if ((ret = SolveAllChunks(bop, solvedbdp, 1)) != RETURN_NO_FAULT) + if (dtest_effective_threads(options.num_threads_, count) <= 1) + { + ret = SolveAllBoardsSeq(bop, solvedbdp); + } + else + { + solvedbdp->no_of_boards = count; + ret = dtest_run_parallel(count, options.num_threads_, + [&](const int j) -> int { + FutureTricks fut; + const int res = SolveBoardPBN( + bop->deals[j], bop->target[j], bop->solutions[j], bop->mode[j], + &fut, 0); + if (res == RETURN_NO_FAULT) + { + solvedbdp->solved_board[j] = fut; + return RETURN_NO_FAULT; + } + return res; + }); + } + if (ret != RETURN_NO_FAULT) { cout << "loop_solve: i " << i << ", return " << ret << "\n"; exit(0); @@ -113,9 +139,8 @@ bool loop_calc( strcpy(dealsp->deals[j].cards, deal_list[i+j].remainCards); timer.start(count); - int ret; - if ((ret = CalcAllTablesPBN(dealsp, -1, filter, resp, parp)) - != RETURN_NO_FAULT) + const int ret = CalcAllTablesPBN(dealsp, -1, filter, resp, parp); + if (ret != RETURN_NO_FAULT) { cout << "loop_calc: i " << i << ", return " << ret << "\n"; exit(0); @@ -270,8 +295,20 @@ bool loop_play( timer.start(count); int ret; - if ((ret = AnalyseAllPlaysPBN(bop, playsp, solvedplp, 1)) - != RETURN_NO_FAULT) + if (dtest_effective_threads(options.num_threads_, count) <= 1) + { + ret = AnalyseAllPlaysPBN(bop, playsp, solvedplp, 1); + } + else + { + solvedplp->no_of_boards = count; + ret = dtest_run_parallel(count, options.num_threads_, + [&](const int j) -> int { + return AnalysePlayPBN( + bop->deals[j], playsp->plays[j], &solvedplp->solved[j], 0); + }); + } + if (ret != RETURN_NO_FAULT) { printf("loop_play i %i: Return %d\n", i, ret); cout << "loop_play: i " << i << ": " << "return " << ret << "\n"; diff --git a/library/tests/solve_board/analyse_play_consistency.cpp b/library/tests/solve_board/analyse_play_consistency.cpp index 20f73303..abb5c4aa 100644 --- a/library/tests/solve_board/analyse_play_consistency.cpp +++ b/library/tests/solve_board/analyse_play_consistency.cpp @@ -251,8 +251,6 @@ auto hand_from_holdings(const std::array& suits) -> std::vector< class AnalysePlayConsistency : public ::testing::Test { -protected: - void SetUp() override { SetMaxThreads(0); } }; // The exact deal from dds-bridge/dds issue #156. diff --git a/library/tests/solve_board/trick_three_bug.cpp b/library/tests/solve_board/trick_three_bug.cpp index c96c746b..2e565237 100644 --- a/library/tests/solve_board/trick_three_bug.cpp +++ b/library/tests/solve_board/trick_three_bug.cpp @@ -35,8 +35,6 @@ inline auto dds_max(FutureTricks const & fut) -> size_t /// @details Reproduces the original bug scenario and validates the fix. TEST_F(TrickThreeBugTests, test_declarer_makes_nine_tricks) { - SetMaxThreads(0); - const int target = 0; const int solutions = 3; const int mode = 0; diff --git a/library/tests/system/BUILD.bazel b/library/tests/system/BUILD.bazel index 6d429426..5c172fd6 100644 --- a/library/tests/system/BUILD.bazel +++ b/library/tests/system/BUILD.bazel @@ -59,6 +59,30 @@ cc_test( ], ) +# Worker-count helper unit test +cc_test( + name = "worker_count_test", + size = "small", + srcs = ["worker_count_test.cpp"], + deps = [ + "//library/src:testable_dds", + "//library/src/api:api_definitions", + "@googletest//:gtest_main", + ], +) + +# max_threads override equivalence + rename/alias initialisation test +cc_test( + name = "max_threads_equivalence_test", + size = "small", + srcs = ["max_threads_equivalence_test.cpp"], + deps = [ + "//library/src:testable_dds", + "//library/src/api:api_definitions", + "@googletest//:gtest_main", + ], +) + # Utilities feature flags tests cc_test( name = "utilities_feature_flags_test", diff --git a/library/tests/system/context_equivalence_test.cpp b/library/tests/system/context_equivalence_test.cpp index a63b5a96..fe586b29 100644 --- a/library/tests/system/context_equivalence_test.cpp +++ b/library/tests/system/context_equivalence_test.cpp @@ -58,7 +58,7 @@ static DdTableDeal make_known_deal() TEST(SystemContextEquivalence, LegacyVsContextReturnCode) { // Ensure DDS system and thread-local memory are initialized - SetMaxThreads(1); + InitializeStaticMemory(); const int thr = 0; FutureTricks ft_legacy{}; FutureTricks ft_ctx{}; diff --git a/library/tests/system/context_tt_facade_test.cpp b/library/tests/system/context_tt_facade_test.cpp index b9c6076a..0b6f9fac 100644 --- a/library/tests/system/context_tt_facade_test.cpp +++ b/library/tests/system/context_tt_facade_test.cpp @@ -15,7 +15,7 @@ extern Memory memory; TEST(SystemContextTTFacades, ResetAndResizeAreNoopsWithoutTT) { - SetMaxThreads(1); + InitializeStaticMemory(); // Some environments may compute 0 allowable threads (e.g., macOS sandbox), // so ensure we have at least one thread allocated for the test. if (memory.NumThreads() == 0) @@ -34,7 +34,7 @@ TEST(SystemContextTTFacades, ResetAndResizeAreNoopsWithoutTT) TEST(SystemContextTTFacades, ResizeCreatesWhenExisting) { - SetMaxThreads(1); + InitializeStaticMemory(); // Ensure at least one thread exists; fall back to a small thread config. if (memory.NumThreads() == 0) memory.Resize(1, DDS_TT_SMALL, THREADMEM_SMALL_DEF_MB, THREADMEM_SMALL_MAX_MB); @@ -51,7 +51,7 @@ TEST(SystemContextTTFacades, ResizeCreatesWhenExisting) TEST(SystemContextTTFacades, Lifecycle_LookupAddClearDispose) { - SetMaxThreads(1); + InitializeStaticMemory(); if (memory.NumThreads() == 0) memory.Resize(1, DDS_TT_SMALL, THREADMEM_SMALL_DEF_MB, THREADMEM_SMALL_MAX_MB); diff --git a/library/tests/system/max_threads_equivalence_test.cpp b/library/tests/system/max_threads_equivalence_test.cpp new file mode 100644 index 00000000..52e48b9a --- /dev/null +++ b/library/tests/system/max_threads_equivalence_test.cpp @@ -0,0 +1,132 @@ +/// @file max_threads_equivalence_test.cpp +/// @brief Tests that the *N batch APIs honour maxThreads and stay equivalent to +/// the auto path, plus that the rename/alias both initialise the library. + +#include +#include +#include + +#include +#include + +namespace +{ + +// Known deal from examples/hands.cpp (hand 0), as used by context_equivalence_test. +// PBN: N:QJ6.K652.J85.T98 873.J97.AT764.Q4 K5.T83.KQ9.A7652 AT942.AQ4.32.KJ3 +DdTableDeal make_known_deal() +{ + DdTableDeal deal{}; + deal.cards[0][0] = 0x1800 | 0x0040; + deal.cards[0][1] = 0x2000 | 0x0060 | 0x0004; + deal.cards[0][2] = 0x0800 | 0x0100 | 0x0020; + deal.cards[0][3] = 0x0400 | 0x0200 | 0x0100; + deal.cards[1][0] = 0x0100 | 0x0080 | 0x0008; + deal.cards[1][1] = 0x0800 | 0x0200 | 0x0080; + deal.cards[1][2] = 0x4000 | 0x0400 | 0x0080 | 0x0040 | 0x0010; + deal.cards[1][3] = 0x1000 | 0x0010; + deal.cards[2][0] = 0x2000 | 0x0020; + deal.cards[2][1] = 0x0400 | 0x0100 | 0x0008; + deal.cards[2][2] = 0x2000 | 0x1000 | 0x0200; + deal.cards[2][3] = 0x4000 | 0x0080 | 0x0040 | 0x0020 | 0x0004; + deal.cards[3][0] = 0x4000 | 0x0400 | 0x0200 | 0x0010 | 0x0004; + deal.cards[3][1] = 0x4000 | 0x1000 | 0x0010; + deal.cards[3][2] = 0x0008 | 0x0004; + deal.cards[3][3] = 0x2000 | 0x0800 | 0x0008; + return deal; +} + +void expect_tables_equal(const DdTableResults& a, const DdTableResults& b) +{ + for (int strain = 0; strain < DDS_STRAINS; strain++) + for (int hand = 0; hand < DDS_HANDS; hand++) + EXPECT_EQ(a.res_table[strain][hand], b.res_table[strain][hand]) + << "Mismatch at strain=" << strain << " hand=" << hand; +} + +} // namespace + +// CalcDDtableN with maxThreads=1 must match the auto CalcDDtable. +TEST(MaxThreadsEquivalence, CalcDDtableNMatchesAuto) +{ + InitializeStaticMemory(); + DdTableDeal deal = make_known_deal(); + + DdTableResults table_auto{}; + ASSERT_EQ(CalcDDtable(deal, &table_auto), RETURN_NO_FAULT); + + DdTableResults table_one{}; + ASSERT_EQ(CalcDDtableN(deal, &table_one, /*maxThreads=*/1), RETURN_NO_FAULT); + expect_tables_equal(table_auto, table_one); + + if (std::thread::hardware_concurrency() > 2) + { + DdTableResults table_two{}; + ASSERT_EQ(CalcDDtableN(deal, &table_two, /*maxThreads=*/2), RETURN_NO_FAULT); + expect_tables_equal(table_auto, table_two); + } +} + +// SolveAllBoardsBinN with maxThreads=1 must match the auto SolveAllBoardsBin. +TEST(MaxThreadsEquivalence, SolveAllBoardsBinNMatchesAuto) +{ + InitializeStaticMemory(); + DdTableDeal table_deal = make_known_deal(); + + // Solve all five strains as separate boards. + Boards bo{}; + bo.no_of_boards = DDS_STRAINS; + for (int tr = 0; tr < DDS_STRAINS; tr++) + { + Deal dl{}; + for (int h = 0; h < DDS_HANDS; h++) + for (int s = 0; s < DDS_SUITS; s++) + dl.remainCards[h][s] = table_deal.cards[h][s]; + dl.trump = tr; + dl.first = 0; + bo.deals[tr] = dl; + bo.target[tr] = -1; + bo.solutions[tr] = 1; + bo.mode[tr] = 1; + } + + SolvedBoards solved_auto{}; + ASSERT_EQ(SolveAllBoardsBin(&bo, &solved_auto), RETURN_NO_FAULT); + + SolvedBoards solved_one{}; + ASSERT_EQ(SolveAllBoardsBinN(&bo, &solved_one, /*maxThreads=*/1), RETURN_NO_FAULT); + + ASSERT_EQ(solved_auto.no_of_boards, solved_one.no_of_boards); + for (int b = 0; b < solved_auto.no_of_boards; b++) + { + const FutureTricks& fa = solved_auto.solved_board[b]; + const FutureTricks& fo = solved_one.solved_board[b]; + ASSERT_EQ(fa.cards, fo.cards) << "card count differs at board=" << b; + // Only the first `cards` entries are meaningful; the tail is uninitialised. + for (int c = 0; c < fa.cards; c++) + { + EXPECT_EQ(fa.suit[c], fo.suit[c]) << "suit at board=" << b << " c=" << c; + EXPECT_EQ(fa.rank[c], fo.rank[c]) << "rank at board=" << b << " c=" << c; + EXPECT_EQ(fa.equals[c], fo.equals[c]) << "equals at board=" << b << " c=" << c; + EXPECT_EQ(fa.score[c], fo.score[c]) << "score at board=" << b << " c=" << c; + } + } +} + +// InitializeStaticMemory leaves the library usable for a subsequent solve. +TEST(MaxThreadsEquivalence, InitializeStaticMemoryThenSolve) +{ + InitializeStaticMemory(); + DdTableDeal deal = make_known_deal(); + DdTableResults table{}; + EXPECT_EQ(CalcDDtable(deal, &table), RETURN_NO_FAULT); +} + +// The deprecated SetMaxThreads alias still initialises the library. +TEST(MaxThreadsEquivalence, DeprecatedSetMaxThreadsAliasStillWorks) +{ + SetMaxThreads(1); + DdTableDeal deal = make_known_deal(); + DdTableResults table{}; + EXPECT_EQ(CalcDDtable(deal, &table), RETURN_NO_FAULT); +} diff --git a/library/tests/system/worker_count_test.cpp b/library/tests/system/worker_count_test.cpp new file mode 100644 index 00000000..dfe6aecd --- /dev/null +++ b/library/tests/system/worker_count_test.cpp @@ -0,0 +1,49 @@ +/// @file worker_count_test.cpp +/// @brief Unit tests for resolve_worker_count (batch worker-count resolution). +/// +/// Validates the shared helper that maps an optional max_threads cap and a work +/// item count onto the number of worker threads to use. + +#include +#include +#include + +#include + +namespace +{ + +// Mirror the helper's "auto" computation so the test is host-independent. +int auto_workers(const int count) +{ + const unsigned hw = std::thread::hardware_concurrency(); + const int hw_or_1 = hw > 0 ? static_cast(hw) : 1; + return std::max(1, std::min(hw_or_1, count)); +} + +} // namespace + +TEST(ResolveWorkerCount, NonPositiveCapUsesAuto) +{ + const int count = 8; + EXPECT_EQ(resolve_worker_count(0, count), auto_workers(count)); + EXPECT_EQ(resolve_worker_count(-4, count), auto_workers(count)); +} + +TEST(ResolveWorkerCount, CapLargerThanCountClampsToCount) +{ + EXPECT_EQ(resolve_worker_count(1000, 5), 5); +} + +TEST(ResolveWorkerCount, CapSmallerThanCountAndHardwareIsHonoured) +{ + // A cap of 1 is always <= count and <= hardware_concurrency. + EXPECT_EQ(resolve_worker_count(1, 8), 1); +} + +TEST(ResolveWorkerCount, SingleItemAlwaysOneWorker) +{ + EXPECT_EQ(resolve_worker_count(0, 1), 1); + EXPECT_EQ(resolve_worker_count(16, 1), 1); + EXPECT_EQ(resolve_worker_count(1, 1), 1); +} diff --git a/python/BUILD.bazel b/python/BUILD.bazel index fc003335..df169761 100644 --- a/python/BUILD.bazel +++ b/python/BUILD.bazel @@ -158,6 +158,14 @@ py_test( ], ) +py_test( + name = "convert_pbn_test", + size = "small", + main = "tests/test_convert_pbn.py", + srcs = ["tests/test_convert_pbn.py"], + deps = ["//python/examples:dd_table_for_deal_lib"], +) + py_wheel( name = "dds3_wheel", distribution = "dds3", diff --git a/python/dds3/__init__.py b/python/dds3/__init__.py index 8da2bf96..dda8d813 100644 --- a/python/dds3/__init__.py +++ b/python/dds3/__init__.py @@ -7,6 +7,7 @@ from ._dds3 import calc_par from ._dds3 import calc_par_from_table from ._dds3 import dealer_par + from ._dds3 import initialise_static_memory from ._dds3 import module_name from ._dds3 import par from ._dds3 import set_max_threads @@ -25,6 +26,7 @@ from _dds3 import calc_par from _dds3 import calc_par_from_table from _dds3 import dealer_par + from _dds3 import initialise_static_memory from _dds3 import module_name from _dds3 import par from _dds3 import set_max_threads @@ -43,6 +45,7 @@ "calc_par", "calc_par_from_table", "dealer_par", + "initialise_static_memory", "module_name", "par", "set_max_threads", diff --git a/python/examples/BUILD.bazel b/python/examples/BUILD.bazel new file mode 100644 index 00000000..6e09bbed --- /dev/null +++ b/python/examples/BUILD.bazel @@ -0,0 +1,19 @@ +load("@rules_python//python:defs.bzl", "py_binary", "py_library") + +py_library( + name = "dd_table_for_deal_lib", + srcs = ["dd_table_for_deal.py"], + imports = ["."], + deps = ["//python:dds3_lib"], + visibility = [ + "//python:__pkg__", + "//python:__subpackages__", + ], +) + +py_binary( + name = "dd_table_for_deal", + srcs = ["dd_table_for_deal.py"], + main = "dd_table_for_deal.py", + deps = [":dd_table_for_deal_lib"], +) diff --git a/python/examples/README.md b/python/examples/README.md new file mode 100644 index 00000000..eb7fe972 --- /dev/null +++ b/python/examples/README.md @@ -0,0 +1,35 @@ +**Python hints** + +See also `python/tests/README.md` and `docs/python_interface.md` + +*Run via Bazel* + +```bash +bazel run //python/examples:dd_table_for_deal "N:73.QJT.AQ54.T752 QT6.876.KJ9.AQ84 5.A95432.7632.K6 AKJ9842.K.T8.J93" +``` + +or + +```bash +bazel run //python/examples:dd_table_for_deal hands/example.pbn +``` + + +*Run without Bazel* + +To run the examples directly, without Bazel, first build the native module and set `PYTHONPATH`: + + bazel build //python:_dds3 + export PYTHONPATH=python:bazel-bin/python + +Direct commands: + +```bash +python python/examples/dd_table_for_deal.py "N:73.QJT.AQ54.T752 QT6.876.KJ9.AQ84 5.A95432.7632.K6 AKJ9842.K.T8.J93" +``` + +or + +```bash +python python/examples/dd_table_for_deal.py hands/example.pbn +``` diff --git a/python/examples/dd_table_for_deal.py b/python/examples/dd_table_for_deal.py new file mode 100644 index 00000000..1498841d --- /dev/null +++ b/python/examples/dd_table_for_deal.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +"""Print the double-dummy table for a deal from PBN on the command line or a file. + +Python counterpart to examples/dd_table_for_deal.cpp. +""" + +from __future__ import annotations + +import os +import re +import sys +from pathlib import Path + +from dds3 import calc_all_tables_pbn + +PBN_FILE_MAX = 8192 + +# res_table[strain][hand]: strain 0-3 = S,H,D,C; 4 = NT. Columns match C++ print_table. +_STRAIN_ROWS = (("NT", 4), ("S", 0), ("H", 1), ("D", 2), ("C", 3)) +_HAND_COLUMNS = (("North", 0), ("South", 2), ("East", 1), ("West", 3)) + +_DDS_FULL_LINE = 80 +_DDS_HAND_OFFSET = 12 +_DDS_HAND_LINES = 12 + +_BIT_MAP_RANK = [ + 0x0000, 0x0000, 0x0001, 0x0002, 0x0004, 0x0008, 0x0010, 0x0020, + 0x0040, 0x0080, 0x0100, 0x0200, 0x0400, 0x0800, 0x1000, 0x2000, +] +_CARD_RANK_CHARS = "xx23456789TJQKA-" + +_DEAL_TAG_RE = re.compile(r'\[Deal\s*"([^"]*)"', re.IGNORECASE) + + +def _read_pbn_stream(stream) -> str | None: + data = stream.read(PBN_FILE_MAX - 1) + if not data: + return None + return data if isinstance(data, str) else data.decode("utf-8", errors="replace") + + +def _read_pbn_file(path: str) -> str | None: + candidates = [Path(path)] + workspace = os.environ.get("BUILD_WORKSPACE_DIRECTORY") + if workspace is not None: + candidates.append(Path(workspace) / path) + for candidate in candidates: + try: + return candidate.read_text(encoding="utf-8", errors="replace")[ + : PBN_FILE_MAX - 1 + ] + except OSError: + continue + return None + + +def _extract_deal_tag(text: str) -> str | None: + match = _DEAL_TAG_RE.search(text) + return match.group(1) if match else None + + +def _load_deal(arg: str) -> str: + if arg == "-": + text = _read_pbn_stream(sys.stdin) + if text is None: + raise ValueError("No PBN input on stdin") + source = "stdin" + else: + text = _read_pbn_file(arg) + source = arg if text is not None else None + + if source is not None: + deal = _extract_deal_tag(text) + if deal is None: + raise ValueError(f'No [Deal "..."] tag found in {source}') + return deal + + if len(arg) >= PBN_FILE_MAX: + raise ValueError(f"PBN deal too long (max {PBN_FILE_MAX - 1} characters)") + return arg + + +def _print_usage(prog: str) -> None: + print( + f"Usage: {prog} \n" + f" {prog} -h | --help\n" + "\n" + "Calculate double-dummy tricks for all strains and leads.\n" + "\n" + "Arguments:\n" + " DDS PBN deal string, or path to a .pbn file\n" + "\n" + 'If stdin is not a terminal, PBN is read from stdin (uses [Deal "..."]).\n' + "\n" + "Examples:\n" + f' {prog} "N:73.QJT.AQ54.T752 QT6.876.KJ9.AQ84 ' + f'5.A95432.7632.K6 AKJ9842.K.T8.J93"\n' + f" {prog} hands/example.pbn\n" + f" {prog} < hands/example.pbn\n", + file=sys.stderr, + ) + + +def _is_card(ch: str) -> int: + ch = ch.upper() + ranks = "23456789TJQKA" + return ranks.index(ch) + 2 if ch in ranks else 0 + + +def _convert_pbn(pbn_deal: str) -> list[list[int]]: + """Match examples/hands.cpp convert_pbn (4 hands x 4 suits bitmasks).""" + remain = [[0] * 4 for _ in range(4)] + bp = 0 + while ( + bp < 3 + and bp < len(pbn_deal) + and pbn_deal[bp] not in "NWESnwes" + ): + bp += 1 + if bp >= 3 or bp >= len(pbn_deal): + return remain + + first = {"N": 0, "E": 1, "S": 2, "W": 3}[pbn_deal[bp].upper()] + bp += 2 + hand_rel_first = 0 + suit_in_hand = 0 + + while bp < 80 and bp < len(pbn_deal): + ch = pbn_deal[bp] + card = _is_card(ch) + if card: + if first == 0: + hand = hand_rel_first + elif first == 1: + hand = 1 if hand_rel_first == 0 else 0 if hand_rel_first == 3 else hand_rel_first + 1 + elif first == 2: + hand = 2 if hand_rel_first == 0 else 3 if hand_rel_first == 1 else hand_rel_first - 2 + else: + hand = 3 if hand_rel_first == 0 else hand_rel_first - 1 + remain[hand][suit_in_hand] |= _BIT_MAP_RANK[card] << 2 + elif ch == ".": + suit_in_hand += 1 + elif ch == " ": + hand_rel_first += 1 + suit_in_hand = 0 + bp += 1 + return remain + + +def _print_pbn_hand(title: str, pbn_deal: str) -> None: + """Match examples/hands.cpp print_pbn_hand / print_hand.""" + remain_cards = _convert_pbn(pbn_deal) + text = [[" "] * _DDS_FULL_LINE for _ in range(_DDS_HAND_LINES)] + row_ends = [_DDS_FULL_LINE] * _DDS_HAND_LINES + + for h in range(4): + if h == 0: + offset, line = _DDS_HAND_OFFSET, 0 + elif h == 1: + offset, line = 2 * _DDS_HAND_OFFSET, 4 + elif h == 2: + offset, line = _DDS_HAND_OFFSET, 8 + else: + offset, line = 0, 4 + + for s in range(4): + row = line + s + c = offset + for r in range(14, 1, -1): + if (remain_cards[h][s] >> 2) & _BIT_MAP_RANK[r]: + text[row][c] = _CARD_RANK_CHARS[r] + c += 1 + if c == offset: + text[row][c] = "-" + c += 1 + if h != 3: + row_ends[row] = c + + sys.stdout.write(title) + dash_len = max(0, len(title) - 1) + print("-" * dash_len) + for i in range(_DDS_HAND_LINES): + print("".join(text[i][: row_ends[i]])) + print("\n") + + +def _print_table(res_table: list[list[int]]) -> None: + """Match examples/hands.cpp print_table (%5s %-5s ... / %5c %5d ...).""" + print(f"{'':>5} {'North':<5} {'South':<5} {'East':<5} {'West':<5}") + + _, nt_strain = _STRAIN_ROWS[0] + print( + f"{'NT':>5} " + f"{res_table[nt_strain][_HAND_COLUMNS[0][1]]:5d} " + f"{res_table[nt_strain][_HAND_COLUMNS[1][1]]:5d} " + f"{res_table[nt_strain][_HAND_COLUMNS[2][1]]:5d} " + f"{res_table[nt_strain][_HAND_COLUMNS[3][1]]:5d}" + ) + + for label, strain in _STRAIN_ROWS[1:]: + print( + f"{label:>5} " + f"{res_table[strain][_HAND_COLUMNS[0][1]]:5d} " + f"{res_table[strain][_HAND_COLUMNS[1][1]]:5d} " + f"{res_table[strain][_HAND_COLUMNS[2][1]]:5d} " + f"{res_table[strain][_HAND_COLUMNS[3][1]]:5d}" + ) + print() + + +def main(argv: list[str] | None = None) -> int: + argv = list(sys.argv if argv is None else argv) + prog = Path(argv[0]).name + + if len(argv) == 2: + if argv[1] in ("-h", "--help"): + _print_usage(prog) + return 0 + input_arg = argv[1] + elif len(argv) == 1 and not sys.stdin.isatty(): + input_arg = "-" + else: + _print_usage(prog) + return 1 + + try: + pbn_deal = _load_deal(input_arg) + except ValueError as exc: + print(exc, file=sys.stderr) + return 1 + + try: + result = calc_all_tables_pbn([pbn_deal]) + except (ValueError, RuntimeError) as exc: + print(f"DDS error: {exc}", file=sys.stderr) + return 1 + + tables = result.get("tables") + if not tables: + print("DDS error: no table returned", file=sys.stderr) + return 1 + + res_table = tables[0]["res_table"] + _print_pbn_hand("dd_table_for_deal:\n", pbn_deal) + _print_table(res_table) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/python/src/bindings.cpp b/python/src/bindings.cpp index be83081a..6240e4cb 100644 --- a/python/src/bindings.cpp +++ b/python/src/bindings.cpp @@ -598,30 +598,51 @@ auto register_calc_par_bindings(py::module_& module) -> void auto register_analysis_bindings(py::module_& module) -> void { - // set_max_threads: configure the worker-thread count used by the batch - // APIs (solve_all_boards_*, analyse_all_plays_pbn). 0 = auto-configure. + // initialise_static_memory: allocate the solver's static memory pools and + // perform one-time lookup-table initialisation. This does NOT control the + // worker-thread count; use solve_all_boards_* (which parallelise across the + // machine's hardware threads automatically) or one SolverContext per worker + // thread for per-board concurrency. + module.def( + "initialise_static_memory", + []() { + py::gil_scoped_release release; + InitializeStaticMemory(); + }, + "Initialise the solver's static memory.\n\n" + "Allocates the transposition-table memory pools and performs one-time\n" + "lookup-table initialisation. This does NOT control the number of worker\n" + "threads: solve_all_boards_* parallelise across the machine's hardware\n" + "threads automatically, and for per-board concurrency from Python you\n" + "create one SolverContext per worker thread and pass it to solve_board /\n" + "solve_board_pbn."); + + // set_max_threads: DEPRECATED alias of initialise_static_memory. The thread + // count argument is ignored; retained only for backward compatibility. module.def( "set_max_threads", [](const int user_threads) { - if (user_threads < 0) { - throw py::value_error( - "user_threads has invalid value " + std::to_string(user_threads) + - " (expected >= 0; 0 = auto)"); - } + if (PyErr_WarnEx( + PyExc_DeprecationWarning, + "set_max_threads() is deprecated; use initialise_static_memory(). " + "The user_threads argument is ignored.", + 1) != 0) { + throw py::error_already_set(); + } + py::gil_scoped_release release; SetMaxThreads(user_threads); }, py::arg("user_threads") = 0, - "Legacy thread-resource hook (wraps the deprecated SetMaxThreads C API).\n\n" + "DEPRECATED: use initialise_static_memory() instead.\n\n" + "Legacy thread-resource hook (wraps the deprecated SetMaxThreads C API,\n" + "now a thin alias of InitializeStaticMemory). Calling this emits a\n" + "DeprecationWarning.\n\n" "This does NOT control DDS's batch parallelism and is retained only for\n" - "backward compatibility. solve_all_boards_* already parallelise across the\n" - "machine's hardware threads automatically (see solve_boards_n); the value\n" - "passed here does not size that pool. analyse_all_plays_pbn currently runs\n" - "sequentially. For per-board concurrency from Python, create one\n" - "SolverContext per worker thread and pass it to solve_board / solve_board_pbn.\n\n" + "backward compatibility. analyse_all_plays_pbn currently runs sequentially. For\n" + "per-board concurrency from Python, create one SolverContext per worker\n" + "thread and pass it to solve_board / solve_board_pbn.\n\n" "Args:\n" - " user_threads (int, optional): Must be >= 0; 0 = auto. Default: 0\n\n" - "Raises:\n" - " ValueError: If user_threads < 0."); + " user_threads (int, optional): Ignored;\n\n"); // analyse_play_pbn: double-dummy trick count after each card of a played hand. module.def( diff --git a/python/tests/test_analyse.py b/python/tests/test_analyse.py index 1f589d2e..544c5b3e 100644 --- a/python/tests/test_analyse.py +++ b/python/tests/test_analyse.py @@ -61,11 +61,6 @@ def test_analyse_all_plays_missing_play(self) -> None: with self.assertRaises(KeyError): analyse_all_plays_pbn([{"remain_cards": DEAL}]) - def test_set_max_threads_rejects_negative(self) -> None: - with self.assertRaises(ValueError): - set_max_threads(-1) - set_max_threads(0) # 0 is valid (auto) - class TestDealerPar(unittest.TestCase): """Tests for dealer_par.""" diff --git a/python/tests/test_convert_pbn.py b/python/tests/test_convert_pbn.py new file mode 100644 index 00000000..905b9430 --- /dev/null +++ b/python/tests/test_convert_pbn.py @@ -0,0 +1,32 @@ +"""Tests for PBN-to-bitmask conversion in dd_table_for_deal.""" + +import unittest + +from dd_table_for_deal import _convert_pbn + +_EMPTY_REMAIN = [[0] * 4 for _ in range(4)] + +_EXAMPLE_DEAL = ( + "N:73.QJT.AQ54.T752 QT6.876.KJ9.AQ84 " + "5.A95432.7632.K6 AKJ9842.K.T8.J93" +) + + +class ConvertPbnTest(unittest.TestCase): + def test_short_deal_strings_return_empty_remain(self) -> None: + """Short or malformed PBN strings must not raise IndexError.""" + for deal in ("", "N", "N:", "12", "abc"): + with self.subTest(deal=deal): + self.assertEqual(_convert_pbn(deal), _EMPTY_REMAIN) + + def test_valid_deal_parses_card_bitmasks(self) -> None: + remain = _convert_pbn(_EXAMPLE_DEAL) + + # North's spades: 73 + self.assertEqual(remain[0][0], 0x0080 | 0x0008) + # North's hearts: QJT + self.assertEqual(remain[0][1], 0x1000 | 0x0800 | 0x0400) + + +if __name__ == "__main__": + unittest.main() diff --git a/solution/DDS.vcxproj b/solution/DDS.vcxproj index 25a2615b..b41de484 100644 --- a/solution/DDS.vcxproj +++ b/solution/DDS.vcxproj @@ -20,9 +20,6 @@ - - - StaticLibrary @@ -32,6 +29,8 @@ StaticLibrary false + + @@ -46,9 +45,9 @@ - - $(BuildDir)\bin\$(platform)\$(Configuration)\ - $(BuildDir)\int\$(platform)\$(Configuration)\$(ProjectName)\ + + ..\Build\bin\$(platform)\$(Configuration)\ + ..\Build\int\$(platform)\$(Configuration)\$(ProjectName)\ diff --git a/solution/Solution.slnx b/solution/Solution.slnx index 50b563dd..68626a69 100644 --- a/solution/Solution.slnx +++ b/solution/Solution.slnx @@ -6,59 +6,74 @@ - + + + + + + + - + + + + + + + + + + diff --git a/solution/analyse_all_plays_bin.vcxproj b/solution/analyse_all_plays_bin.vcxproj index 43f4b8a0..6d99d7d7 100644 --- a/solution/analyse_all_plays_bin.vcxproj +++ b/solution/analyse_all_plays_bin.vcxproj @@ -18,7 +18,6 @@ - @@ -29,8 +28,8 @@ - $(BuildDir)\bin\$(platform)\$(Configuration)\ - $(BuildDir)\int\$(platform)\$(Configuration)\$(ProjectName)\ + ..\Build\bin\$(platform)\$(Configuration)\ + ..\Build\int\$(platform)\$(Configuration)\$(ProjectName)\ diff --git a/solution/analyse_play_bin.vcxproj b/solution/analyse_play_bin.vcxproj index 084139b4..9d76ff4d 100644 --- a/solution/analyse_play_bin.vcxproj +++ b/solution/analyse_play_bin.vcxproj @@ -19,9 +19,6 @@ - - - @@ -32,9 +29,9 @@ - - $(BuildDir)\bin\$(platform)\$(Configuration)\ - $(BuildDir)\int\$(platform)\$(Configuration)\$(ProjectName)\ + + ..\Build\bin\$(platform)\$(Configuration)\ + ..\Build\int\$(platform)\$(Configuration)\$(ProjectName)\ diff --git a/solution/analyse_play_pbn.vcxproj b/solution/analyse_play_pbn.vcxproj index c64dee6f..d94e3dff 100644 --- a/solution/analyse_play_pbn.vcxproj +++ b/solution/analyse_play_pbn.vcxproj @@ -17,9 +17,7 @@ false - - - + @@ -30,9 +28,9 @@ - - $(BuildDir)\bin\$(platform)\$(Configuration)\ - $(BuildDir)\int\$(platform)\$(Configuration)\$(ProjectName)\ + + ..\Build\bin\$(platform)\$(Configuration)\ + ..\Build\int\$(platform)\$(Configuration)\$(ProjectName)\ diff --git a/solution/calc_all_tables.vcxproj b/solution/calc_all_tables.vcxproj index c1f9d949..fe9c3061 100644 --- a/solution/calc_all_tables.vcxproj +++ b/solution/calc_all_tables.vcxproj @@ -17,10 +17,7 @@ false - - - - + @@ -30,9 +27,9 @@ - - $(BuildDir)\bin\$(platform)\$(Configuration)\ - $(BuildDir)\int\$(platform)\$(Configuration)\$(ProjectName)\ + + ..\Build\bin\$(platform)\$(Configuration)\ + ..\Build\int\$(platform)\$(Configuration)\$(ProjectName)\ diff --git a/solution/calc_all_tables_pbn.vcxproj b/solution/calc_all_tables_pbn.vcxproj index 3347854b..edcc78bd 100644 --- a/solution/calc_all_tables_pbn.vcxproj +++ b/solution/calc_all_tables_pbn.vcxproj @@ -16,10 +16,7 @@ false - - - - + @@ -29,9 +26,9 @@ - - $(BuildDir)\bin\$(platform)\$(Configuration)\ - $(BuildDir)\int\$(platform)\$(Configuration)\$(ProjectName)\ + + ..\Build\bin\$(platform)\$(Configuration)\ + ..\Build\int\$(platform)\$(Configuration)\$(ProjectName)\ diff --git a/solution/calc_dd_table.vcxproj b/solution/calc_dd_table.vcxproj index 5b4f83ce..2ec6afe0 100644 --- a/solution/calc_dd_table.vcxproj +++ b/solution/calc_dd_table.vcxproj @@ -16,9 +16,7 @@ false - - - + @@ -28,9 +26,9 @@ - - $(BuildDir)\bin\$(platform)\$(Configuration)\ - $(BuildDir)\int\$(platform)\$(Configuration)\$(ProjectName)\ + + ..\Build\bin\$(platform)\$(Configuration)\ + ..\Build\int\$(platform)\$(Configuration)\$(ProjectName)\ diff --git a/solution/calc_par_context_example.vcxproj b/solution/calc_par_context_example.vcxproj index 4ca4320e..36c1c6e6 100644 --- a/solution/calc_par_context_example.vcxproj +++ b/solution/calc_par_context_example.vcxproj @@ -23,10 +23,6 @@ - - - @@ -46,10 +42,10 @@ - - $(BuildDir)\bin\$(platform)\$(Configuration)\ - $(BuildDir)\int\$(platform)\$(Configuration)\$(ProjectName)\ - + + ..\Build\bin\$(platform)\$(Configuration)\ + ..\Build\int\$(platform)\$(Configuration)\$(ProjectName)\ + diff --git a/solution/dds_native.vcxproj b/solution/dds_native.vcxproj new file mode 100644 index 00000000..1c4f7a36 --- /dev/null +++ b/solution/dds_native.vcxproj @@ -0,0 +1,149 @@ + + + + + Debug + x64 + + + Release + x64 + + + + 18.0 + true + {D38617A2-482A-1C00-7C88-02B17B693906} + NetCoreCProj + ddsnative + 10.0 + + + + DynamicLibrary + true + + + DynamicLibrary + false + + + + + + + + + + + + + + + + ..\Build\bin\$(platform)\$(Configuration)\ + ..\Build\int\$(platform)\$(Configuration)\$(ProjectName)\ + + + + _DEBUG;_WINDOWS;%(PreprocessorDefinitions) + ..\library\src;%(AdditionalIncludeDirectories) + stdcpp20 + + + $(IntDir)$(MSBuildProjectName).log + + + + + NDEBUG;_WINDOWS;%(PreprocessorDefinitions) + ..\library\src;%(AdditionalIncludeDirectories) + stdcpp20 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/solution/dds_native.vcxproj.filters b/solution/dds_native.vcxproj.filters new file mode 100644 index 00000000..74d08c55 --- /dev/null +++ b/solution/dds_native.vcxproj.filters @@ -0,0 +1,279 @@ + + + + + + + + {F1000000-0000-4000-8000-000000000001} + + + {F1000000-0000-4000-8000-000000000002} + + + {F1000000-0000-4000-8000-000000000003} + + + {F1000000-0000-4000-8000-000000000004} + + + {F1000000-0000-4000-8000-000000000005} + + + {F1000000-0000-4000-8000-000000000006} + + + {F1000000-0000-4000-8000-000000000007} + + + {F1000000-0000-4000-8000-000000000008} + + + {F1000000-0000-4000-8000-000000000009} + + + {F1000000-0000-4000-8000-00000000000A} + + + + + + + + library\src + + + library\src + + + library\src + + + library\src + + + library\src + + + library\src + + + library\src + + + library\src + + + library\src + + + library\src\heuristic_sorting + + + library\src + + + library\src + + + library\src\lookup_tables + + + library\src\moves + + + library\src + + + library\src + + + library\src + + + library\src + + + library\src\solver_context + + + library\src + + + library\src + + + library\src + + + library\src\system + + + library\src\system + + + library\src\system + + + library\src\system + + + library\src\system + + + library\src\system + + + library\src\system + + + library\src\system + + + library\src\system + + + library\src\system + + + library\src\trans_table + + + library\src\trans_table + + + library\src\utility + + + + + + + + library\src\api + + + library\src + + + library\src + + + library\src\api + + + library\src\api + + + library\src\api + + + library\src\api + + + library\src\api + + + library\src\api + + + library\src\api + + + library\src + + + library\src + + + library\src + + + library\src + + + library\src\heuristic_sorting + + + library\src\heuristic_sorting + + + library\src + + + library\src + + + library\src\lookup_tables + + + library\src\moves + + + library\src + + + library\src + + + library\src + + + library\src\solver_context + + + library\src + + + library\src + + + library\src\system + + + library\src\system + + + library\src\system + + + library\src\system + + + library\src\system + + + library\src\system + + + library\src\system + + + library\src\system + + + library\src\system + + + library\src\system + + + library\src\system + + + library\src\trans_table + + + library\src\trans_table + + + library\src\trans_table + + + library\src\utility + + + library\src\utility + + + \ No newline at end of file diff --git a/solution/dealer_par.vcxproj b/solution/dealer_par.vcxproj index 80c488db..c769e8e0 100644 --- a/solution/dealer_par.vcxproj +++ b/solution/dealer_par.vcxproj @@ -17,9 +17,7 @@ false - - - + @@ -30,9 +28,9 @@ - - $(BuildDir)\bin\$(platform)\$(Configuration)\ - $(BuildDir)\int\$(platform)\$(Configuration)\$(ProjectName)\ + + ..\Build\bin\$(platform)\$(Configuration)\ + ..\Build\int\$(platform)\$(Configuration)\$(ProjectName)\ diff --git a/solution/hands.vcxproj b/solution/hands.vcxproj index 1a743094..1192b6fc 100644 --- a/solution/hands.vcxproj +++ b/solution/hands.vcxproj @@ -16,8 +16,6 @@ false - - @@ -27,9 +25,9 @@ - - $(BuildDir)\bin\$(platform)\$(Configuration)\ - $(BuildDir)\int\$(platform)\$(Configuration)\$(ProjectName)\ + + ..\Build\bin\$(platform)\$(Configuration)\ + ..\Build\int\$(platform)\$(Configuration)\$(ProjectName)\ diff --git a/solution/migration_example.vcxproj b/solution/migration_example.vcxproj index dfcc6b32..9c6da4bf 100644 --- a/solution/migration_example.vcxproj +++ b/solution/migration_example.vcxproj @@ -16,9 +16,7 @@ false - - - + @@ -28,9 +26,9 @@ - - $(BuildDir)\bin\$(platform)\$(Configuration)\ - $(BuildDir)\int\$(platform)\$(Configuration)\$(ProjectName)\ + + ..\Build\bin\$(platform)\$(Configuration)\ + ..\Build\int\$(platform)\$(Configuration)\$(ProjectName)\ diff --git a/solution/par.vcxproj b/solution/par.vcxproj index a699bc92..62d2b6bb 100644 --- a/solution/par.vcxproj +++ b/solution/par.vcxproj @@ -16,10 +16,7 @@ false - - - - + @@ -29,9 +26,9 @@ - - $(BuildDir)\bin\$(platform)\$(Configuration)\ - $(BuildDir)\int\$(platform)\$(Configuration)\$(ProjectName)\ + + ..\Build\bin\$(platform)\$(Configuration)\ + ..\Build\int\$(platform)\$(Configuration)\$(ProjectName)\ diff --git a/solution/solve_all_boards.vcxproj b/solution/solve_all_boards.vcxproj index 238ca532..d373a9b2 100644 --- a/solution/solve_all_boards.vcxproj +++ b/solution/solve_all_boards.vcxproj @@ -16,9 +16,7 @@ false - - - + @@ -29,9 +27,9 @@ - - $(BuildDir)\bin\$(platform)\$(Configuration)\ - $(BuildDir)\int\$(platform)\$(Configuration)\$(ProjectName)\ + + ..\Build\bin\$(platform)\$(Configuration)\ + ..\Build\int\$(platform)\$(Configuration)\$(ProjectName)\ diff --git a/solution/solve_board.vcxproj b/solution/solve_board.vcxproj index 1bd0b9cc..36068883 100644 --- a/solution/solve_board.vcxproj +++ b/solution/solve_board.vcxproj @@ -16,8 +16,6 @@ false - - @@ -27,9 +25,9 @@ - - $(BuildDir)\bin\$(platform)\$(Configuration)\ - $(BuildDir)\int\$(platform)\$(Configuration)\$(ProjectName)\ + + ..\Build\bin\$(platform)\$(Configuration)\ + ..\Build\int\$(platform)\$(Configuration)\$(ProjectName)\ diff --git a/solution/solve_board_pbn.vcxproj b/solution/solve_board_pbn.vcxproj index 8bd53286..dd7580a0 100644 --- a/solution/solve_board_pbn.vcxproj +++ b/solution/solve_board_pbn.vcxproj @@ -16,10 +16,7 @@ false - - - - + @@ -28,9 +25,9 @@ - - $(BuildDir)\bin\$(platform)\$(Configuration)\ - $(BuildDir)\int\$(platform)\$(Configuration)\$(ProjectName)\ + + ..\Build\bin\$(platform)\$(Configuration)\ + ..\Build\int\$(platform)\$(Configuration)\$(ProjectName)\ diff --git a/web/BUILD.bazel b/web/BUILD.bazel index 6a439057..9c2d2e8f 100644 --- a/web/BUILD.bazel +++ b/web/BUILD.bazel @@ -74,6 +74,18 @@ py_test( ], ) +py_test( + name = "dds_mvp_js_test", + size = "small", + timeout = "short", + main = "tests/test_dds_mvp_js.py", + srcs = ["tests/test_dds_mvp_js.py"], + data = [ + "dds_mvp.js", + "tests/dds_mvp_test.mjs", + ], +) + # Stages wasm artifacts and runs Node smoke (~1–2s; wasm build is a separate analysis action). py_test( name = "dds_mvp_wasm_system_test", @@ -123,6 +135,7 @@ py_test( test_suite( name = "web_tests", tests = [ + ":dds_mvp_js_test", ":dds_mvp_wasm_test", ":wasm_scripts_test", ], diff --git a/web/dds_mvp.js b/web/dds_mvp.js index 10de0f38..68813682 100644 --- a/web/dds_mvp.js +++ b/web/dds_mvp.js @@ -4,7 +4,10 @@ // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT -// TODO: Add tests for the exported functions. +// Unit tests: web/tests/dds_mvp_test.mjs +// Run with: bazel test //web:dds_mvp_js_test +// or: python -m unittest web.tests.test_dds_mvp_js +// or: node --test web/tests/dds_mvp_test.mjs // ESLint configuration // https://eslint.org/demo diff --git a/web/tests/dds_mvp_test.mjs b/web/tests/dds_mvp_test.mjs new file mode 100644 index 00000000..e80a5b24 --- /dev/null +++ b/web/tests/dds_mvp_test.mjs @@ -0,0 +1,257 @@ +/** + * Unit tests for web/dds_mvp.js (Node built-in test runner). + * + * Run with: + * bazel test //web:dds_mvp_js_test + * or python -m unittest web.tests.test_dds_mvp_js + * or node --test web/tests/dds_mvp_test.mjs + */ +import assert from "node:assert/strict"; +import { existsSync, readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { createContext, runInContext } from "node:vm"; + +const DIRECTIONS = ["north", "east", "south", "west"]; +const SUITS = ["spades", "hearts", "diamonds", "clubs"]; + +function findDdsMvpJsPath() { + if (process.env.DDS_MVP_JS && existsSync(process.env.DDS_MVP_JS)) { + return process.env.DDS_MVP_JS; + } + + const here = dirname(fileURLToPath(import.meta.url)); + const adjacent = join(here, "..", "dds_mvp.js"); + if (existsSync(adjacent)) { + return adjacent; + } + + for (const base of [process.env.TEST_SRCDIR, process.env.RUNFILES_DIR]) { + if (!base) { + continue; + } + for (const sub of ["web/dds_mvp.js", "_main/web/dds_mvp.js"]) { + const candidate = join(base, sub); + if (existsSync(candidate)) { + return candidate; + } + } + } + + throw new Error("dds_mvp.js not found"); +} + +function createMockDocument(initialValues = {}) { + const store = new Map(); + + const makeElement = (id) => { + const element = { + id, + value: initialValues[id] ?? "", + innerHTML: "", + focus() {}, + }; + store.set(id, element); + return element; + }; + + for (const direction of DIRECTIONS) { + for (const suit of SUITS) { + makeElement(`${direction}_${suit}`); + } + } + makeElement("valid-pips"); + makeElement("result"); + + const rows = []; + for (let row = 0; row < 5; row++) { + const cells = []; + for (let column = 0; column < 6; column++) { + cells.push({ innerHTML: "" }); + } + rows.push({ cells }); + } + store.set("result-table", { rows }); + + return { + getElementById(id) { + return store.get(id) ?? null; + }, + element(id) { + return store.get(id); + }, + setValue(id, value) { + store.get(id).value = value; + }, + values() { + const out = {}; + for (const [id, element] of store) { + if (id.includes("_")) { + out[id] = element.value; + } + } + return out; + }, + }; +} + +function loadDdsMvp(document) { + const code = readFileSync(findDdsMvpJsPath(), "utf8"); + const sandbox = { + document, + console, + Promise, + Error, + }; + const context = createContext(sandbox); + runInContext(code, context, { filename: "dds_mvp.js" }); + return context; +} + +test("handsToPbn formats part-score deal", () => { + const document = createMockDocument(); + const ctx = loadDdsMvp(document); + ctx.fillFormWithPartScoreTestData(); + const pbn = ctx.handsToPbn(ctx.collectHands()); + assert.equal( + pbn, + "N:AQ85.AK976.5.J87 JT.QJ5432.Q9.KQ9 972..JT863.A6432 K643.T8.AK742.T5" + ); +}); + +test("inputIsValid rejects incomplete deal", () => { + const ctx = loadDdsMvp(createMockDocument()); + assert.equal( + ctx.inputIsValid({ N: ["SA"], E: [], S: [], W: [] }), + "Please enter 13 cards per hand." + ); +}); + +test("inputIsValid rejects invalid pip", () => { + const ctx = loadDdsMvp(createMockDocument()); + const hands = { + N: ["S1", "S2", "S3", "S4", "S5", "S6", "S7", "S8", "S9", "ST", "SJ", "SQ", "SK"], + E: ["HA", "H2", "H3", "H4", "H5", "H6", "H7", "H8", "H9", "HT", "HJ", "HQ", "HK"], + S: ["DA", "D2", "D3", "D4", "D5", "D6", "D7", "D8", "D9", "DT", "DJ", "DQ", "DK"], + W: ["CA", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9", "CT", "CJ", "CQ", "CK"], + }; + assert.match(ctx.inputIsValid(hands), /^Please use only these pips:/); +}); + +test("inputIsValid rejects duplicate cards", () => { + const ctx = loadDdsMvp(createMockDocument()); + const hands = { + N: ["SA", "SA", "S2", "S3", "S4", "S5", "S6", "S7", "S8", "S9", "ST", "SJ", "SQ"], + E: ["HA", "H2", "H3", "H4", "H5", "H6", "H7", "H8", "H9", "HT", "HJ", "HQ", "HK"], + S: ["DA", "D2", "D3", "D4", "D5", "D6", "D7", "D8", "D9", "DT", "DJ", "DQ", "DK"], + W: ["CA", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9", "CT", "CJ", "CQ", "CK"], + }; + const message = ctx.inputIsValid(hands); + assert.match(message, /^Duplicated card/); + assert.match(message, /♠A/); +}); + +test("inputIsValid accepts part-score deal", () => { + const document = createMockDocument(); + const ctx = loadDdsMvp(document); + ctx.fillFormWithPartScoreTestData(); + assert.equal(ctx.inputIsValid(ctx.collectHands()), ""); +}); + +test("collectHands reads suit holdings from inputs", () => { + const document = createMockDocument({ + north_spades: "AKQ", + north_hearts: "JT", + north_diamonds: "987", + north_clubs: "65432", + east_spades: "", + east_hearts: "AKQ", + east_diamonds: "", + east_clubs: "JT98765432", + south_spades: "JT98765432", + south_hearts: "", + south_diamonds: "AKQ", + south_clubs: "", + west_spades: "", + west_hearts: "98765432", + west_diamonds: "JT", + west_clubs: "AKQ", + }); + const ctx = loadDdsMvp(document); + const hands = ctx.collectHands(); + assert.equal(hands.N.length, 13); + assert.equal(hands.E.length, 13); + assert.equal(hands.S.length, 13); + assert.equal(hands.W.length, 13); + assert.ok(hands.N.includes("SA")); + assert.ok(hands.N.includes("SK")); + assert.ok(hands.E.includes("CJ")); +}); + +test("clearTestData clears all hand inputs", () => { + const document = createMockDocument({ north_spades: "AKQ", west_clubs: "JT" }); + const ctx = loadDdsMvp(document); + ctx.clearTestData(); + assert.equal(document.element("north_spades").value, ""); + assert.equal(document.element("west_clubs").value, ""); +}); + +test("rotateClockwise shifts holdings west to north", () => { + const document = createMockDocument(); + let index = 1; + for (const direction of DIRECTIONS) { + for (const suit of SUITS) { + document.setValue(`${direction}_${suit}`, String(index)); + index += 1; + } + } + const ctx = loadDdsMvp(document); + ctx.rotateClockwise(); + assert.equal(document.element("north_spades").value, "13"); + assert.equal(document.element("north_hearts").value, "14"); + assert.equal(document.element("north_diamonds").value, "15"); + assert.equal(document.element("north_clubs").value, "16"); + assert.equal(document.element("east_spades").value, "1"); +}); + +test("fillFormWithPartScoreTestData populates inputs", () => { + const document = createMockDocument(); + const ctx = loadDdsMvp(document); + ctx.fillFormWithPartScoreTestData(); + assert.equal(document.element("north_spades").value, "AQ85"); + assert.equal(document.element("west_clubs").value, "T5"); +}); + +test("pageLoad shows valid pips", () => { + const document = createMockDocument(); + const ctx = loadDdsMvp(document); + ctx.pageLoad(); + assert.equal(document.element("valid-pips").innerHTML, "AKQJT98765432"); +}); + +test("loadDdsModule rejects missing wasm globals", async () => { + const ctx = loadDdsMvp(createMockDocument()); + await assert.rejects( + () => ctx.loadDdsModule(), + /WASM module not found/ + ); +}); + +test("fillFormWithGrandSlamTestData populates inputs", () => { + const document = createMockDocument(); + const ctx = loadDdsMvp(document); + ctx.fillFormWithGrandSlamTestData(); + assert.equal(document.element("north_spades").value, "AKQJ"); + assert.equal(document.element("east_clubs").value, "432"); + assert.equal(ctx.inputIsValid(ctx.collectHands()), ""); +}); + +test("fillFormWithEveryoneMakes3nTestData populates inputs", () => { + const document = createMockDocument(); + const ctx = loadDdsMvp(document); + ctx.fillFormWithEveryoneMakes3nTestData(); + assert.equal(document.element("north_hearts").value, "A8765432"); + assert.equal(document.element("west_spades").value, ""); + assert.equal(ctx.inputIsValid(ctx.collectHands()), ""); +}); diff --git a/web/tests/test_dds_mvp_js.py b/web/tests/test_dds_mvp_js.py new file mode 100644 index 00000000..e0d247b4 --- /dev/null +++ b/web/tests/test_dds_mvp_js.py @@ -0,0 +1,80 @@ +"""Unit tests for web/dds_mvp.js via Node's built-in test runner. + +Run with: bazel test //web:dds_mvp_js_test +or: python -m unittest web.tests.test_dds_mvp_js +or: node --test web/tests/dds_mvp_test.mjs +""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import unittest +from pathlib import Path + +TESTS_ROOT = Path(__file__).resolve().parent + + +def _runfiles_root() -> Path: + for key in ("RUNFILES_DIR", "TEST_SRCDIR"): + if key in os.environ: + return Path(os.environ[key]) + return TESTS_ROOT.parent.parent + + +def rlocation(relpath: str) -> Path: + root = _runfiles_root() + for candidate in (root / relpath, root / "_main" / relpath): + if candidate.exists(): + return candidate + raise FileNotFoundError(relpath) + + +class DdsMvpJsTest(unittest.TestCase): + def test_dds_mvp_js(self) -> None: + node = shutil.which("node") + if not node: + raise unittest.SkipTest("node not found") + + try: + version = subprocess.run( + [node, "--version"], + capture_output=True, + text=True, + check=False, + timeout=5, + ).stdout.strip() + except subprocess.TimeoutExpired: + raise unittest.SkipTest("node --version timed out") + try: + major = int(version.lstrip("v").split(".", 1)[0]) + except ValueError: + raise unittest.SkipTest(f"could not parse node version: {version!r}") + if major < 18: + raise unittest.SkipTest(f"node >= 18 required for `node --test` (found {version})") + + test_script = rlocation("web/tests/dds_mvp_test.mjs") + dds_mvp_js = rlocation("web/dds_mvp.js") + env = os.environ.copy() + env["DDS_MVP_JS"] = str(dds_mvp_js) + try: + proc = subprocess.run( + [node, "--test", str(test_script)], + capture_output=True, + text=True, + check=False, + env=env, + timeout=55, + ) + except subprocess.TimeoutExpired as exc: + self.fail(f"node --test timed out: {exc}") + self.assertEqual( + proc.returncode, + 0, + msg=proc.stdout + proc.stderr, + ) + + +if __name__ == "__main__": + unittest.main()