Compare commits

...
Author SHA1 Message Date
admin ea7695f48a Fix Shardok touch overlays and arm troops pointer layout 2026-06-25 06:56:44 -07:00
adminandGitHub 0b9ebb04c8 Split Arm Troops controls by input mode (#8085)
* Split Arm Troops controls by input mode

* Allow editor touch simulation for Arm Troops

* Constrain Arm Troops touch action strip

* Move Arm Troops touch controls beside table

* Compact Arm Troops touch controls

* Tighten Arm Troops touch table width

* Polish Arm Troops touch selection controls

* Clarify Arm Troops touch columns

* Align Arm Troops touch column headers

* Separate Arm Troops all-battalion controls

* Use column layout for Arm Troops touch controls

* Revert "Use column layout for Arm Troops touch controls"

This reverts commit e6273c22f900e990c86fcac5e761723d54bd15d0.

* Use dedicated touch all-battalions controls

* Handle missing touch button style sources

* Revert "Handle missing touch button style sources"

This reverts commit 2a2b2204bbc0deede8e92ffa5d07cd86ccf3cb96.

* Revert "Use dedicated touch all-battalions controls"

This reverts commit 44fd112162518b9d0658f9374af5771196ccefbb.

* Fit Arm Troops all-battalion touch column

* Use correct Arm Troops all-controls parent

* Align Arm Troops touch column headers
2026-06-25 06:46:45 -07:00
adminandGitHub 10a3b3a046 Store evacuate prisoners settings getter by value (#8151)
* Store evacuate prisoners settings getter by value

* Keep evacuate settings getter const
2026-06-25 06:43:42 -07:00
adminandGitHub 7cf852ab52 Synchronize GamesManager state updates (#8150) 2026-06-25 06:40:04 -07:00
adminandGitHub 2341df8998 Synchronize GamesManager state snapshots (#8149) 2026-06-25 05:20:18 -07:00
adminandGitHub 190e71d3c4 Validate sequence random generator input (#8148) 2026-06-24 22:19:06 -07:00
adminandGitHub c0a9b90715 Decode Shardok unit bytes safely in tests (#8140) 2026-06-24 22:13:40 -07:00
adminandGitHub 6d13774fd0 Synchronize GamesManager creation paths (#8146) 2026-06-24 22:13:17 -07:00
adminandGitHub 41252ccaa6 Harden random generator input validation (#8147) 2026-06-24 22:12:56 -07:00
adminandGitHub ff05a35991 Centralize GamesManager state transitions (#8143) 2026-06-24 21:52:31 -07:00
adminandGitHub 8b7e9eaeb6 Print Shardok AI failure ids numerically (#8145) 2026-06-24 21:51:59 -07:00
adminandGitHub 7f80eb0ef2 Clamp random integer endpoint samples (#8144)
* Clamp random integer endpoint samples

* Cover endpoint undead command fallback
2026-06-24 21:51:18 -07:00
adminandGitHub ef151c2ef4 Log Shardok AI failure context (#8142) 2026-06-24 15:57:37 -07:00
adminandGitHub e7cad8dd36 Extract immutable GamesManager state (#8138)
* Extract immutable games manager state

* Use direct GamesManager state deps

* Add direct ControllerInfo deps
2026-06-24 15:53:15 -07:00
adminandGitHub c4edb2f6a0 Remove raw Unit byte casts in Shardok paths (#8141) 2026-06-24 15:43:42 -07:00
adminandGitHub 11709e6d48 Decode secured Shardok units safely (#8139) 2026-06-24 15:36:50 -07:00
adminandGitHub 0b0fcc411d Extract game controller registry (#8135)
* Extract game controller registry

* Split game controller registry target

* Make game controller registry immutable
2026-06-24 15:23:50 -07:00
adminandGitHub 1aadcc863f Copy action result unit bytes before reading (#8137) 2026-06-24 10:51:22 -07:00
adminandGitHub 9ac167e66c Add Shardok sanitizer CI (#8136) 2026-06-24 10:31:07 -07:00
adminandGitHub 1530873df5 Bound action filter leakage logging (#8134) 2026-06-24 09:41:59 -07:00
adminandGitHub 75a5b4fa21 Use timed logger in Shardok interface client (#8133) 2026-06-24 09:41:44 -07:00
adminandGitHub b6c45da7bc Use timed logger for history fallback messages (#8131) 2026-06-24 09:41:28 -07:00
adminandGitHub 5e30673281 Use timed logger in client text stores (#8132) 2026-06-24 09:41:14 -07:00
adminandGitHub d38c611a18 Use timed logger in exception interceptor (#8128) 2026-06-24 08:59:09 -07:00
adminandGitHub 570bc557f5 Remove temporary backstory visibility debug check (#8130) 2026-06-24 08:58:56 -07:00
adminandGitHub c1f4152cae Cache target indexes in attack grouping (#8129) 2026-06-24 08:58:04 -07:00
adminandGitHub 0472362abc Broaden ActionResultC copy coverage (#8127) 2026-06-24 08:53:05 -07:00
adminandGitHub b732b60520 Use indexed distances for CoordsSet scans (#8126) 2026-06-24 08:52:17 -07:00
adminandGitHub fc1f988654 Clarify command option validation (#8125) 2026-06-24 08:48:58 -07:00
adminandGitHub 7ef93c4a11 Optimize sparse CoordsSet filtering (#8124) 2026-06-24 08:45:55 -07:00
adminandGitHub 247aa3e685 Reject malformed ransom offer commands (#8122) 2026-06-24 08:26:44 -07:00
adminandGitHub aa0292bad6 Use CoordsSet index iterator for victory scoring (#8123) 2026-06-24 08:26:11 -07:00
adminandGitHub 08cd608ac2 Use CoordsSet index iterator for attack locations (#8121) 2026-06-24 08:05:03 -07:00
adminandGitHub f85a32dc08 Fix ActionResultC copy removed faction ids (#8118) 2026-06-24 07:54:12 -07:00
adminandGitHub b5deb69ef7 Template CoordsSet filter (#8120) 2026-06-24 07:53:22 -07:00
adminandGitHub 872883f710 Shrink CoordsSet dimension fields (#8119) 2026-06-24 07:10:35 -07:00
adminandGitHub 3d402e7bdd Add CoordsSet microbenchmark (#8117) 2026-06-24 07:04:38 -07:00
adminandGitHub 862480c470 Assert CoordsSet bounds in debug builds (#8100) 2026-06-24 06:54:40 -07:00
adminandGitHub 98b5d7f034 Standardize CoordsSet iterators (#8103)
* Standardize CoordsSet iterators

* Add CoordsSet iterator tests
2026-06-24 06:48:42 -07:00
adminandGitHub e5c5a929ec Use inside for action-result Option lookups (#8113)
* Use inside for feast changed hero lookups

* Use inside for march command province lookups

* Use inside for diplomacy command lookups

* Use inside for captured hero deferred changes

* Use inside for applier relationship lookups

* Refresh CI after CoordsSet main update
2026-06-24 06:47:05 -07:00
adminandGitHub d39ee392b9 Use inside for command-choice Option selections (#8114)
* Use inside for expand command choices

* Use inside for offer command selections

* Refresh CI after CoordsSet main update
2026-06-24 06:46:38 -07:00
adminandGitHub b574c6c950 Use inside for availability command Options (#8115)
* Use inside for diplomacy command Options

* Use inside for resolve availability Options

* Use inside for defend and train Options

* Refresh CI after CoordsSet main update
2026-06-24 06:43:40 -07:00
adminandGitHub 074c984f3b Fix CoordsSet test raw index helper name (#8116) 2026-06-24 06:04:02 -07:00
adminandGitHub 2817ca529d Clarify CoordsSet dimension naming (#8098) 2026-06-24 05:34:14 -07:00
adminandGitHub 4f932d0a56 Remove unsafe CoordsSet string conversion (#8097) 2026-06-24 05:33:42 -07:00
adminandGitHub 2dd931dc8f Expand CoordsSet test coverage (#8105) 2026-06-24 05:32:30 -07:00
adminandGitHub c7ab2ecb44 Rename CoordsSet raw index add (#8102) 2026-06-24 05:30:28 -07:00
adminandGitHub 8466ab6ed9 Use inside for decline quest availability Option (#8096) 2026-06-23 22:17:47 -07:00
adminandGitHub 7b74905973 Use inside for divine availability Options (#8095) 2026-06-23 22:17:33 -07:00
adminandGitHub ecfb5a0a94 Use inside for excess supplies Options (#8089) 2026-06-23 22:17:19 -07:00
adminandGitHub 967cb74aa5 Use inside for recruit heroes Options (#8094) 2026-06-23 22:16:54 -07:00
adminandGitHub 34daf8f38d Make byte_vector own its storage (#8086) 2026-06-23 22:14:23 -07:00
adminandGitHub e12359821c Use inside for apprehend outlaw Options (#8093) 2026-06-23 22:10:51 -07:00
adminandGitHub eea41e6c05 Use inside for march destination lookups (#8092) 2026-06-23 22:10:34 -07:00
adminandGitHub 36d1e75719 Use inside for manage prisoner lookups (#8091) 2026-06-23 22:10:13 -07:00
adminandGitHub 35e93c8ce1 Use inside for control weather availability Options (#8090) 2026-06-23 22:09:50 -07:00
adminandGitHub d603b01733 Use inside for game state view full info (#8088) 2026-06-23 22:09:33 -07:00
adminandGitHub 6c66b5df82 Use inside for game state view diffs (#8087) 2026-06-23 22:09:09 -07:00
adminandGitHub 3b59e21407 Normalize Shardok engine test suffixes (#8083) 2026-06-23 21:49:21 -07:00
adminandGitHub d0e74ac4cb Consolidate nodiscard helper annotations (#8078) 2026-06-23 21:49:08 -07:00
adminandGitHub c04aa3d753 Use inside for unaffiliated hero change lookups (#8077) 2026-06-23 21:48:45 -07:00
adminandGitHub f3fade7ed2 Use inside for new round action lookups (#8076) 2026-06-23 21:48:30 -07:00
adminandGitHub 79d8e837e8 Use inside for riot command choice new values (#8074) 2026-06-23 21:48:08 -07:00
adminandGitHub c39795fa9f Use inside for diplomacy availability Options (#8072) 2026-06-23 21:47:31 -07:00
adminandGitHub 4a7c55a382 Use inside for view test lookups (#8068) 2026-06-23 21:45:43 -07:00
adminandGitHub 7e4396552e Use inside for resource availability Options (#8063) 2026-06-23 21:45:30 -07:00
adminandGitHub 40219f2a67 Replace remaining numeric functional casts (#8079) 2026-06-23 21:45:09 -07:00
adminandGitHub ce35c176ee Make CoordsSet iterator casts explicit (#8082) 2026-06-23 21:44:53 -07:00
adminandGitHub a852f03214 Use from_chars for hex byte parsing (#8084) 2026-06-23 21:40:53 -07:00
adminandGitHub 7c8b43d91b Tidy AI unit score readability (#8080) 2026-06-23 19:26:43 -07:00
adminandGitHub ecf763b24b Tidy common byte helpers (#8081) 2026-06-23 19:25:24 -07:00
adminandGitHub 8ad4fc52e3 Use inside for province availability Options (#8060) 2026-06-23 19:24:16 -07:00
adminandGitHub 7e48f6fd2b Use inside for hero availability Options (#8058) 2026-06-23 19:24:00 -07:00
adminandGitHub 5c81832cde Align notification popup height with command panel (#8061)
* Align notification popup height with command panel

* Widen Arm Troops table for touch controls

* Improve Arm Troops row button spacing

* Add Arm Troops scrollbar gutter
2026-06-23 19:21:06 -07:00
adminandGitHub 569458d691 Use inside for attack decision new values (#8066) 2026-06-23 17:42:33 -07:00
adminandGitHub c825bf4606 Use inside for supplies and riot availability Options (#8070) 2026-06-23 17:41:23 -07:00
160 changed files with 2982 additions and 1266 deletions
+2
View File
@@ -37,6 +37,8 @@ build:ubsan --strip=never
build:ubsan --copt=-fno-omit-frame-pointer
build:ubsan --copt=-Wno-macro-redefined
build:ubsan --copt=-fsanitize=undefined
# Protobuf's arena implementation trips UBSan alignment checks in external code on macOS.
build:ubsan --copt=-fno-sanitize=alignment
build:ubsan --linkopt=-fsanitize=undefined
test:ubsan --test_env=UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1
@@ -0,0 +1,92 @@
name: Shardok Sanitizer Test
on:
push:
branches: ["main"]
paths:
- "src/main/cpp/net/eagle0/shardok/**"
- "src/test/cpp/net/eagle0/shardok/**"
- "src/main/protobuf/net/eagle0/shardok/**"
- "src/main/protobuf/net/eagle0/common/**"
- "WORKSPACE"
- "MODULE.bazel"
- "MODULE.bazel.lock"
- "BUILD.bazel"
- ".bazelrc"
- ".github/actions/setup-bazel/**"
- ".github/workflows/shardok_sanitizer_test.yml"
- "ci/github_actions/ensure_bazel_installed.sh"
- "ci/github_actions/summarize_bazel_bep.py"
pull_request:
paths:
- "src/main/cpp/net/eagle0/shardok/**"
- "src/test/cpp/net/eagle0/shardok/**"
- "src/main/protobuf/net/eagle0/shardok/**"
- "src/main/protobuf/net/eagle0/common/**"
- "WORKSPACE"
- "MODULE.bazel"
- "MODULE.bazel.lock"
- "BUILD.bazel"
- ".bazelrc"
- ".github/actions/setup-bazel/**"
- ".github/workflows/shardok_sanitizer_test.yml"
- "ci/github_actions/ensure_bazel_installed.sh"
- "ci/github_actions/summarize_bazel_bep.py"
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
shardok-sanitizer-test:
name: Shardok ${{ matrix.name }}
runs-on: [self-hosted, bazel]
strategy:
fail-fast: false
matrix:
include:
- name: ASan
config: asan
- name: UBSan
config: ubsan
steps:
- name: Ensure Git LFS available for checkout
run: |
COMMON_PATHS=(/opt/homebrew/bin /usr/local/bin)
for path in "${COMMON_PATHS[@]}"; do
if [ -d "$path" ]; then
echo "$path" >> "$GITHUB_PATH"
export PATH="$path:$PATH"
fi
done
if command -v git-lfs >/dev/null 2>&1; then
git-lfs version
exit 0
fi
brew install git-lfs
git-lfs version
- name: Checkout repository
uses: actions/checkout@v6
with:
persist-credentials: false
lfs: false
- name: Setup Bazel
uses: ./.github/actions/setup-bazel
- name: Run Shardok sanitizer tests
run: bazel test --config=${{ matrix.config }} --build_event_json_file=${{ matrix.config }}.json //src/test/cpp/net/eagle0/shardok/...
- name: Summarize Bazel build metrics
if: always()
run: python3 ci/github_actions/summarize_bazel_bep.py ${{ matrix.config }}.json
- name: Archive sanitizer test results
if: always()
uses: actions/upload-artifact@v7
with:
name: shardok-${{ matrix.config }}.json
path: ${{ matrix.config }}.json
retention-days: 3
@@ -28,7 +28,7 @@ static inline auto HashBuffer(const uint8_t* data, size_t size) -> uint64_t {
// Process 8 bytes at a time
while (data + 8 <= end) {
uint64_t word;
uint64_t word = 0;
// Use memcpy to avoid alignment issues and let compiler optimize
__builtin_memcpy(&word, data, sizeof(word));
hash ^= word;
@@ -9,6 +9,7 @@
#include <bit>
#include <cstdint>
#include <cstdlib>
#include <iterator>
#define ITERABLE_BITSET_INDEX_CHECKS false
@@ -104,9 +105,11 @@ public:
class Iter {
public:
using iterator_category = std::forward_iterator_tag;
using difference_type = std::ptrdiff_t;
using value_type = size_t;
using pointer = size_t*;
using reference = size_t&;
using pointer = void;
using reference = size_t;
private:
size_t storeIndex;
@@ -142,13 +145,20 @@ public:
auto operator!=(const Iter& rhs) const -> bool {
return storeIndex != rhs.storeIndex || bitIndex != rhs.bitIndex;
}
void operator++() {
auto operator++() -> Iter& {
bitIndex++;
if (bitIndex >= STORE_BITS) {
storeIndex++;
bitIndex = 0;
}
ToNext();
return *this;
}
auto operator++(int) -> Iter {
Iter previous = *this;
++(*this);
return previous;
}
auto operator*() const -> size_t { return (storeIndex << BIT_SHIFT) + bitIndex; }
@@ -8,7 +8,9 @@
#include "RandomGenerator.hpp"
#include <algorithm>
#include <ctime>
#include <stdexcept>
constexpr double kOpenEndedHighThreshold = 95.0;
constexpr double kOpenEndedLowThreshold = 5.0;
@@ -67,7 +69,8 @@ auto RandomGenerator::ReverseOpenEndedPercentile() -> double {
}
auto RandomGenerator::IntBetween(const int min, const int max) -> int {
return int(DoubleZeroToOne() * (max - min)) + min;
if (min >= max) { throw std::invalid_argument("random integer range must be non-empty"); }
return std::min(int(DoubleZeroToOne() * (max - min)) + min, max - 1);
}
auto RandomGenerator::IntBelow(const int max) -> int { return IntBetween(0, max); }
@@ -94,6 +97,7 @@ StdLibraryGenerator::StdLibraryGenerator(const std::mt19937_64::result_type seed
}
auto StdLibraryGenerator::IntBetween(const int min, const int max) -> int {
if (min >= max) { throw std::invalid_argument("random integer range must be non-empty"); }
std::uniform_int_distribution<int> unifInt(min, max - 1);
return unifInt(engine);
}
@@ -12,6 +12,7 @@
#include <cstddef>
#include <memory>
#include <random>
#include <stdexcept>
#include <vector>
class RandomGenerator;
@@ -41,6 +42,7 @@ public:
template<class T>
auto RandomElement(const std::vector<T>& vec) -> T {
if (vec.empty()) { throw std::invalid_argument("cannot choose from an empty vector"); }
return vec.at(static_cast<std::size_t>(IntBelow(static_cast<int>(vec.size()))));
}
@@ -9,7 +9,9 @@
#ifndef EAGLE0_COMMON_SEQUENCE_RANDOM_GENERATOR_HPP
#define EAGLE0_COMMON_SEQUENCE_RANDOM_GENERATOR_HPP
#include <cmath>
#include <memory>
#include <stdexcept>
#include <utility>
#include <vector>
@@ -36,7 +38,17 @@ private:
}
public:
explicit SequenceRandomGenerator(std::vector<double> s) : sequence(std::move(s)) {}
explicit SequenceRandomGenerator(std::vector<double> s) : sequence(std::move(s)) {
if (sequence.empty()) {
throw std::invalid_argument("sequence random generator requires at least one value");
}
for (const double value : sequence) {
if (!std::isfinite(value) || value < 0.0 || value > 1.0) {
throw std::invalid_argument(
"sequence random generator values must be between 0 and 1");
}
}
}
static auto WithSequence(const std::vector<double>& s)
-> std::shared_ptr<SequenceRandomGenerator> {
return std::make_shared<SequenceRandomGenerator>(s);
+90 -59
View File
@@ -9,18 +9,24 @@
#ifndef EAGLE0_COMMON_BYTE_VECTOR_HPP
#define EAGLE0_COMMON_BYTE_VECTOR_HPP
#include <charconv>
#include <cstdint>
#include <cstring>
#include <fstream>
#include <sstream>
#include <initializer_list>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
using std::string;
using std::vector;
class byte_vector : public vector<uint8_t> {
class byte_vector {
private:
std::vector<uint8_t> bytes_;
public:
class HexException : public std::exception {
const std::string description;
@@ -35,60 +41,78 @@ public:
};
private:
static auto HexNibbleToValue(const char nibble) -> uint8_t {
if (nibble >= '0' && nibble <= '9') {
return uint8_t(nibble - '0');
} else if (nibble >= 'A' && nibble <= 'F') {
return uint8_t(nibble - 'A' + 10);
} else if (nibble >= 'a' && nibble <= 'f') {
return uint8_t(nibble - 'a' + 10);
static auto HexCharsToByte(const std::string_view digits) -> uint8_t {
unsigned int parsedByte = 0;
const auto [ptr, ec] =
std::from_chars(digits.data(), digits.data() + digits.size(), parsedByte, 16);
if (ec != std::errc() || ptr != digits.data() + digits.size()) {
throw HexException(ptr == digits.data() + digits.size() ? digits.back() : *ptr);
}
throw HexException(nibble);
}
static auto HexCharsToByte(std::istringstream& str) -> uint8_t {
uint8_t b = 0;
char c = 0;
str >> c;
b |= (HexNibbleToValue(c) << 4u);
str >> c;
b |= HexNibbleToValue(c);
return b;
return static_cast<uint8_t>(parsedByte);
}
public:
byte_vector() = default;
byte_vector(const std::initializer_list<uint8_t> list) : vector<uint8_t>(list) {}
byte_vector(const std::initializer_list<uint8_t> list) : bytes_(list) {}
template<class InputIt>
byte_vector(InputIt begin, InputIt end) : vector<uint8_t>(begin, end) {}
byte_vector(InputIt begin, InputIt end) : bytes_(begin, end) {}
byte_vector(const byte_vector& rhs) = default;
explicit byte_vector(const std::string& s) : byte_vector(s.begin(), s.end()) {}
explicit byte_vector(const size_t size) : vector<uint8_t>(size) {}
explicit byte_vector(void* data, const size_t size) : vector<uint8_t>(size) {
this->assign((uint8_t*)data, (uint8_t*)data + size);
byte_vector(byte_vector&& rhs) noexcept = default;
explicit byte_vector(const std::string& s) : bytes_(s.begin(), s.end()) {}
explicit byte_vector(std::vector<uint8_t> bytes) : bytes_(std::move(bytes)) {}
explicit byte_vector(const size_t size) : bytes_(size) {}
explicit byte_vector(const void* data, const size_t size) {
auto* bytes = static_cast<const uint8_t*>(data);
bytes_.assign(bytes, bytes + size);
}
auto operator==(const vector<uint8_t>& vec) -> bool {
if (this->size() != vec.size()) return false;
for (size_t i = 0; i < size(); i++) {
if ((*this)[i] != vec[i]) return false;
}
return true;
auto operator=(const byte_vector& rhs) -> byte_vector& = default;
auto operator=(byte_vector&& rhs) noexcept -> byte_vector& = default;
[[nodiscard]] auto operator==(const std::vector<uint8_t>& vec) const -> bool {
return bytes_ == vec;
}
[[nodiscard]] auto operator==(const byte_vector& vec) const -> bool {
return bytes_ == vec.bytes_;
}
[[nodiscard]] auto data() -> uint8_t* { return bytes_.data(); }
[[nodiscard]] auto data() const -> const uint8_t* { return bytes_.data(); }
[[nodiscard]] auto size() const -> size_t { return bytes_.size(); }
[[nodiscard]] auto empty() const -> bool { return bytes_.empty(); }
void reserve(const size_t count) { bytes_.reserve(count); }
void resize(const size_t count) { bytes_.resize(count); }
void swap(byte_vector& other) noexcept { bytes_.swap(other.bytes_); }
[[nodiscard]] auto begin() -> std::vector<uint8_t>::iterator { return bytes_.begin(); }
[[nodiscard]] auto begin() const -> std::vector<uint8_t>::const_iterator {
return bytes_.begin();
}
[[nodiscard]] auto end() -> std::vector<uint8_t>::iterator { return bytes_.end(); }
[[nodiscard]] auto end() const -> std::vector<uint8_t>::const_iterator { return bytes_.end(); }
[[nodiscard]] auto cbegin() const -> std::vector<uint8_t>::const_iterator {
return bytes_.cbegin();
}
[[nodiscard]] auto cend() const -> std::vector<uint8_t>::const_iterator {
return bytes_.cend();
}
[[nodiscard]] auto operator[](const size_t index) -> uint8_t& { return bytes_[index]; }
[[nodiscard]] auto operator[](const size_t index) const -> uint8_t { return bytes_[index]; }
auto append(const char* bytes, const size_t length) {
const auto previousSize = this->size();
this->resize(previousSize + length);
std::memcpy(this->data() + previousSize, bytes, length);
const auto previousSize = size();
resize(previousSize + length);
std::memcpy(data() + previousSize, bytes, length);
}
template<class T>
auto append(const T* elts, const size_t count) {
append((char*)elts, count * sizeof(T));
append(reinterpret_cast<const char*>(elts), count * sizeof(T));
}
template<class T>
auto append(const vector<T> elts) {
auto append(const vector<T>& elts) {
append(elts.data(), elts.size());
}
@@ -98,7 +122,7 @@ public:
}
auto operator+=(const uint8_t c) -> byte_vector& {
this->insert(end(), c);
bytes_.push_back(c);
return *this;
}
@@ -111,7 +135,7 @@ public:
template<class Iterable>
auto operator+=(const Iterable& rhs) -> byte_vector& {
this->insert(end(), rhs.cbegin(), rhs.cend());
bytes_.insert(end(), rhs.cbegin(), rhs.cend());
return *this;
}
@@ -119,7 +143,7 @@ public:
if (size() < 8) {
uint64_t total = 0;
for (size_t i = 0; i < size(); i++) {
total <<= 8u;
total <<= 8U;
total += (*this)[i];
}
return total;
@@ -128,9 +152,9 @@ public:
uint64_t firstBytes = 0;
uint64_t lastBytes = 0;
for (unsigned i = 0; i < 8; i++) {
firstBytes <<= 8u;
firstBytes <<= 8U;
firstBytes += (*this)[i];
lastBytes <<= 8u;
lastBytes <<= 8U;
lastBytes += (*this)[i];
}
@@ -141,8 +165,8 @@ public:
std::ifstream inputFileStream(path, std::ios::binary | std::ios::ate);
const std::streamsize size = inputFileStream.tellg();
inputFileStream.seekg(0, std::ios::beg);
this->resize((size_t(size)));
inputFileStream.read((char*)this->data(), size);
this->resize(static_cast<size_t>(size));
inputFileStream.read(reinterpret_cast<char*>(this->data()), size);
return *this;
}
@@ -153,34 +177,41 @@ public:
}
const std::streamsize size = inputFileStream.tellg();
inputFileStream.seekg(0, std::ios::beg);
byte_vector serializedGame((size_t(size)));
inputFileStream.read((char*)serializedGame.data(), size);
byte_vector serializedGame(static_cast<size_t>(size));
inputFileStream.read(reinterpret_cast<char*>(serializedGame.data()), size);
return serializedGame;
}
static auto FromHex(const char* hexString) -> byte_vector { return FromHex(string(hexString)); }
static auto FromHex(string hexString) -> byte_vector {
if (hexString[0] == '0' && (hexString[1] == 'x' || hexString[1] == 'X')) {
hexString = string(hexString.begin() + 1, hexString.end());
static auto FromHex(const string& hexString) -> byte_vector {
std::string_view digits(hexString);
if (digits.starts_with("0x") || digits.starts_with("0X")) {
digits.remove_prefix(2);
} else if (digits.starts_with("x") || digits.starts_with("X")) {
digits.remove_prefix(1);
}
if (hexString[0] == 'x' || hexString[0] == 'X') {
hexString = string(hexString.begin() + 1, hexString.end());
}
byte_vector vec;
const size_t len = hexString.size();
vec.reserve(len / 2);
std::istringstream hexStream(hexString);
while (size_t(hexStream.tellg()) < len) { vec += HexCharsToByte(hexStream); }
if (digits.size() % 2 != 0) { throw HexException(digits.empty() ? '\0' : digits.back()); }
byte_vector vec;
vec.reserve(digits.size() / 2);
for (size_t index = 0; index < digits.size(); index += 2) {
vec += HexCharsToByte(digits.substr(index, 2));
}
return vec;
}
explicit operator std::string() const { return std::string(begin(), end()); }
};
inline auto operator<<(std::ostream& ostr, byte_vector vec) -> std::ostream& {
ostr.write((char*)vec.data(), (std::streamsize)vec.size());
inline auto operator==(const std::vector<uint8_t>& lhs, const byte_vector& rhs) -> bool {
return rhs == lhs;
}
inline auto operator<<(std::ostream& ostr, const byte_vector& vec) -> std::ostream& {
ostr.write(reinterpret_cast<const char*>(vec.data()), static_cast<std::streamsize>(vec.size()));
return ostr;
}
@@ -18,7 +18,7 @@ struct alignas(64) OneMapInfo {
std::map<int, int> positionsRequiringCrossing;
};
auto CalculateMap(
[[nodiscard]] auto CalculateMap(
const std::string& mapName,
const std::shared_ptr<shardok::ActionPointDistancesCache>& apdCache,
const shardok::BattalionTypeSPtr& battalionType) -> OneMapInfo;
@@ -25,25 +25,44 @@ DIST_T NormalizedCostWhenBraving(const DIST_T cost) {
struct TargetAndDistance {
Coords target;
size_t targetIndex;
CoordsSet attackLocations;
double targetPower;
DIST_T distance;
TargetAndDistance(const Coords& t, const CoordsSet& al, const double tp, const DIST_T d)
TargetAndDistance(
const Coords& t,
const size_t ti,
const CoordsSet& al,
const double tp,
const DIST_T d)
: target(t),
targetIndex(ti),
attackLocations(al),
targetPower(tp),
distance(d) {}
};
auto CoordsIndex(const Coords& coords, const int columnCount) {
return coords.row() * columnCount + coords.column();
}
auto CoordsFromIndex(const size_t index, const int columnCount) -> Coords {
return Coords(
static_cast<int8_t>(index / static_cast<size_t>(columnCount)),
static_cast<int8_t>(index % static_cast<size_t>(columnCount)));
}
auto MinDistance(
const Coords& start,
const CoordsSet& destinations,
const ActionPointDistances* apd) -> DIST_T {
DIST_T minDistance = ActionPointDistances::IMPOSSIBLE;
const int startIndex = CoordsIndex(start, destinations.ColumnCount());
for (const Coords& dest : destinations) {
if (const DIST_T distance = apd->Distance(start, dest); distance < minDistance) {
for (const auto destIndex : destinations.indexIterator()) {
if (const DIST_T distance = apd->Distance(startIndex, static_cast<int>(destIndex));
distance < minDistance) {
minDistance = distance;
}
}
@@ -113,10 +132,6 @@ auto EffectiveDistance(
auto Power(const Unit* unit) -> double { return unit->battalion().size(); }
auto CoordsIndex(const Coords& coords, const int columnCount) {
return coords.row() * columnCount + coords.column();
}
// Chooses a list of targets in priority order for each unit.
auto GenerateTargetPriorities(
const vector<const Unit*>& occupants,
@@ -163,10 +178,10 @@ auto GenerateTargetPriorities(
bravingApd = apdCache->GetRaw(map, mapId, battType, true, braveWaterCost);
}
for (const Coords& targetLocation : targets) {
const auto coordsIndex = CoordsIndex(targetLocation, cc);
const auto& occupant = occupants[coordsIndex];
for (const auto targetIndex : targets.indexIterator()) {
const auto& occupant = occupants[targetIndex];
const Coords targetLocation = CoordsFromIndex(targetIndex, cc);
const auto& attackLocations = alCache->CachedLocations(targetLocation, isLateGame);
if (occupant && occupant->player_id() != attackerId) {
@@ -179,12 +194,14 @@ auto GenerateTargetPriorities(
targetsWithDistance.emplace_back(
targetLocation,
targetIndex,
attackLocsForUnit,
occupantPower,
distance);
} else {
targetsWithDistance.emplace_back(
targetLocation,
targetIndex,
attackLocations.LocationsWithEnemyInRange(unit),
occupantPower,
ActionPointDistances::IMPOSSIBLE);
@@ -195,13 +212,11 @@ auto GenerateTargetPriorities(
// Sort by distance
std::ranges::sort(
targetsWithDistance,
[&powerAttackingEachTarget,
cc](const TargetAndDistance& left, const TargetAndDistance& right) {
const auto leftIndex = CoordsIndex(left.target, cc);
const auto rightIndex = CoordsIndex(right.target, cc);
const double powerAttackingLeft = powerAttackingEachTarget[leftIndex];
const double powerAttackingRight = powerAttackingEachTarget[rightIndex];
[&powerAttackingEachTarget](
const TargetAndDistance& left,
const TargetAndDistance& right) {
const double powerAttackingLeft = powerAttackingEachTarget[left.targetIndex];
const double powerAttackingRight = powerAttackingEachTarget[right.targetIndex];
const bool leftAlreadyOverwhelmed =
powerAttackingLeft >= kOverpowerRatio * left.targetPower;
@@ -215,8 +230,7 @@ auto GenerateTargetPriorities(
});
if (!targetsWithDistance.empty()) {
powerAttackingEachTarget[CoordsIndex(targetsWithDistance.front().target, cc)] +=
Power(unit);
powerAttackingEachTarget[targetsWithDistance.front().targetIndex] += Power(unit);
}
tpl.priorityOrder.reserve(targetsWithDistance.size());
@@ -114,8 +114,7 @@ auto AttackLocationsCache::CachedLocations(const CoordsSet &targets, const bool
-> AttackLocations {
AttackLocations al(rowCount, columnCount);
for (const auto &target : targets) {
const int index = target.row() * columnCount + target.column();
for (const auto index : targets.indexIterator()) {
if (lateGame) {
al += lateGameLocations[index];
} else {
@@ -137,4 +136,4 @@ auto AttackLocationsCache::CachedLocations(const vector<const Unit *> &enemies,
return CachedLocations(targets, lateGame);
}
} // namespace shardok
} // namespace shardok
@@ -116,8 +116,9 @@ auto DefenderDistanceBuf(
double sum = 0.0;
double decr = kPerUnitDebufDecay;
for (const WithoutAndWith &pointCost : pointCosts) {
const int effectiveCost =
(pointCost.without == 999) ? int(1.2 * pointCost.with) : pointCost.without;
const int effectiveCost = (pointCost.without == 999)
? static_cast<int>(1.2 * pointCost.with)
: pointCost.without;
sum += decr / (effectiveCost + 1);
decr *= kPerUnitDebufDecay;
}
@@ -5,15 +5,34 @@
#include "AIMinimumDistanceAndTarget.hpp"
namespace shardok {
namespace {
auto CoordsIndex(const Coords &coords, const int columnCount) -> int {
return coords.row() * columnCount + coords.column();
}
auto CoordsFromIndex(const size_t index, const int columnCount) -> Coords {
return Coords(
static_cast<int8_t>(index / static_cast<size_t>(columnCount)),
static_cast<int8_t>(index % static_cast<size_t>(columnCount)));
}
} // namespace
auto MinimumDistanceAndTarget(
const ActionPointDistances *apd,
const Coords &origin,
const CoordsSet &destinations) -> CoordsAndDistance {
CoordsAndDistance min{Coords(-1, -1), ActionPointDistances::IMPOSSIBLE};
for (const auto &dest : destinations) {
const DIST_T dist = apd->Distance(origin, dest);
const int columnCount = destinations.ColumnCount();
const int originIndex = CoordsIndex(origin, columnCount);
for (const auto destIndex : destinations.indexIterator()) {
const DIST_T dist = apd->Distance(originIndex, static_cast<int>(destIndex));
if (dist == ActionPointDistances::IMPOSSIBLE) continue;
if (dist < min.distance) min = CoordsAndDistance{dest, dist};
if (dist < min.distance) {
min = CoordsAndDistance{CoordsFromIndex(destIndex, columnCount), dist};
}
}
return min;
@@ -25,4 +44,4 @@ auto MinimumDistance(
const CoordsSet &destinations) -> int {
return MinimumDistanceAndTarget(apd, origin, destinations).distance;
}
} // namespace shardok
} // namespace shardok
@@ -25,7 +25,8 @@ using Unit = net::eagle0::shardok::storage::fb::Unit;
net::eagle0::shardok::storage::fb::Profession profession) -> bool;
[[nodiscard]] auto CastleClaimCapableAttackerUnitCount(const GameStateW &gameState) -> int;
[[nodiscard]] auto PlayerInfoForPid(const GameStateW &, PlayerId pid) -> const PlayerInfo *;
[[nodiscard]] auto PlayerInfoForPid(const GameStateW &gameState, PlayerId pid)
-> const PlayerInfo *;
} // namespace shardok
@@ -88,8 +88,8 @@ auto ContextFreeUnitValue(const Unit *unit) -> ScoreValue {
break;
}
const double battalionValue = battalionTypeMultiplier * (1.0 + armament / 100.0) *
(1.0 + training / 100.0) * (0.5 + morale / 100.0) *
const double battalionValue = battalionTypeMultiplier * (1.0 + (armament / 100.0)) *
(1.0 + (training / 100.0)) * (0.5 + (morale / 100.0)) *
unit->battalion().size();
const double heroValue =
@@ -120,15 +120,16 @@ auto archeryValue(const vector<const Unit *> &enemyUnits, const bool canShootNow
auto reduceValue(const Unit *unit, const Terrain *unitTerrain) -> double {
// TODO: make this depend on value of reducing castle too
if (unit->fortified() || unitTerrain->modifier().castle().present())
if (unit->fortified() || unitTerrain->modifier().castle().present()) {
return kReduceFortifiedPossibleValue;
else if (AllowsFortification(unitTerrain))
return kReduceUnfortifiedPossibleValue;
else
return 0.0;
}
if (AllowsFortification(unitTerrain)) { return kReduceUnfortifiedPossibleValue; }
return 0.0;
}
auto fearValue(const vector<const Unit *> &) -> double { return kFearPossibleValue; }
auto fearValue([[maybe_unused]] const vector<const Unit *> &enemyUnits) -> double {
return kFearPossibleValue;
}
auto lightningValue(const Unit *unit) -> double {
// TODO: make this depend on the value of the targets
@@ -235,9 +236,8 @@ auto meteorValue(
multiplier = kMeteorNoneRoundMultiplier * vigorFactor;
}
return maxValue * multiplier;
} else {
return 0.0;
}
return 0.0;
}
case net::eagle0::shardok::storage::fb::MultiroundMagicState_TARGET:
@@ -247,9 +247,8 @@ auto meteorValue(
map,
enemyOccupants,
friendlyOccupants);
} else {
return bestTargetValue(unit, map, enemyOccupants, friendlyOccupants, meteorRange);
}
return bestTargetValue(unit, map, enemyOccupants, friendlyOccupants, meteorRange);
case net::eagle0::shardok::storage::fb::MultiroundMagicState_CAST:
return kMeteorCastRoundMultiplier *
meteorDropRawValue(
@@ -169,6 +169,7 @@ auto WaterCrossingTiles(
auto *terr = const_cast<net::eagle0::shardok::storage::fb::Terrain *>(
mapCopy->mutable_terrain()->GetMutableObject(index));
terr->mutable_modifier().mutable_bridge().mutate_present(true);
terr->mutable_modifier().mutable_bridge().mutate_integrity(100.0);
terr->mutable_modifier().mutable_fire().mutate_present(false);
auto hash = ActionPointDistancesCache::GetMapId(mapCopy);
@@ -133,8 +133,7 @@ auto DefenderHoldsCriticalTilesVictoryScore(
const auto cc = gameState->hex_map()->column_count();
const auto occupants = Occupants(*gameState->units(), rc, cc);
for (const Coords& criticalTileLocation : criticalTileLocations) {
const auto index = criticalTileLocation.row() * cc + criticalTileLocation.column();
for (const auto index : criticalTileLocations.indexIterator()) {
const auto& occupant = occupants[index];
if (occupant && occupant->battalion().type() !=
@@ -32,7 +32,7 @@ public:
* Initialize game settings from the default configuration files.
* Must be called before creating game states.
*/
static auto InitializeGameSettings() -> GameSettingsSPtr;
[[nodiscard]] static auto InitializeGameSettings() -> GameSettingsSPtr;
/**
* Create the standard "Perf" test configuration:
@@ -44,7 +44,7 @@ public:
* @param defenderToggle If true, AI is defender; if false, AI is attacker
* @return A GameStateW with the configured battle
*/
static auto CreatePerfTestGameState(
[[nodiscard]] static auto CreatePerfTestGameState(
const GameSettingsSPtr& settings,
bool defenderToggle = false) -> GameStateW;
@@ -58,7 +58,7 @@ public:
* @param defenderToggle If true, AI is defender; if false, AI is attacker
* @return A GameStateW with the configured battle
*/
static auto CreateCustomTestGameState(
[[nodiscard]] static auto CreateCustomTestGameState(
const GameSettingsSPtr& settings,
const std::string& mapName,
int aiUnitCount,
@@ -67,11 +67,11 @@ public:
private:
// Helper functions for building game state components
static auto
[[nodiscard]] static auto
AddPlayerInfo(flatbuffers::FlatBufferBuilder& fbb, int playerId, bool isDefender, int food)
-> flatbuffers::Offset<net::eagle0::shardok::storage::fb::PlayerInfo>;
static auto AddGenericUnit(
[[nodiscard]] static auto AddGenericUnit(
PlayerId playerId,
UnitId unitId,
const net::eagle0::shardok::storage::fb::Coords& location,
@@ -54,7 +54,7 @@ public:
* @param mctsConfig MCTS settings, used only when algorithmType is MCTS
* @return Unique pointer to created ShardokAIClient
*/
static auto Create(
[[nodiscard]] static auto Create(
PlayerId playerId,
bool isDefender,
const net::eagle0::shardok::storage::fb::HexMap* hexMap,
@@ -29,7 +29,7 @@ public:
*
* @return Shared pointer to initialized GameSettings
*/
static auto CreateDefault() -> GameSettingsSPtr;
[[nodiscard]] static auto CreateDefault() -> GameSettingsSPtr;
};
} // namespace shardok::ai_testing_common
@@ -14,6 +14,7 @@
#include <iostream>
#include <iterator>
#include <ranges>
#include <sstream>
#include <string>
#include <thread>
@@ -27,6 +28,38 @@ using net::eagle0::common::ScopedShardokLatencyTrace;
using net::eagle0::shardok::common::GameStatus;
namespace {
[[nodiscard]] auto DescribeCommandForLog(const CommandSPtr &command) -> std::string {
if (!command) { return "<null command>"; }
std::ostringstream out;
out << "type=" << command->GetCommandType()
<< " player=" << static_cast<int>(command->GetPlayerId())
<< " actor=" << command->GetActorUnitId() << " target=("
<< static_cast<int>(command->GetTargetRow()) << ','
<< static_cast<int>(command->GetTargetColumn()) << ')';
if (command->HasOdds()) { out << " odds_percentile=" << command->GetOddsPercentile(); }
try {
out << " proto={" << command->GetCommandProto().ShortDebugString() << '}';
} catch (const std::exception &e) { out << " proto_error={" << e.what() << '}'; }
return out.str();
}
[[nodiscard]] auto DescribeCommandsForLog(const CommandList &commands) -> std::string {
std::ostringstream out;
out << "available_command_count=" << commands.size();
for (size_t i = 0; i < commands.size(); ++i) {
out << "\n [" << i << "] " << DescribeCommandForLog(commands[i]);
}
return out.str();
}
} // namespace
class InvalidTokenException : public std::exception {};
class MismatchedPlayerIdException : public std::exception {
@@ -154,6 +187,8 @@ void ShardokGameController::DoAIThread() {
continue;
}
std::string aiPhase = "choosing command";
int64_t chosenIndex = -1;
try {
// Phase 2: AI thinks (NO LOCK - this is the slow part)
auto results = [&] {
@@ -165,6 +200,7 @@ void ShardokGameController::DoAIThread() {
{"available_command_count", std::to_string(availableCommands->size())}});
return aiClient->ChooseCommandIndex(settings, gsv, availableCommands);
}();
chosenIndex = results.chosenIndex;
// Phase 3: Post the command (brief lock)
{
@@ -183,6 +219,7 @@ void ShardokGameController::DoAIThread() {
}
{
aiPhase = "posting chosen command";
ScopedShardokLatencyTrace trace(
"ai_engine_post_command",
cachedGameId,
@@ -193,6 +230,7 @@ void ShardokGameController::DoAIThread() {
}
LockedNotifyClients();
{
aiPhase = "posting forced follow-up commands";
ScopedShardokLatencyTrace trace(
"ai_engine_post_forced_commands",
cachedGameId,
@@ -206,6 +244,17 @@ void ShardokGameController::DoAIThread() {
} catch (const std::exception &e) {
std::cerr << "AI thread exception in game " << cachedGameId << ", player " << playerId
<< ", round " << gsv.current_round() << ": " << e.what() << '\n';
std::cerr << "AI failure context: phase=" << aiPhase
<< ", expected_history_count=" << expectedHistoryCount
<< ", current_history_count=" << engine->GetUnfilteredHistoryCount()
<< ", chosen_index=" << chosenIndex
<< ", current_player=" << static_cast<int>(engine->GetCurrentPlayerId())
<< '\n';
if (availableCommands) {
std::cerr << DescribeCommandsForLog(*availableCommands) << '\n';
} else {
std::cerr << "available_command_count=<cleared>" << '\n';
}
void *backtraceArray[20];
const int backtraceSize = backtrace(backtraceArray, 20);
@@ -189,7 +189,8 @@ struct BattalionType {
}
[[nodiscard]] auto GetCostToEnterTerrain(const TerrainProto &terrain) const -> ActionCost {
if (HasBridge(terrain.modifier())) {
if (HasBridge(terrain.modifier()) && terrain.modifier().bridge().has_integrity() &&
terrain.modifier().bridge().integrity().value() > 0.0) {
return {ActionCost::standard,
ActionPoints(std::ceil(
100.0 * minimumCostToEnterBridge /
@@ -208,7 +209,7 @@ struct BattalionType {
[[nodiscard]] auto GetCostToEnterTerrain(
const net::eagle0::shardok::storage::fb::Terrain_::Type terrainType,
const net::eagle0::shardok::storage::fb::TileModifier &tm) const -> ActionCost {
if (tm.bridge().present()) {
if (tm.bridge().present() && tm.bridge().integrity() > 0.0) {
return {ActionCost::standard,
ActionPoints(
std::ceil(100.0 * minimumCostToEnterBridge / tm.bridge().integrity()))};
@@ -22,13 +22,13 @@ namespace shardok {
using PercentileRollOdds = net::eagle0::shardok::storage::Odds;
using OtherFactor = net::eagle0::shardok::storage::Odds_OtherFactor;
double BonusFromStat(double stat);
[[nodiscard]] double BonusFromStat(double stat);
int16_t AverageBonusFromStats(const std::vector<double> &stats);
[[nodiscard]] int16_t AverageBonusFromStats(const std::vector<double> &stats);
OtherFactor MakeOtherFactor(int value, const std::string &id);
[[nodiscard]] OtherFactor MakeOtherFactor(int value, const std::string &id);
PercentileRollOdds MakeOdds(
[[nodiscard]] PercentileRollOdds MakeOdds(
int base = 0,
int terr = 0,
int weath = 0,
@@ -36,7 +36,7 @@ PercentileRollOdds MakeOdds(
int statF = 0,
std::span<const OtherFactor> others = {});
PercentileRollOdds MakeOdds(
[[nodiscard]] PercentileRollOdds MakeOdds(
int base,
int terr,
int weath,
@@ -44,11 +44,11 @@ PercentileRollOdds MakeOdds(
const std::vector<double> &stats,
std::span<const OtherFactor> others = {});
int16_t GetTotalOdds(const PercentileRollOdds &odds);
[[nodiscard]] int16_t GetTotalOdds(const PercentileRollOdds &odds);
bool PercentileRollSucceeds(const PercentileRollOdds &odds, double roll);
[[nodiscard]] bool PercentileRollSucceeds(const PercentileRollOdds &odds, double roll);
int16_t GetSuccessChance(const PercentileRollOdds &odds);
[[nodiscard]] int16_t GetSuccessChance(const PercentileRollOdds &odds);
} // namespace shardok
#endif // EAGLE0_SHARDOK_LIBRARY_PERCENTILE_ROLL_ODDS_HPP
@@ -718,8 +718,8 @@ auto SecuredByPlayerByUnitIdFromHistory(
continue;
}
if (!resolvedUnit.has_secured_by_player()) { continue; }
const auto *unit = reinterpret_cast<const Unit *>(resolvedUnit.unit_bytes().data());
securedByPlayerByUnitId[unit->unit_id()] = resolvedUnit.secured_by_player().value();
const auto unit = UnitFromBytes(resolvedUnit.unit_bytes());
securedByPlayerByUnitId[unit.unit_id()] = resolvedUnit.secured_by_player().value();
}
}
return securedByPlayerByUnitId;
@@ -14,6 +14,7 @@
#include "src/main/cpp/net/eagle0/shardok/library/action_result_applier/UnitHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/HexMapHasher.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/unit/Unit.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/ActionResultFlatbufferHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
@@ -30,10 +31,6 @@ inline auto GetMutableUnit(GameState *state, UnitId unitId) -> Unit * {
return const_cast<Unit *>(state->mutable_units()->GetMutableObject(unitId));
}
inline auto UnitFromBytes(const std::string &unitBytes) -> const Unit & {
return *reinterpret_cast<const Unit *>(unitBytes.data());
}
using net::eagle0::shardok::storage::fb::VictoryCondition;
using net::eagle0::shardok::storage::fb::VictoryType;
@@ -101,7 +98,7 @@ void ApplyResolvedUnit(
GameStateT &inoutState,
const ResolvedUnitProto &resolvedUnit,
const double vipCapturedMoraleAdjustment) {
const Unit &unit = UnitFromBytes(resolvedUnit.unit_bytes());
const Unit unit = UnitFromBytes(resolvedUnit.unit_bytes());
UnitId unitId = unit.unit_id();
if (unit.has_attached_hero() &&
@@ -169,15 +166,15 @@ void MutatingAddUnits(GameStateW &mutatingState, const ActionResultProto &result
// First pass: check what kind of modifications we need
for (const auto &unitBytes : result.changed_units_fb()) {
const auto *unit = &UnitFromBytes(unitBytes);
maxChangedUnitId = std::max(maxChangedUnitId, unit->unit_id());
const auto unit = UnitFromBytes(unitBytes);
maxChangedUnitId = std::max(maxChangedUnitId, unit.unit_id());
if (static_cast<unsigned int>(unit->unit_id()) >= mutatingState->units()->size()) {
if (static_cast<unsigned int>(unit.unit_id()) >= mutatingState->units()->size()) {
// Unit ID beyond vector size - must expand
needsVectorExpansion = true;
break; // No point checking further
} else if (
mutatingState->units()->Get(unit->unit_id())->status() ==
mutatingState->units()->Get(unit.unit_id())->status() ==
net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT) {
// Unit wants to use a reserved slot
needsReservedSlotConversion = true;
@@ -199,8 +196,8 @@ void MutatingAddUnits(GameStateW &mutatingState, const ActionResultProto &result
// Convert reserved slots to real units in place
// We only need to process the units that are being changed
for (const auto &unitBytes : result.changed_units_fb()) {
const auto *unit = &UnitFromBytes(unitBytes);
auto *mutableUnit = GetMutableUnit(mutatingState.Get(), unit->unit_id());
const auto unit = UnitFromBytes(unitBytes);
auto *mutableUnit = GetMutableUnit(mutatingState.Get(), unit.unit_id());
if (mutableUnit->status() ==
net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT) {
// Convert this reserved slot to a real unit
@@ -299,51 +296,48 @@ void MutatingApplyResult(
const double vipCapturedMoraleAdjustment = settings.Backing().vip_captured_morale_adjustment();
for (const auto &changedUnitBytes : result.changed_units_fb()) {
const Unit *changedUnit = &UnitFromBytes(changedUnitBytes);
const Unit changedUnit = UnitFromBytes(changedUnitBytes);
internalAssert(
changedUnit->unit_id() < static_cast<int32_t>(mutatingGameState->units()->size()),
changedUnit.unit_id() < static_cast<int32_t>(mutatingGameState->units()->size()),
"Got a changed unit outside acceptable range");
const auto battalionSizeAfter = changedUnit->battalion().size();
const auto battalionSizeAfter = changedUnit.battalion().size();
const int battalionSizeBefore =
(changedUnit->unit_id() < static_cast<int32_t>(initialUnitCount))
? mutatingGameState->units()
->Get(changedUnit->unit_id())
->battalion()
.size()
: changedUnit->battalion().size();
(changedUnit.unit_id() < static_cast<int32_t>(initialUnitCount))
? mutatingGameState->units()->Get(changedUnit.unit_id())->battalion().size()
: changedUnit.battalion().size();
auto status = changedUnit->status();
if (IsDestroyed(*changedUnit)) {
auto status = changedUnit.status();
if (IsDestroyed(changedUnit)) {
status =
changedUnit->has_attached_hero()
changedUnit.has_attached_hero()
? net::eagle0::shardok::storage::fb::UnitStatus_CAPTURED_UNIT
: net::eagle0::shardok::storage::fb::UnitStatus_DESTROYED_SUMMONED_UNIT;
ApplyResolvedUnit(
mutatingGameState.Get(),
*changedUnit,
changedUnit,
status,
vipCapturedMoraleAdjustment,
settings);
}
// Capture old position before applying changes
auto *mutableUnit = GetMutableUnit(mutatingGameState.Get(), changedUnit->unit_id());
auto *mutableUnit = GetMutableUnit(mutatingGameState.Get(), changedUnit.unit_id());
const auto oldLocation = mutableUnit->location();
fb::ApplyUnit(mutableUnit, changedUnit, status);
fb::ApplyUnit(mutableUnit, &changedUnit, status);
// Update occupied tiles bitfield if position changed
const auto &newLocation = changedUnit->location();
const auto &newLocation = changedUnit.location();
if (oldLocation.row() != newLocation.row() ||
oldLocation.column() != newLocation.column()) {
mutatingGameState.UpdateOccupiedTile(oldLocation, newLocation);
}
if (battalionSizeBefore != battalionSizeAfter) {
if (changedUnit->battalion().type() ==
if (changedUnit.battalion().type() ==
net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD) {
if (battalionSizeAfter > battalionSizeBefore) {
// Undead consumed some bodies
@@ -355,7 +349,7 @@ void MutatingApplyResult(
if (battalionSizeAfter > battalionSizeBefore) {
std::ostringstream stringStream;
stringStream << "Battalion got larger! Was " << battalionSizeBefore << ", now "
<< changedUnit << " result was " << result.DebugString();
<< &changedUnit << " result was " << result.DebugString();
throw ShardokInternalErrorException(stringStream.str());
}
@@ -365,7 +359,7 @@ void MutatingApplyResult(
if (newCount != 0) {
newArmament =
(mutatingGameState->dead_count() * mutatingGameState->dead_armament() +
casualties * changedUnit->battalion().armament()) /
casualties * changedUnit.battalion().armament()) /
newCount;
}
@@ -16,6 +16,7 @@ cc_library(
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
"//src/main/cpp/net/eagle0/shardok/library/map:hex_map_hasher",
"//src/main/cpp/net/eagle0/shardok/library/unit",
"//src/main/cpp/net/eagle0/shardok/library/util:action_result_flatbuffer_helpers",
"//src/main/cpp/net/eagle0/shardok/library/util:hex_map_utils",
"//src/main/protobuf/net/eagle0/shardok/storage:action_result_cc_proto",
],
@@ -60,7 +60,8 @@ auto StartPlayerTurnAction::InternalExecute(
->Get(startingState->month() - 1)
->is_winter() &&
!inCastle) {
effectiveBase += float(settingsGetter.Backing().winter_morale_adjustment());
effectiveBase +=
static_cast<float>(settingsGetter.Backing().winter_morale_adjustment());
}
float moraleDiff = effectiveBase - updatedUnit.battalion().morale();
MutatingMaybeAdjustMorale(
@@ -34,7 +34,8 @@ auto UndeadChangeAction::InternalExecute(
int growth = newCount - decayedSize;
float newArmament =
float(unit->battalion().armament() * decayedSize + armament * growth) / newCount;
static_cast<float>(unit->battalion().armament() * decayedSize + armament * growth) /
newCount;
auto newUnit = *unit;
newUnit.mutable_battalion().mutate_size(newCount);
@@ -11,7 +11,7 @@
namespace shardok {
class EvacuatePrisonersCommandFactory : public CommandFactory {
const SettingsGetter& settings;
const SettingsGetter settings;
public:
explicit EvacuatePrisonersCommandFactory(const SettingsGetter& getter);
@@ -61,9 +61,11 @@ void FleeCommandFactory::AddAvailableFleeCommands(
const auto *occupant = Occupant(allUnits, twoAwayCoords);
if (occupant && !occupant->hidden()) {
if (std::ranges::contains(allyPids, occupant->player_id())) {
adjacentFriendliesMod += int32_t(perAdjacentFriendly * adjustmentForTwoAway);
adjacentFriendliesMod +=
static_cast<int32_t>(perAdjacentFriendly * adjustmentForTwoAway);
} else {
adjacentEnemiesMod += int32_t(perAdjacentEnemy * adjustmentForTwoAway);
adjacentEnemiesMod +=
static_cast<int32_t>(perAdjacentEnemy * adjustmentForTwoAway);
}
}
}
@@ -59,7 +59,7 @@ auto HolyWaveDamageAction::InternalExecute(
resultProto.set_type(net::eagle0::shardok::common::HOLY_WAVE_DAMAGE);
const Unit *damagedUnit = currentState->units()->Get(damagedUnitId);
auto newSize = int32_t(damagedUnit->battalion().size() * (1.0 - damagePercentage));
auto newSize = static_cast<int32_t>(damagedUnit->battalion().size() * (1.0 - damagePercentage));
auto damagedUnitAfter = *damagedUnit;
damagedUnitAfter.mutable_battalion().mutate_size(newSize);
@@ -171,7 +171,8 @@ auto SetupInitialGameState(
net::eagle0::shardok::storage::fb::MultiroundMagicState_NONE);
if (modifiedUnit.can_archery()) {
modifiedUnit.mutate_volleys_remaining(
int32_t(modifiedUnit.battalion().armament() *
static_cast<int32_t>(
modifiedUnit.battalion().armament() *
settings.Backing().armament_to_volleys()) +
settings.GetBattalionType(modifiedUnit.battalion().type())->bonusVolleys);
}
@@ -1,4 +1,4 @@
load("@rules_cc//cc:defs.bzl", "cc_library")
load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library")
load("//tools:copts.bzl", "COPTS")
cc_library(
@@ -28,6 +28,14 @@ cc_library(
],
)
cc_binary(
name = "coords_set_benchmark",
srcs = ["CoordsSetBenchmark.cpp"],
copts = COPTS,
visibility = ["//visibility:public"],
deps = [":coords_set"],
)
cc_library(
name = "hex_map_direction",
hdrs = ["HexMapDirection.hpp"],
@@ -60,7 +60,7 @@ namespace std {
template<>
struct hash<net::eagle0::shardok::common::Coords> {
[[nodiscard]] auto operator()(const net::eagle0::shardok::common::Coords &c) const -> size_t {
return size_t(c.row() << 16 | c.column());
return static_cast<size_t>(c.row() << 16 | c.column());
}
};
} // namespace std
@@ -6,7 +6,10 @@
#define EAGLE0_SHARDOK_LIBRARY_MAP_COORDS_SET_HPP
#include <bit>
#include <functional>
#include <cassert>
#include <cstdint>
#include <iterator>
#include <limits>
#include <utility>
#include <vector>
@@ -17,7 +20,11 @@
namespace shardok {
#ifndef NDEBUG
constexpr bool kCoordsSetIndexChecks = true;
#else
constexpr bool kCoordsSetIndexChecks = false;
#endif
constexpr size_t STANDARD_SIZE = size_t{12} * 14;
@@ -28,13 +35,28 @@ class CoordsSet {
private:
BackingBitset store;
int mapWidth;
int mapHeight;
uint8_t columnCount;
uint8_t rowCount;
uint32_t indexCount;
[[maybe_unused]] uint16_t indexCount;
void AssertCoordsInBounds([[maybe_unused]] const int row, [[maybe_unused]] const int column)
const {
if constexpr (kCoordsSetIndexChecks) {
assert(row >= 0);
assert(column >= 0);
assert(row < rowCount);
assert(column < columnCount);
}
}
void AssertIndexInBounds([[maybe_unused]] const uint32_t idx) const {
if constexpr (kCoordsSetIndexChecks) { assert(idx < indexCount); }
}
[[nodiscard]] auto Index(const int row, const int column) const -> int {
return (row * mapWidth) + column;
AssertCoordsInBounds(row, column);
return (row * columnCount) + column;
}
[[nodiscard]] auto Index(const Coords& c) const -> int { return Index(c.row(), c.column()); }
@@ -49,18 +71,25 @@ private:
};
void Remove(uint32_t idx) {
if (kCoordsSetIndexChecks) { assert(idx < indexCount); }
AssertIndexInBounds(idx);
store.Unset(idx);
}
[[nodiscard]] auto Contains(uint32_t idx) const -> bool {
if (kCoordsSetIndexChecks) { assert(idx < indexCount); }
AssertIndexInBounds(idx);
return store.IsSet(idx);
}
public:
CoordsSet(int w, int h) : mapWidth(w), mapHeight(h), indexCount(w * h) {
if (kCoordsSetIndexChecks) {
assert(static_cast<size_t>(w) * static_cast<size_t>(h) < STANDARD_SIZE);
CoordsSet(const int w, const int h)
: columnCount(static_cast<uint8_t>(w)),
rowCount(static_cast<uint8_t>(h)),
indexCount(static_cast<uint16_t>(w * h)) {
if constexpr (kCoordsSetIndexChecks) {
assert(w >= 0);
assert(h >= 0);
assert(w <= std::numeric_limits<uint8_t>::max());
assert(h <= std::numeric_limits<uint8_t>::max());
assert(static_cast<size_t>(w) * static_cast<size_t>(h) <= STANDARD_SIZE);
}
clear();
}
@@ -74,15 +103,20 @@ public:
auto operator=(const CoordsSet& rhs) -> CoordsSet& = default;
auto operator=(CoordsSet&& rhs) -> CoordsSet& = default;
auto operator==(const CoordsSet& rhs) const -> bool {
if (mapWidth != rhs.mapWidth) return false;
if (mapHeight != rhs.mapHeight) return false;
[[nodiscard]] auto operator==(const CoordsSet& rhs) const -> bool {
if (columnCount != rhs.columnCount) return false;
if (rowCount != rhs.rowCount) return false;
return store == rhs.store;
}
void Add(const Coords& c) { AddIndex(Index(c)); }
void Add(const int row, const int column) { AddIndex(Index(row, column)); }
void AddIndex(const uint32_t idx) { store.Set(idx); }
void Add(const Coords& c) { AddMapIndexUnchecked(Index(c)); }
void Add(const int row, const int column) { AddMapIndexUnchecked(Index(row, column)); }
// For callers that already have row * column_count + column.
void AddMapIndexUnchecked(const uint32_t idx) {
AssertIndexInBounds(idx);
store.Set(idx);
}
// Always succeeds
void Remove(const Coords& c) { Remove(Index(c)); }
@@ -101,13 +135,15 @@ public:
[[nodiscard]] auto empty() const -> bool { return store.empty(); }
[[nodiscard]] auto ColumnCount() const -> int { return columnCount; }
// Should only be used for debugging.
[[nodiscard]] auto as_vector() const -> std::vector<Coords> {
return std::vector<Coords>{this->begin(), this->end()};
}
auto operator+=(const CoordsSet& rhs) -> CoordsSet& {
if (mapWidth != rhs.mapWidth || mapHeight != rhs.mapHeight) {
if (columnCount != rhs.columnCount || rowCount != rhs.rowCount) {
throw MismatchException("mismatched sizes");
}
@@ -115,14 +151,14 @@ public:
return *this;
}
auto operator+(const CoordsSet& rhs) const -> CoordsSet {
[[nodiscard]] auto operator+(const CoordsSet& rhs) const -> CoordsSet {
CoordsSet cs = *this;
cs += rhs;
return cs;
}
auto operator-=(const CoordsSet& rhs) -> CoordsSet& {
if (mapWidth != rhs.mapWidth || mapHeight != rhs.mapHeight) {
if (columnCount != rhs.columnCount || rowCount != rhs.rowCount) {
throw MismatchException("mismatched sizes");
}
@@ -130,15 +166,25 @@ public:
return *this;
}
auto operator-(const CoordsSet& rhs) const -> CoordsSet {
[[nodiscard]] auto operator-(const CoordsSet& rhs) const -> CoordsSet {
CoordsSet cs = *this;
cs -= rhs;
return cs;
}
void Filter(const std::function<bool(int, int)>& f) {
for (uint32_t i = 0; i < indexCount; ++i) {
if (Contains(i) && !f(i / mapWidth, i % mapWidth)) { Remove(i); }
template<class Func>
void Filter(Func&& f) {
if (size() * 4 > indexCount) {
for (uint32_t i = 0; i < indexCount; ++i) {
if (Contains(i) && !f(i / columnCount, i % columnCount)) { Remove(i); }
}
return;
}
for (auto iter = store.begin(); iter != store.end();) {
const uint32_t i = static_cast<uint32_t>(*iter);
++iter;
if (!f(i / columnCount, i % columnCount)) { Remove(i); }
}
}
@@ -146,9 +192,9 @@ public:
public:
using iterator_category = std::forward_iterator_tag;
using value_type = Coords;
using difference_type = int;
using pointer = Coords*;
using reference = Coords&;
using difference_type = std::ptrdiff_t;
using pointer = void;
using reference = Coords;
private:
BackingBitset::Iter iterator;
@@ -158,15 +204,28 @@ public:
public:
Iter(const CoordsSet& cs, BackingBitset::Iter iter) : iterator(iter), cs(cs) {}
auto operator==(const Iter& rhs) const -> bool { return iterator == rhs.iterator; }
auto operator!=(const Iter& rhs) const -> bool { return iterator != rhs.iterator; }
void operator++() { ++iterator; }
[[nodiscard]] auto operator==(const Iter& rhs) const -> bool {
return iterator == rhs.iterator;
}
[[nodiscard]] auto operator!=(const Iter& rhs) const -> bool {
return iterator != rhs.iterator;
}
auto operator++() -> Iter& {
++iterator;
return *this;
}
auto operator*() const -> Coords {
auto operator++(int) -> Iter {
Iter previous = *this;
++(*this);
return previous;
}
[[nodiscard]] auto operator*() const -> Coords {
const size_t index = *iterator;
const int row = index / cs.mapWidth;
const int column = index % cs.mapWidth;
const int row = static_cast<int>(index / static_cast<size_t>(cs.columnCount));
const int column = static_cast<int>(index % static_cast<size_t>(cs.columnCount));
return Coords(row, column);
}
};
@@ -185,25 +244,21 @@ public:
[[nodiscard]] auto end() const -> BackingBitset::Iter { return cs.store.end(); }
};
[[nodiscard]] auto indexIterator() const -> IndexIterator { return IndexIterator(*this); }
[[nodiscard]] auto ToString() const -> std::string {
return std::string(reinterpret_cast<char const*>(&store), STANDARD_SIZE);
}
};
} // namespace shardok
namespace common {
static inline auto Contains(const shardok::CoordsSet& container, const shardok::Coords& elt)
-> bool {
[[nodiscard]] static inline auto Contains(
const shardok::CoordsSet& container,
const shardok::Coords& elt) -> bool {
return container.Contains(elt);
}
template<class Func>
void FilterInPlace(shardok::CoordsSet& container, Func fn) {
for (const shardok::Coords& c : container) {
if (!fn(c)) container.Remove(c);
}
container.Filter(
[&fn](const int row, const int column) { return fn(shardok::Coords(row, column)); });
}
} // namespace common
@@ -0,0 +1,204 @@
#include <chrono>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <string_view>
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
namespace shardok {
namespace {
constexpr int kColumnCount = 12;
constexpr int kRowCount = 14;
constexpr int kIndexCount = kColumnCount * kRowCount;
volatile uint64_t benchmarkSink = 0;
template<class T>
void DoNotOptimize(const T& value) {
#if defined(__GNUC__) || defined(__clang__)
asm volatile("" : : "g"(&value) : "memory");
#else
benchmarkSink ^= reinterpret_cast<uintptr_t>(&value);
#endif
}
struct BenchmarkResult {
std::string_view name;
int iterations;
std::chrono::nanoseconds elapsed;
uint64_t checksum;
};
void Consume(uint64_t checksum) { benchmarkSink = checksum; }
[[nodiscard]] auto MakePatternedCoordsSet(const int step, const int offset = 0) -> CoordsSet {
CoordsSet coords(kColumnCount, kRowCount);
for (int index = offset; index < kIndexCount; index += step) {
coords.AddMapIndexUnchecked(static_cast<uint32_t>(index));
}
return coords;
}
template<class Func>
[[nodiscard]] auto RunBenchmark(std::string_view name, const int iterations, Func func)
-> BenchmarkResult {
const auto start = std::chrono::steady_clock::now();
const uint64_t checksum = func(iterations);
const auto end = std::chrono::steady_clock::now();
Consume(checksum);
return BenchmarkResult{
name,
iterations,
std::chrono::duration_cast<std::chrono::nanoseconds>(end - start),
checksum};
}
void PrintResult(const BenchmarkResult& result) {
const double nsPerIteration =
static_cast<double>(result.elapsed.count()) / static_cast<double>(result.iterations);
std::cout << std::left << std::setw(28) << result.name << " iterations=" << std::right
<< std::setw(10) << result.iterations << " ns/op=" << std::fixed
<< std::setprecision(2) << std::setw(10) << nsPerIteration
<< " checksum=" << result.checksum << '\n';
}
[[nodiscard]] auto BenchmarkCopies(const int iterations) -> uint64_t {
const CoordsSet source = MakePatternedCoordsSet(3);
uint64_t checksum = 0;
for (int i = 0; i < iterations; ++i) {
CoordsSet copy = source;
DoNotOptimize(copy);
checksum += copy.size();
}
return checksum;
}
[[nodiscard]] auto BenchmarkUnions(const int iterations) -> uint64_t {
const CoordsSet left = MakePatternedCoordsSet(3);
const CoordsSet right = MakePatternedCoordsSet(5, 1);
uint64_t checksum = 0;
for (int i = 0; i < iterations; ++i) {
CoordsSet combined = left;
combined += right;
DoNotOptimize(combined);
checksum += combined.size();
}
return checksum;
}
[[nodiscard]] auto BenchmarkSubtractions(const int iterations) -> uint64_t {
const CoordsSet full = MakePatternedCoordsSet(1);
const CoordsSet removed = MakePatternedCoordsSet(4);
uint64_t checksum = 0;
for (int i = 0; i < iterations; ++i) {
CoordsSet remaining = full;
remaining -= removed;
DoNotOptimize(remaining);
checksum += remaining.size();
}
return checksum;
}
[[nodiscard]] auto BenchmarkContains(const int iterations) -> uint64_t {
const CoordsSet coords = MakePatternedCoordsSet(3);
uint64_t checksum = 0;
for (int i = 0; i < iterations; ++i) {
const int index = i % kIndexCount;
checksum += coords.Contains(index / kColumnCount, index % kColumnCount) ? 1 : 0;
}
return checksum;
}
[[nodiscard]] auto BenchmarkCoordinateIteration(const int iterations) -> uint64_t {
const CoordsSet coords = MakePatternedCoordsSet(3);
uint64_t checksum = 0;
for (int i = 0; i < iterations; ++i) {
for (const auto& coord : coords) {
checksum += static_cast<uint64_t>(coord.row() + coord.column());
}
}
return checksum;
}
[[nodiscard]] auto BenchmarkIndexIteration(const int iterations) -> uint64_t {
const CoordsSet coords = MakePatternedCoordsSet(3);
uint64_t checksum = 0;
for (int i = 0; i < iterations; ++i) {
for (const auto index : coords.indexIterator()) { checksum += index; }
}
return checksum;
}
[[nodiscard]] auto BenchmarkDenseFilter(const int iterations) -> uint64_t {
const CoordsSet source = MakePatternedCoordsSet(1);
uint64_t checksum = 0;
for (int i = 0; i < iterations; ++i) {
CoordsSet filtered = source;
filtered.Filter([](const int row, const int column) { return (row + column) % 3 == 0; });
DoNotOptimize(filtered);
checksum += filtered.size();
}
return checksum;
}
[[nodiscard]] auto BenchmarkSparseFilter(const int iterations) -> uint64_t {
const CoordsSet source = MakePatternedCoordsSet(7);
uint64_t checksum = 0;
for (int i = 0; i < iterations; ++i) {
CoordsSet filtered = source;
filtered.Filter([](const int row, const int column) { return (row + column) % 3 == 0; });
DoNotOptimize(filtered);
checksum += filtered.size();
}
return checksum;
}
} // namespace
} // namespace shardok
auto main() -> int {
using shardok::BenchmarkContains;
using shardok::BenchmarkCoordinateIteration;
using shardok::BenchmarkCopies;
using shardok::BenchmarkDenseFilter;
using shardok::BenchmarkIndexIteration;
using shardok::BenchmarkSparseFilter;
using shardok::BenchmarkSubtractions;
using shardok::BenchmarkUnions;
using shardok::CoordsSet;
using shardok::PrintResult;
using shardok::RunBenchmark;
std::cout << "CoordsSet benchmark\n";
std::cout << "sizeof(CoordsSet)=" << sizeof(CoordsSet)
<< " alignof(CoordsSet)=" << alignof(CoordsSet) << '\n';
PrintResult(RunBenchmark("copy", 5'000'000, BenchmarkCopies));
PrintResult(RunBenchmark("union", 3'000'000, BenchmarkUnions));
PrintResult(RunBenchmark("subtraction", 3'000'000, BenchmarkSubtractions));
PrintResult(RunBenchmark("contains", 20'000'000, BenchmarkContains));
PrintResult(RunBenchmark("coord iteration", 1'000'000, BenchmarkCoordinateIteration));
PrintResult(RunBenchmark("index iteration", 1'000'000, BenchmarkIndexIteration));
PrintResult(RunBenchmark("dense filter", 500'000, BenchmarkDenseFilter));
PrintResult(RunBenchmark("sparse filter", 500'000, BenchmarkSparseFilter));
return 0;
}
@@ -25,7 +25,7 @@ using TileModifierProto = net::eagle0::shardok::common::TileModifier;
SingleModifier MakeSingleModifier(const std::optional<double>& integ = std::nullopt);
bool operator==(const SingleModifier& lhs, const SingleModifier& rhs);
[[nodiscard]] bool operator==(const SingleModifier& lhs, const SingleModifier& rhs);
TileModifierProto MakeTileModifier(bool onFire = false);
@@ -12,10 +12,10 @@
namespace shardok {
using TileModifierWithCoords = net::eagle0::shardok::common::TileModifierWithCoords;
auto MakeTmc(const Coords &coordinates, const TileModifierProto &modifier)
[[nodiscard]] auto MakeTmc(const Coords &coordinates, const TileModifierProto &modifier)
-> TileModifierWithCoords;
auto MakeTmc(
[[nodiscard]] auto MakeTmc(
const Coords &coordinates,
const net::eagle0::shardok::storage::fb::TileModifier &modifier) -> TileModifierWithCoords;
@@ -13,7 +13,7 @@ namespace shardok {
class SettingsLoader {
public:
static auto LoadSettings() -> std::shared_ptr<GameSettings>;
[[nodiscard]] static auto LoadSettings() -> std::shared_ptr<GameSettings>;
};
} // namespace shardok
@@ -5,7 +5,10 @@
#ifndef EAGLE0_SHARDOK_LIBRARY_UTIL_ACTION_RESULT_FLATBUFFER_HELPERS_HPP
#define EAGLE0_SHARDOK_LIBRARY_UTIL_ACTION_RESULT_FLATBUFFER_HELPERS_HPP
#include <cstring>
#include <stdexcept>
#include <string>
#include <type_traits>
#include "src/main/flatbuffer/net/eagle0/shardok/storage/unit.hpp"
#include "src/main/protobuf/net/eagle0/shardok/storage/action_result.pb.h"
@@ -16,13 +19,30 @@ using ActionResult = net::eagle0::shardok::storage::ActionResult;
using ResolvedUnit = net::eagle0::shardok::storage::ResolvedUnit;
using Unit = net::eagle0::shardok::storage::fb::Unit;
static_assert(std::is_trivially_copyable_v<Unit>);
[[nodiscard]] static inline auto UnitToBytes(const Unit& unit) -> std::string {
std::string bytes(sizeof(Unit), '\0');
std::memcpy(bytes.data(), &unit, sizeof(Unit));
return bytes;
}
[[nodiscard]] static inline auto UnitFromBytes(const std::string& unitBytes) -> Unit {
if (unitBytes.size() != sizeof(Unit)) {
throw std::invalid_argument("Unexpected unit byte length");
}
Unit unit{};
std::memcpy(&unit, unitBytes.data(), sizeof(Unit));
return unit;
}
static inline void AddChangedUnit(ActionResult& result, const Unit& unit) {
result.add_changed_units_fb(&unit, sizeof(Unit));
}
static inline void AddUnitToResolved(ResolvedUnit& resolvedUnit, const Unit& unit) {
*resolvedUnit.mutable_unit_bytes() =
std::string(reinterpret_cast<const char*>(&unit), sizeof(Unit));
*resolvedUnit.mutable_unit_bytes() = UnitToBytes(unit);
}
} // namespace shardok
@@ -6,6 +6,7 @@ cc_library(
hdrs = ["ActionResultFlatbufferHelpers.hpp"],
copts = COPTS,
visibility = [
"//src/main/cpp/net/eagle0/shardok:__subpackages__",
"//src/main/cpp/net/eagle0/shardok/library:__subpackages__",
"//src/test/cpp/net/eagle0/shardok/library:__subpackages__",
],
@@ -306,10 +306,9 @@ auto MakeActionResultFb(const ActionType type, const MeleeResultFb &results, con
ar.mutable_actor()->set_value(results.attackerAfter.unit_id());
ar.mutable_target_unit()->set_value(results.defenderAfter.unit_id());
AddChangedUnit(ar, results.attackerAfter);
auto *attCU = reinterpret_cast<Unit *>(
ar.mutable_changed_units_fb(ar.changed_units_fb_size() - 1)->data());
if (resetZoc) attCU->mutate_has_moved_in_zoc(false);
Unit attackerAfter = results.attackerAfter;
if (resetZoc) attackerAfter.mutate_has_moved_in_zoc(false);
AddChangedUnit(ar, attackerAfter);
AddChangedUnit(ar, results.defenderAfter);
@@ -104,7 +104,7 @@ const std::vector<CoordsSet> HexMapUtils::standardCoords =
auto HexMapUtils::ComputeAllStandardAdjacentCoords() -> std::vector<CoordsSet> {
std::vector<CoordsSet> arr;
arr.reserve(size_t(STANDARD_MAP_ROWS * STANDARD_MAP_COLUMNS));
arr.reserve(static_cast<size_t>(STANDARD_MAP_ROWS * STANDARD_MAP_COLUMNS));
for (int index = 0; index < STANDARD_MAP_ROWS * STANDARD_MAP_COLUMNS; ++index) {
arr.push_back(ComputeAdjacentCoords(
@@ -127,25 +127,25 @@ auto HexMapUtils::ComputeAdjacentCoords(
// Row above
if (origin.row() > 0) {
cs.AddIndex(originIndex - columnCount);
cs.AddMapIndexUnchecked(originIndex - columnCount);
if (origin.row() % 2 == 0) {
if (origin.column() > 0) { cs.AddIndex(originIndex - columnCount - 1); }
if (origin.column() > 0) { cs.AddMapIndexUnchecked(originIndex - columnCount - 1); }
} else if (origin.column() < columnCount - 1) {
cs.AddIndex(originIndex - columnCount + 1);
cs.AddMapIndexUnchecked(originIndex - columnCount + 1);
}
}
// This row
if (origin.column() > 0) { cs.AddIndex(originIndex - 1); }
if (origin.column() < columnCount - 1) { cs.AddIndex(originIndex + 1); }
if (origin.column() > 0) { cs.AddMapIndexUnchecked(originIndex - 1); }
if (origin.column() < columnCount - 1) { cs.AddMapIndexUnchecked(originIndex + 1); }
// Row below
if (origin.row() < rowCount - 1) {
cs.AddIndex(originIndex + columnCount);
cs.AddMapIndexUnchecked(originIndex + columnCount);
if (origin.row() % 2 == 0) {
if (origin.column() > 0) { cs.AddIndex(originIndex + columnCount - 1); }
if (origin.column() > 0) { cs.AddMapIndexUnchecked(originIndex + columnCount - 1); }
} else if (origin.column() < columnCount - 1) {
cs.AddIndex(originIndex + columnCount + 1);
cs.AddMapIndexUnchecked(originIndex + columnCount + 1);
}
}
@@ -36,7 +36,7 @@ struct alignas(16) AdjacentTile {
AdjacentTile(const Coords co, const HexMapDirection dir) : directionTo(dir), coords(co) {}
auto operator==(const AdjacentTile &rhs) const -> bool {
[[nodiscard]] auto operator==(const AdjacentTile &rhs) const -> bool {
return coords == rhs.coords && directionTo == rhs.directionTo;
}
};
@@ -73,21 +73,21 @@ public:
[[nodiscard]] auto AllCastleCoords(const HexMap *hexMap) -> CoordsSet;
auto HasForestAccess(
[[nodiscard]] auto HasForestAccess(
const Coords &coords,
const Units *units,
const HexMap *hexMap,
const std::vector<PlayerId> &allyPids,
PlayerId player) -> bool;
auto GetTerrain(const HexMap *map, const Coords &coords) -> const Terrain *;
auto GetMutableTerrain(HexMap *map, const Coords &coords) -> Terrain *;
[[nodiscard]] auto GetTerrain(const HexMap *map, const Coords &coords) -> const Terrain *;
[[nodiscard]] auto GetMutableTerrain(HexMap *map, const Coords &coords) -> Terrain *;
// Overload for const HexMap - uses const_cast internally. Safe when the underlying buffer is
// mutable.
auto GetMutableTerrain(const HexMap *map, const Coords &coords) -> Terrain *;
[[nodiscard]] auto GetMutableTerrain(const HexMap *map, const Coords &coords) -> Terrain *;
auto CoordsAreValid(const HexMap *map, const Coords &coords) -> bool;
auto CoordsAreValid(int rowCount, int columnCount, const Coords &coords) -> bool;
[[nodiscard]] auto CoordsAreValid(const HexMap *map, const Coords &coords) -> bool;
[[nodiscard]] auto CoordsAreValid(int rowCount, int columnCount, const Coords &coords) -> bool;
[[nodiscard]] auto GetTilesAcrossWater(const HexMap *map, const Coords &coords) -> CoordsSet;
@@ -100,7 +100,8 @@ auto CoordsAreValid(int rowCount, int columnCount, const Coords &coords) -> bool
[[nodiscard]] auto TwoAwayTilesUnblockedByMountains(const HexMap *map, const Coords &coords)
-> CoordsSet;
static inline auto Occupant(const Units *units, const Coords &coords) -> const Unit * {
[[nodiscard]] static inline auto Occupant(const Units *units, const Coords &coords)
-> const Unit * {
for (const auto &unit : *units) {
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
unit->location() == coords) {
@@ -111,7 +112,8 @@ static inline auto Occupant(const Units *units, const Coords &coords) -> const U
return nullptr;
}
static inline auto Occupant(const Units &units, const Coords &coords) -> const Unit * {
[[nodiscard]] static inline auto Occupant(const Units &units, const Coords &coords)
-> const Unit * {
for (const auto &unit : units) {
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
unit->location() == coords) {
@@ -122,7 +124,7 @@ static inline auto Occupant(const Units &units, const Coords &coords) -> const U
return nullptr;
}
static inline auto Occupants(
[[nodiscard]] static inline auto Occupants(
const std::vector<const Unit *> &units,
const int rowCount,
const int columnCount) -> std::vector<const Unit *> {
@@ -138,7 +140,7 @@ static inline auto Occupants(
return positions;
}
static inline auto Occupants(
[[nodiscard]] static inline auto Occupants(
const flatbuffers::Vector<const Unit *> &units,
const int rowCount,
const int columnCount) -> std::vector<const Unit *> {
@@ -154,8 +156,9 @@ static inline auto Occupants(
return positions;
}
static inline auto Occupant(const std::vector<const Unit *> &units, const Coords &coords)
-> const Unit * {
[[nodiscard]] static inline auto Occupant(
const std::vector<const Unit *> &units,
const Coords &coords) -> const Unit * {
for (const auto &unit : units) {
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
unit->location() == coords) {
@@ -167,7 +170,7 @@ static inline auto Occupant(const std::vector<const Unit *> &units, const Coords
}
template<class UnitContainer>
auto Occupant(const UnitContainer &units, const Coords &coords)
[[nodiscard]] auto Occupant(const UnitContainer &units, const Coords &coords)
-> std::optional<typename UnitContainer::value_type> {
for (const auto &unit : units) {
if (unit.location() == coords) { return unit; }
@@ -177,7 +180,8 @@ auto Occupant(const UnitContainer &units, const Coords &coords)
}
template<class U>
auto Occupant(const std::map<UnitId, U> &units, const Coords &coords) -> std::optional<U> {
[[nodiscard]] auto Occupant(const std::map<UnitId, U> &units, const Coords &coords)
-> std::optional<U> {
for (const auto &[unitId, unit] : units) {
if (unit.location() == coords) { return unit; }
}
@@ -186,7 +190,7 @@ auto Occupant(const std::map<UnitId, U> &units, const Coords &coords) -> std::op
}
template<class U>
auto Occupant(const ::google::protobuf::Map<UnitId, U> &units, const Coords &coords)
[[nodiscard]] auto Occupant(const ::google::protobuf::Map<UnitId, U> &units, const Coords &coords)
-> std::optional<U> {
for (const auto &[unitId, unit] : units) {
if (unit.location() == coords) { return unit; }
@@ -196,7 +200,7 @@ auto Occupant(const ::google::protobuf::Map<UnitId, U> &units, const Coords &coo
}
template<class U, class UnitContainer>
auto Occupant(const UnitContainer &units, const Coords &coords) -> std::optional<U> {
[[nodiscard]] auto Occupant(const UnitContainer &units, const Coords &coords) -> std::optional<U> {
for (const auto &unit : units) {
if (unit.location() == coords) { return unit; }
}
@@ -204,7 +208,7 @@ auto Occupant(const UnitContainer &units, const Coords &coords) -> std::optional
return std::nullopt;
}
static inline auto FriendlyOccupant(
[[nodiscard]] static inline auto FriendlyOccupant(
const PlayerId pid,
const Units *units,
const std::vector<PlayerId> &allyPids,
@@ -221,7 +225,7 @@ static inline auto FriendlyOccupant(
}
template<class U, class UnitContainer>
auto FriendlyOccupant(
[[nodiscard]] auto FriendlyOccupant(
PlayerId pid,
const UnitContainer &units,
const std::vector<PlayerId> &allyPids,
@@ -237,14 +241,14 @@ auto FriendlyOccupant(
return {};
}
auto KnownEnemyOccupant(
[[nodiscard]] auto KnownEnemyOccupant(
PlayerId playerId,
const Units *units,
const std::vector<PlayerId> &allyPids,
const Coords &coords) -> const Unit *;
template<class UnitContainer>
auto KnownEnemyOccupant(
[[nodiscard]] auto KnownEnemyOccupant(
PlayerId playerId,
const UnitContainer &units,
const std::vector<PlayerId> &allyPids,
@@ -277,31 +281,35 @@ auto KnownEnemyOccupant(
return nullptr;
}
auto KnownAdjacentEnemies(
[[nodiscard]] auto KnownAdjacentEnemies(
PlayerId playerId,
const Units *units,
const HexMap *map,
const std::vector<PlayerId> &allyPids,
const Coords &coords) -> std::vector<const Unit *>;
auto DirectionsTo(int columnCount, const Coords &origin, const Coords &destination)
[[nodiscard]] auto DirectionsTo(int columnCount, const Coords &origin, const Coords &destination)
-> HexMapDirectionsTo;
static inline auto DirectionsTo(const HexMap *map, const Coords &origin, const Coords &destination)
-> HexMapDirectionsTo {
[[nodiscard]] static inline auto DirectionsTo(
const HexMap *map,
const Coords &origin,
const Coords &destination) -> HexMapDirectionsTo {
return DirectionsTo(map->column_count(), origin, destination);
}
auto MaybeTileInDirection(const HexMap *map, const Coords &inCoords, HexMapDirection orientation)
-> std::optional<Coords>;
[[nodiscard]] auto MaybeTileInDirection(
const HexMap *map,
const Coords &inCoords,
HexMapDirection orientation) -> std::optional<Coords>;
auto MaybeOccupantInDirection(
[[nodiscard]] auto MaybeOccupantInDirection(
const HexMap *map,
const Units *units,
const Coords &coords,
HexMapDirection direction) -> const Unit *;
auto GetAllCoords(const HexMap *hexMap) -> CoordsSet;
[[nodiscard]] auto GetAllCoords(const HexMap *hexMap) -> CoordsSet;
void MutatingSetTileModifier(
HexMapT *hexMap,
@@ -318,9 +326,10 @@ void MutatingSetTileModifier(
const CoordsProto &coords,
const TileModifierProto &TileModifierProto);
auto GetCriticalTileLocations(const HexMap *hexMap) -> CoordsSet;
[[nodiscard]] auto GetCriticalTileLocations(const HexMap *hexMap) -> CoordsSet;
auto IsBelievedEmpty(PlayerId playerId, const Coords &coords, const Units *units) -> bool;
[[nodiscard]] auto IsBelievedEmpty(PlayerId playerId, const Coords &coords, const Units *units)
-> bool;
} // namespace shardok
@@ -15,10 +15,10 @@ namespace shardok {
using PlayerInfoFb = net::eagle0::shardok::storage::fb::PlayerInfo;
using GameState = net::eagle0::shardok::storage::fb::GameState;
auto PlayerIsDefender(const GameState *gameState, int playerId) -> bool;
auto NextPlayerId(const GameState *gameState, PlayerId pid, bool allowUncontrolled = true)
-> PlayerId;
auto AlliedPids(const GameState *gameState, int playerId) -> std::vector<PlayerId>;
[[nodiscard]] auto PlayerIsDefender(const GameState *gameState, int playerId) -> bool;
[[nodiscard]] auto
NextPlayerId(const GameState *gameState, PlayerId pid, bool allowUncontrolled = true) -> PlayerId;
[[nodiscard]] auto AlliedPids(const GameState *gameState, int playerId) -> std::vector<PlayerId>;
} // namespace shardok
@@ -8,6 +8,7 @@
#include <ranges>
#include "src/main/cpp/net/eagle0/shardok/library/unit/Unit.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/ActionResultFlatbufferHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/view_filters/GameStateFilter.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/view_filters/GameStateViewDiffer.hpp"
#include "src/main/protobuf/net/eagle0/shardok/common/action_type.pb.h"
@@ -87,7 +88,7 @@ auto TargetIsHiddenFromOpponents(const ActionResultType type) -> bool {
if (result.has_actor()) {
const auto actorId = result.actor().value();
for (const auto &unitData : result.changed_units_fb()) {
const auto &unit = *reinterpret_cast<const Unit *>(unitData.data());
const auto unit = UnitFromBytes(unitData);
if (unit.unit_id() == actorId) {
opponentKnowledgeOfActor = OpponentKnowledge(unit, askingPlayer);
hiddenActor = unit.hidden();
@@ -13,6 +13,7 @@ cc_library(
deps = [
":unit_filter",
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
"//src/main/cpp/net/eagle0/shardok/library/util:action_result_flatbuffer_helpers",
"//src/main/cpp/net/eagle0/shardok/library/view_filters:game_state_filter",
"//src/main/cpp/net/eagle0/shardok/library/view_filters:game_state_view_differ",
"//src/main/protobuf/net/eagle0/shardok/api:action_result_view_cc_proto",
@@ -70,7 +70,8 @@ auto MakeGameStateViewDiff(const GameStateView &before, const GameStateView &aft
for (int tileIndex = 0; tileIndex < afterCount; ++tileIndex) {
Coords coords =
Coords(int8_t(tileIndex / afterColumnCount), int8_t(tileIndex % afterColumnCount));
Coords(static_cast<int8_t>(tileIndex / afterColumnCount),
static_cast<int8_t>(tileIndex % afterColumnCount));
const auto afterModifier = after.hex_map().terrain(tileIndex).modifier();
if (tileIndex >= before.hex_map().terrain_size()) {
@@ -17,7 +17,7 @@ namespace shardok {
using net::eagle0::shardok::api::GameStateView;
using net::eagle0::shardok::api::GameStateViewDiff;
auto MakeGameStateViewDiff(const GameStateView& before, const GameStateView& after)
[[nodiscard]] auto MakeGameStateViewDiff(const GameStateView& before, const GameStateView& after)
-> std::optional<GameStateViewDiff>;
} // namespace shardok
@@ -57,6 +57,7 @@ cc_library(
deps = [
"//src/main/cpp/net/eagle0/shardok/library:shardok_c_types",
"//src/main/cpp/net/eagle0/shardok/library:shardok_exception",
"//src/main/cpp/net/eagle0/shardok/library/util:action_result_flatbuffer_helpers",
"//src/main/flatbuffer/net/eagle0/shardok/storage:unit_cc_fbs",
"//src/main/protobuf/net/eagle0/common:common_unit_cc_proto",
"//src/main/protobuf/net/eagle0/common:shardok_internal_interface_cc_grpc",
@@ -9,6 +9,7 @@
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokException.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/ActionResultFlatbufferHelpers.hpp"
#include "src/main/protobuf/net/eagle0/common/victory_condition.pb.h"
namespace shardok {
@@ -71,13 +72,13 @@ auto PopulateGameOverResponse(
// copy in all the resolvedUnits that correspond to this player
for (const auto &internalRu : resolvedUnits) {
const auto *unit = reinterpret_cast<const Unit *>(internalRu.unit_bytes().data());
if (!unit->has_attached_hero()) continue;
const auto unit = UnitFromBytes(internalRu.unit_bytes());
if (!unit.has_attached_hero()) continue;
if (!IncludeUnitProtoInReturn(internalRu.status())) continue;
if (unit->player_id() != shardokPid) continue;
if (unit.player_id() != shardokPid) continue;
auto *ru = resolutionInfo->add_units();
*ru->mutable_unit() = FromUnitFb(*unit);
*ru->mutable_unit() = FromUnitFb(unit);
ru->set_status(FromInternalStatus(internalRu.status()));
if (internalRu.status() ==
net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_SECURED_UNIT) {
@@ -22,12 +22,12 @@ auto PopulateGameOverResponse(
const std::vector<net::eagle0::shardok::storage::ResolvedUnit> &resolvedUnits,
net::eagle0::common::GameOverResponse *gameOverResponse) -> bool;
auto IncludeUnitProtoInReturn(
[[nodiscard]] auto IncludeUnitProtoInReturn(
const net::eagle0::shardok::storage::ResolvedUnit::UnitStatus &internalStatus) -> bool;
auto FromUnitFb(const Unit &unit) -> net::eagle0::common::CommonUnit;
[[nodiscard]] auto FromUnitFb(const Unit &unit) -> net::eagle0::common::CommonUnit;
auto FromInternalStatus(
[[nodiscard]] auto FromInternalStatus(
const net::eagle0::shardok::storage::ResolvedUnit::UnitStatus internalStatus)
-> net::eagle0::common::UnitStatus;
@@ -94,7 +94,7 @@ private:
* Read auth token from a file, stripping whitespace.
* Returns empty string if file doesn't exist or is empty.
*/
inline auto ReadAuthTokenFromFile(const std::string& filePath) -> std::string {
[[nodiscard]] inline auto ReadAuthTokenFromFile(const std::string& filePath) -> std::string {
if (filePath.empty()) { return ""; }
std::ifstream file(filePath);
@@ -26,10 +26,10 @@ public:
[[nodiscard]] auto what() const noexcept -> const char* override { return description.c_str(); }
};
auto LoadMap(const std::string& mapName) -> HexMapProto;
[[nodiscard]] auto LoadMap(const std::string& mapName) -> HexMapProto;
auto LoadMapFromBytes(const std::string& mapBytes) -> HexMapProto;
[[nodiscard]] auto LoadMapFromBytes(const std::string& mapBytes) -> HexMapProto;
auto GetMapNames() -> std::vector<std::string>;
[[nodiscard]] auto GetMapNames() -> std::vector<std::string>;
#endif // EAGLE0_SHARDOK_UTIL_MAP_LOADER_HPP
@@ -37,7 +37,7 @@ RectTransform:
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: -2, y: 0}
m_SizeDelta: {x: -14, y: 0}
m_Pivot: {x: 0, y: 1}
--- !u!222 &8488118418423736121
CanvasRenderer:
@@ -1863,7 +1863,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 217.5914, y: -8}
m_AnchoredPosition: {x: 330, y: -8}
m_SizeDelta: {x: 173.1829, y: 202}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &5247279438940124088
@@ -2369,8 +2369,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0}
m_AnchorMax: {x: 0.5, y: 1}
m_AnchoredPosition: {x: -79, y: 0}
m_SizeDelta: {x: 395, y: -20}
m_AnchoredPosition: {x: -150, y: 0}
m_SizeDelta: {x: 560, y: -20}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &531306430670215360
MonoBehaviour:
@@ -20,6 +20,35 @@ namespace eagle {
private int _availableGold;
private int _maxArmament;
private MaxAttainableEverywhereBattalions _maxAttainableEverywhereBattalions;
private int? _selectedBattalionIndex;
private GameObject _touchActionStrip;
private TMP_Text _touchActionStripLabel;
private Button _touchPlusButton;
private Button _touchMinusButton;
private Button _touchResetButton;
private Button _touchMaxButton;
private Image _touchPlusIcon;
private Image _touchMinusIcon;
private Image _touchResetIcon;
private Image _touchMaxIcon;
private Transform _touchActionButtonGrid;
private GameObject _allBattalionsHeader;
private GameObject _allBattalionsMaxButtonRow;
private bool _capturedTableLayout;
private bool _capturedAllBattalionsLayout;
private Vector2 _tableAnchoredPosition;
private Vector2 _tableSizeDelta;
private RectTransform _allBattalionsControlsRectTransform;
private Vector2 _allBattalionsControlsAnchoredPosition;
private Vector2 _allBattalionsControlsSizeDelta;
private VerticalLayoutGroup _allBattalionsControlsLayout;
private TextAnchor _allBattalionsControlsAlignment;
private RectTransform _maxAllButtonRectTransform;
private RectTransform _maxEvenButtonRectTransform;
private Transform _maxAllButtonParent;
private Transform _maxEvenButtonParent;
private int _maxAllButtonSiblingIndex;
private int _maxEvenButtonSiblingIndex;
public EventBasedTable armTroopsTable;
public TMP_Text totalCostText;
@@ -34,6 +63,9 @@ namespace eagle {
private ArmTroopsAvailableCommand ArmTroopsAvailableCommand =>
_availableCommand.ArmTroopsCommand;
private ProvinceId ActingProvinceId => ArmTroopsAvailableCommand.ActingProvinceId;
private bool UseTouchActionStrip =>
IsNarrowLayout && GameInput.HasTouchscreen() &&
(!GameInput.HasMousePointer() || GameInput.HasSimulatedTouchscreen());
private struct MaxAttainableEverywhereBattalions {
public int Armament;
@@ -107,6 +139,14 @@ namespace eagle {
}
protected override void SetUpUI() {
EnsureTouchActionStrip();
if (armTroopsTable.RowClickedHandler == null) {
armTroopsTable.RowClickedHandler = new EventBasedTable.RowClickedEvent();
}
armTroopsTable.WireRuntimeRowClickHandlers = true;
armTroopsTable.RowClickedHandler.RemoveListener(RowClicked);
armTroopsTable.RowClickedHandler.AddListener(RowClicked);
var province = _model.Provinces[ActingProvinceId];
_battalions =
ArmTroopsAvailableCommand.AvailableBattalions
@@ -126,16 +166,21 @@ namespace eagle {
_maxArmament = ArmTroopsAvailableCommand.MaxArmament;
_maxAttainableEverywhereBattalions = SpendRemainingGold(SpendEvenlyOnly());
if (_maxAttainableEverywhereBattalions.Armament == _maxArmament) {
maxEvenButton.gameObject.SetActive(false);
} else {
maxEvenLabel.text = _maxAttainableEverywhereBattalions.Armament.ToString();
maxEvenButton.gameObject.SetActive(true);
}
maxEvenLabel.text = _maxAttainableEverywhereBattalions.Armament.ToString();
maxEvenButton.gameObject.SetActive(true);
maxEvenButton.interactable =
_maxAttainableEverywhereBattalions.Armament != _maxArmament;
maxLabel.text = _maxArmament.ToString();
availableText.text = _availableGold.ToString();
ApplyInputModeLayout();
UpdateTableAndCost();
}
public override void OnLayoutModeChanged() {
if (_battalions == null) return;
ApplyInputModeLayout();
UpdateTableAndCost();
}
@@ -159,6 +204,11 @@ namespace eagle {
row.MinusButtonClickedCallback = () => MinusClicked(index);
row.ResetButtonClickedCallback = () => ResetClicked(index);
row.MaxButtonClickedCallback = () => MaxClicked(index);
row.RowClickedCallback = () => RowClicked(index);
row.SetActionButtonsVisible(!UseTouchActionStrip);
row.SetContentRaycastTargets(!UseTouchActionStrip);
row.Selectable = true;
row.Selected = UseTouchActionStrip && _selectedBattalionIndex == index;
SetValuesAndButtons(index);
}
@@ -172,12 +222,13 @@ namespace eagle {
row.NewArmament = newArmament;
row.Name = battalion.Name;
row.AllowMax = battalion.Armament < _maxArmament;
row.AllowPlus = battalion.Armament < _maxArmament;
row.AllowMax = newArmament < _maxArmament;
row.AllowPlus = newArmament < _maxArmament;
row.AllowMinus = battalion.Armament < newArmament;
row.AllowReset = battalion.Armament != newArmament;
UpdateCost();
UpdateTouchActionStrip();
}
private void UpdateCost() {
@@ -198,11 +249,28 @@ namespace eagle {
}
private void UpdateTableAndCost() {
if (UseTouchActionStrip && _battalions.Count > 0) {
if (_selectedBattalionIndex == null || _selectedBattalionIndex < 0 ||
_selectedBattalionIndex >= _battalions.Count) {
_selectedBattalionIndex = 0;
}
} else {
_selectedBattalionIndex = null;
}
armTroopsTable.RowCount = _battalions.Count;
for (int i = 0; i < _battalions.Count; i++) { PopulateRow(i); }
armTroopsTable.RefreshRowEventHandlers();
UpdateCost();
UpdateTouchActionStrip();
}
public void RowClicked(int index) {
if (!UseTouchActionStrip) return;
SelectBattalion(index);
}
public void PlusClicked(int index) {
@@ -276,5 +344,351 @@ namespace eagle {
}
private int Cost() { return Cost(_newArmaments); }
private void SelectBattalion(int index) {
_selectedBattalionIndex = index;
for (int i = 0; i < _battalions.Count; i++) {
armTroopsTable.ComponentAt<ArmTroopsRowController>(i).Selected = i == index;
}
UpdateTouchActionStrip();
}
private bool TrySelectedBattalionIndex(out int index) {
index = _selectedBattalionIndex ?? -1;
return UseTouchActionStrip && index >= 0 && index < _battalions.Count;
}
private void UpdateTouchActionStrip() {
if (_touchActionStrip == null) return;
if (!TrySelectedBattalionIndex(out int index)) {
_touchActionStrip.SetActive(false);
return;
}
var battalion = _battalions[index];
var originalArmament = (int)(battalion.Armament ?? 0);
var newArmament = _newArmaments[index];
_touchActionStrip.SetActive(true);
_touchActionStripLabel.text = battalion.Name;
UpdateTouchActionButtonIcons();
_touchPlusButton.interactable = newArmament < _maxArmament;
_touchMinusButton.interactable = originalArmament < newArmament;
_touchResetButton.interactable = originalArmament != newArmament;
_touchMaxButton.interactable = newArmament < _maxArmament;
}
private void EnsureTouchActionStrip() {
if (_touchActionStrip != null) return;
_touchActionStrip = new GameObject(
"Touch Action Strip",
typeof(RectTransform),
typeof(Image),
typeof(VerticalLayoutGroup));
_touchActionStrip.transform.SetParent(transform, false);
var rectTransform = _touchActionStrip.GetComponent<RectTransform>();
rectTransform.anchorMin = new Vector2(0.5f, 0.5f);
rectTransform.anchorMax = new Vector2(0.5f, 0.5f);
rectTransform.pivot = new Vector2(0.5f, 0.5f);
rectTransform.anchoredPosition = new Vector2(55f, -16f);
rectTransform.sizeDelta = new Vector2(150f, 186f);
var background = _touchActionStrip.GetComponent<Image>();
background.color = Color.clear;
var layout = _touchActionStrip.GetComponent<VerticalLayoutGroup>();
layout.padding = new RectOffset(0, 0, 0, 0);
layout.spacing = 6f;
layout.childAlignment = TextAnchor.UpperCenter;
layout.childControlWidth = true;
layout.childControlHeight = true;
layout.childForceExpandWidth = false;
layout.childForceExpandHeight = false;
_touchActionStripLabel = CreateActionStripLabel();
_touchActionButtonGrid = CreateActionButtonGrid();
_touchMinusButton = CreateActionStripButton("Minus Button", out _touchMinusIcon, () => {
if (TrySelectedBattalionIndex(out int index)) { MinusClicked(index); }
});
_touchPlusButton = CreateActionStripButton("Plus Button", out _touchPlusIcon, () => {
if (TrySelectedBattalionIndex(out int index)) { PlusClicked(index); }
});
_touchResetButton = CreateActionStripButton("Reset Button", out _touchResetIcon, () => {
if (TrySelectedBattalionIndex(out int index)) { ResetClicked(index); }
});
_touchMaxButton = CreateActionStripButton("Max Button", out _touchMaxIcon, () => {
if (TrySelectedBattalionIndex(out int index)) { MaxClicked(index); }
});
_touchActionStrip.SetActive(false);
EnsureAllBattalionsHeader();
}
private void ApplyInputModeLayout() {
var tableRectTransform = armTroopsTable.GetComponent<RectTransform>();
if (!_capturedTableLayout) {
_tableAnchoredPosition = tableRectTransform.anchoredPosition;
_tableSizeDelta = tableRectTransform.sizeDelta;
_allBattalionsControlsRectTransform =
maxEvenButton.transform.parent.GetComponent<RectTransform>();
_allBattalionsControlsAnchoredPosition =
_allBattalionsControlsRectTransform.anchoredPosition;
_allBattalionsControlsSizeDelta = _allBattalionsControlsRectTransform.sizeDelta;
_capturedTableLayout = true;
}
CaptureAllBattalionsLayout();
EnsureAllBattalionsHeader();
EnsureAllBattalionsMaxButtonRow();
if (UseTouchActionStrip) {
var leftEdge = _tableAnchoredPosition.x - _tableSizeDelta.x / 2f;
var touchTableWidth = 270f;
tableRectTransform.anchoredPosition =
new Vector2(leftEdge + touchTableWidth / 2f, _tableAnchoredPosition.y);
tableRectTransform.sizeDelta = new Vector2(touchTableWidth, _tableSizeDelta.y);
_allBattalionsControlsRectTransform.anchoredPosition =
_allBattalionsControlsAnchoredPosition;
_allBattalionsControlsRectTransform.sizeDelta = new Vector2(190f, 202f);
_allBattalionsControlsLayout.childAlignment = TextAnchor.UpperCenter;
var touchActionStripRectTransform = _touchActionStrip.GetComponent<RectTransform>();
touchActionStripRectTransform.anchoredPosition = new Vector2(
touchActionStripRectTransform.anchoredPosition.x,
_allBattalionsControlsAnchoredPosition.y);
touchActionStripRectTransform.sizeDelta =
new Vector2(touchActionStripRectTransform.sizeDelta.x, 202f);
_allBattalionsHeader.transform.SetParent(
_allBattalionsControlsRectTransform,
false);
_allBattalionsHeader.transform.SetSiblingIndex(0);
_allBattalionsMaxButtonRow.transform.SetParent(
_allBattalionsControlsRectTransform,
false);
_allBattalionsMaxButtonRow.transform.SetSiblingIndex(1);
_maxAllButtonRectTransform.SetParent(_allBattalionsMaxButtonRow.transform, false);
_maxAllButtonRectTransform.SetSiblingIndex(0);
_maxEvenButtonRectTransform.SetParent(_allBattalionsMaxButtonRow.transform, false);
_maxEvenButtonRectTransform.SetSiblingIndex(1);
_allBattalionsHeader.SetActive(true);
_allBattalionsMaxButtonRow.SetActive(true);
} else {
tableRectTransform.anchoredPosition = _tableAnchoredPosition;
tableRectTransform.sizeDelta = _tableSizeDelta;
_allBattalionsControlsRectTransform.anchoredPosition =
_allBattalionsControlsAnchoredPosition;
_allBattalionsControlsRectTransform.sizeDelta = _allBattalionsControlsSizeDelta;
_allBattalionsControlsLayout.childAlignment = _allBattalionsControlsAlignment;
RestoreAllBattalionsMaxButtons();
if (_allBattalionsHeader != null) { _allBattalionsHeader.SetActive(false); }
if (_allBattalionsMaxButtonRow != null) {
_allBattalionsMaxButtonRow.SetActive(false);
}
}
}
private void CaptureAllBattalionsLayout() {
if (_capturedAllBattalionsLayout) return;
_allBattalionsControlsLayout =
_allBattalionsControlsRectTransform.GetComponent<VerticalLayoutGroup>();
_allBattalionsControlsAlignment = _allBattalionsControlsLayout.childAlignment;
_maxAllButtonRectTransform =
_allBattalionsControlsRectTransform.GetChild(0).GetComponent<RectTransform>();
_maxEvenButtonRectTransform = maxEvenButton.GetComponent<RectTransform>();
_maxAllButtonParent = _maxAllButtonRectTransform.parent;
_maxEvenButtonParent = _maxEvenButtonRectTransform.parent;
_maxAllButtonSiblingIndex = _maxAllButtonRectTransform.GetSiblingIndex();
_maxEvenButtonSiblingIndex = _maxEvenButtonRectTransform.GetSiblingIndex();
_capturedAllBattalionsLayout = true;
}
private void RestoreAllBattalionsMaxButtons() {
_maxAllButtonRectTransform.SetParent(_maxAllButtonParent, false);
_maxAllButtonRectTransform.SetSiblingIndex(_maxAllButtonSiblingIndex);
_maxEvenButtonRectTransform.SetParent(_maxEvenButtonParent, false);
_maxEvenButtonRectTransform.SetSiblingIndex(_maxEvenButtonSiblingIndex);
}
private TMP_Text CreateActionStripLabel() {
var label = new GameObject(
"Selected Battalion Label",
typeof(RectTransform),
typeof(LayoutElement),
typeof(TextMeshProUGUI));
label.transform.SetParent(_touchActionStrip.transform, false);
var layoutElement = label.GetComponent<LayoutElement>();
layoutElement.minWidth = 150f;
layoutElement.preferredWidth = 150f;
layoutElement.preferredHeight = 44f;
var text = label.GetComponent<TextMeshProUGUI>();
text.alignment = TextAlignmentOptions.Center;
text.color = Color.black;
text.font = maxLabel.font;
text.fontSize = 18f;
text.enableWordWrapping = true;
text.overflowMode = TextOverflowModes.Overflow;
return text;
}
private Transform CreateActionButtonGrid() {
var gridObject = new GameObject(
"Action Button Grid",
typeof(RectTransform),
typeof(LayoutElement),
typeof(GridLayoutGroup));
gridObject.transform.SetParent(_touchActionStrip.transform, false);
var layoutElement = gridObject.GetComponent<LayoutElement>();
layoutElement.minWidth = 108f;
layoutElement.preferredWidth = 108f;
layoutElement.minHeight = 108f;
layoutElement.preferredHeight = 108f;
var grid = gridObject.GetComponent<GridLayoutGroup>();
grid.cellSize = new Vector2(48f, 48f);
grid.spacing = new Vector2(6f, 6f);
grid.constraint = GridLayoutGroup.Constraint.FixedColumnCount;
grid.constraintCount = 2;
grid.childAlignment = TextAnchor.MiddleCenter;
return gridObject.transform;
}
private Button CreateActionStripButton(
string name,
out Image icon,
UnityEngine.Events.UnityAction action) {
var buttonObject = new GameObject(
name,
typeof(RectTransform),
typeof(LayoutElement),
typeof(Image),
typeof(Button));
buttonObject.transform.SetParent(_touchActionButtonGrid, false);
var layoutElement = buttonObject.GetComponent<LayoutElement>();
layoutElement.minWidth = 48f;
layoutElement.preferredWidth = 48f;
layoutElement.minHeight = 48f;
layoutElement.preferredHeight = 48f;
var image = buttonObject.GetComponent<Image>();
image.color = new Color(0.94f, 0.94f, 0.96f, 1f);
CopyButtonBackground(maxEvenButton, image);
var button = buttonObject.GetComponent<Button>();
button.targetGraphic = image;
button.onClick.AddListener(action);
var iconObject = new GameObject("Icon", typeof(RectTransform), typeof(Image));
iconObject.transform.SetParent(buttonObject.transform, false);
var iconRectTransform = iconObject.GetComponent<RectTransform>();
iconRectTransform.anchorMin = new Vector2(0.5f, 0.5f);
iconRectTransform.anchorMax = new Vector2(0.5f, 0.5f);
iconRectTransform.anchoredPosition = Vector2.zero;
iconRectTransform.sizeDelta = new Vector2(34f, 34f);
icon = iconObject.GetComponent<Image>();
icon.color = Color.black;
icon.preserveAspect = true;
return button;
}
private void UpdateTouchActionButtonIcons() {
if (_battalions == null || _battalions.Count == 0) return;
var row = armTroopsTable.ComponentAt<ArmTroopsRowController>(0);
_touchMinusIcon.sprite = ButtonIconSprite(row.minusButton);
_touchPlusIcon.sprite = ButtonIconSprite(row.plusButton);
_touchResetIcon.sprite = ButtonIconSprite(row.resetButton);
_touchMaxIcon.sprite = ButtonIconSprite(row.maxButton);
}
private Sprite ButtonIconSprite(Button button) {
foreach (var image in button.GetComponentsInChildren<Image>(includeInactive: true)) {
if (image.gameObject != button.gameObject && image.sprite != null) {
return image.sprite;
}
}
return null;
}
private void CopyButtonBackground(Button sourceButton, Image targetImage) {
var sourceImage = sourceButton.targetGraphic as Image;
if (sourceImage == null) return;
targetImage.sprite = sourceImage.sprite;
targetImage.type = sourceImage.type;
targetImage.pixelsPerUnitMultiplier = sourceImage.pixelsPerUnitMultiplier;
}
private void EnsureAllBattalionsMaxButtonRow() {
if (_allBattalionsMaxButtonRow != null) return;
_allBattalionsMaxButtonRow = new GameObject(
"All Battalions Max Button Row",
typeof(RectTransform),
typeof(LayoutElement),
typeof(HorizontalLayoutGroup));
_allBattalionsMaxButtonRow.transform.SetParent(
_allBattalionsControlsRectTransform,
false);
var layoutElement = _allBattalionsMaxButtonRow.GetComponent<LayoutElement>();
layoutElement.minWidth = 190f;
layoutElement.preferredWidth = 190f;
layoutElement.minHeight = 60f;
layoutElement.preferredHeight = 60f;
var layout = _allBattalionsMaxButtonRow.GetComponent<HorizontalLayoutGroup>();
layout.padding = new RectOffset(0, 0, 0, 0);
layout.spacing = 8f;
layout.childAlignment = TextAnchor.MiddleCenter;
layout.childControlWidth = true;
layout.childControlHeight = true;
layout.childForceExpandWidth = false;
layout.childForceExpandHeight = false;
_allBattalionsMaxButtonRow.SetActive(false);
}
private void EnsureAllBattalionsHeader() {
if (_allBattalionsHeader != null) return;
_allBattalionsHeader = new GameObject(
"All Battalions Header",
typeof(RectTransform),
typeof(LayoutElement),
typeof(TextMeshProUGUI));
_allBattalionsHeader.transform.SetParent(transform, false);
var layoutElement = _allBattalionsHeader.GetComponent<LayoutElement>();
layoutElement.minWidth = 190f;
layoutElement.preferredWidth = 190f;
layoutElement.minHeight = 44f;
layoutElement.preferredHeight = 44f;
var text = _allBattalionsHeader.GetComponent<TextMeshProUGUI>();
text.text = "All Battalions";
text.alignment = TextAlignmentOptions.Center;
text.color = Color.black;
text.font = maxLabel.font;
text.fontSize = 18f;
text.enableWordWrapping = false;
_allBattalionsHeader.SetActive(false);
}
}
}
}
@@ -67,12 +67,7 @@ namespace eagle {
}
}
void Awake() {
TouchTargetUtils.EnsureMinimumTouchTarget(dismissAllButton);
TouchTargetUtils.EnsureMinimumTouchTarget(
dismissOneButtonText.GetComponentInParent<Button>());
HideBeastVisual();
}
void Awake() { HideBeastVisual(); }
void OnDestroy() {
_isDestroying = true;
@@ -99,7 +99,7 @@ MonoBehaviour:
m_EditorClassIdentifier:
m_Padding:
m_Left: 5
m_Right: 5
m_Right: 8
m_Top: 5
m_Bottom: 5
m_ChildAlignment: 3
@@ -124,9 +124,9 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinWidth: 540
m_MinHeight: -1
m_PreferredWidth: -1
m_PreferredWidth: 540
m_PreferredHeight: -1
m_FlexibleWidth: -1
m_FlexibleHeight: 1
@@ -633,9 +633,9 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_IgnoreLayout: 0
m_MinWidth: 207
m_MinWidth: 320
m_MinHeight: -1
m_PreferredWidth: 207
m_PreferredWidth: 320
m_PreferredHeight: -1
m_FlexibleWidth: -1
m_FlexibleHeight: -1
@@ -922,9 +922,9 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_IgnoreLayout: 0
m_MinWidth: 207
m_MinWidth: 320
m_MinHeight: -1
m_PreferredWidth: 207
m_PreferredWidth: 320
m_PreferredHeight: -1
m_FlexibleWidth: -1
m_FlexibleHeight: -1
@@ -1149,7 +1149,7 @@ RectTransform:
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 25, y: 25}
m_SizeDelta: {x: 26, y: 26}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &3462703612756757987
CanvasRenderer:
@@ -1225,7 +1225,7 @@ RectTransform:
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 25, y: 25}
m_SizeDelta: {x: 26, y: 26}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &7037872268356765622
CanvasRenderer:
@@ -1510,10 +1510,10 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: 25
m_PreferredHeight: 25
m_MinWidth: 36
m_MinHeight: 36
m_PreferredWidth: 36
m_PreferredHeight: 36
m_FlexibleWidth: -1
m_FlexibleHeight: -1
m_LayoutPriority: 1
@@ -1553,7 +1553,7 @@ RectTransform:
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 25, y: 25}
m_SizeDelta: {x: 26, y: 26}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &4669985286182351739
CanvasRenderer:
@@ -1629,7 +1629,7 @@ RectTransform:
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 25, y: 25}
m_SizeDelta: {x: 26, y: 26}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &2353172733458224956
CanvasRenderer:
@@ -1862,10 +1862,10 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: 25
m_PreferredHeight: 25
m_MinWidth: 36
m_MinHeight: 36
m_PreferredWidth: 36
m_PreferredHeight: 36
m_FlexibleWidth: -1
m_FlexibleHeight: -1
m_LayoutPriority: 1
@@ -2017,10 +2017,10 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: 25
m_PreferredHeight: 25
m_MinWidth: 36
m_MinHeight: 36
m_PreferredWidth: 36
m_PreferredHeight: 36
m_FlexibleWidth: -1
m_FlexibleHeight: -1
m_LayoutPriority: 1
@@ -2266,10 +2266,10 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: 25
m_PreferredHeight: 25
m_MinWidth: 36
m_MinHeight: 36
m_PreferredWidth: 36
m_PreferredHeight: 36
m_FlexibleWidth: -1
m_FlexibleHeight: -1
m_LayoutPriority: 1
@@ -1,10 +1,14 @@
using System;
using System;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using static UnityEngine.EventSystems.PointerEventData;
namespace eagle {
public class ArmTroopsRowController : TableRowController {
public class ArmTroopsRowController : TableRowController, IPointerClickHandler {
private static readonly Color SelectedBackgroundColor = new Color(0.68f, 0.84f, 0.96f, 1f);
public RawImage battalionImage;
public TMP_Text oldArmamentLabel;
public TMP_Text newArmamentLabel;
@@ -21,16 +25,10 @@ namespace eagle {
public Action MinusButtonClickedCallback;
public Action ResetButtonClickedCallback;
public Action MaxButtonClickedCallback;
public Action RowClickedCallback;
public Texture BattalionImage { set => battalionImage.texture = value; }
private void Awake() {
TouchTargetUtils.EnsureMinimumTouchTarget(plusButton);
TouchTargetUtils.EnsureMinimumTouchTarget(minusButton);
TouchTargetUtils.EnsureMinimumTouchTarget(resetButton);
TouchTargetUtils.EnsureMinimumTouchTarget(maxButton);
}
public double CostPerPoint { set => costPerPointLabel.text = value.ToString("0.##"); }
public double OldArmament { set => oldArmamentLabel.text = Math.Floor(value).ToString(); }
@@ -49,6 +47,29 @@ namespace eagle {
public bool AllowMax { set => maxButton.interactable = value; }
public void SetActionButtonsVisible(bool visible) {
plusButton.gameObject.SetActive(visible);
minusButton.gameObject.SetActive(visible);
resetButton.gameObject.SetActive(visible);
maxButton.gameObject.SetActive(visible);
}
public void SetContentRaycastTargets(bool enabled) {
foreach (var graphic in GetComponentsInChildren<Graphic>()) {
if (graphic.gameObject == gameObject) continue;
graphic.raycastTarget = enabled;
}
}
public override void ColorRow() {
base.ColorRow();
if (Selected) { gameObject.GetComponent<Image>().color = SelectedBackgroundColor; }
}
public void OnPointerClick(PointerEventData eventData) {
if (eventData.button == InputButton.Left) { RowClickedCallback?.Invoke(); }
}
public void PlusButtonClicked() { PlusButtonClickedCallback(); }
public void MinusButtonClicked() { MinusButtonClickedCallback(); }
@@ -61420,8 +61420,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 67.5, y: -17.5}
m_SizeDelta: {x: 125, y: 35}
m_AnchoredPosition: {x: 67.5, y: -24}
m_SizeDelta: {x: 125, y: 48}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &448680368
MonoBehaviour:
@@ -61437,9 +61437,9 @@ MonoBehaviour:
m_EditorClassIdentifier:
m_IgnoreLayout: 0
m_MinWidth: 125
m_MinHeight: 35
m_MinHeight: 48
m_PreferredWidth: 125
m_PreferredHeight: 35
m_PreferredHeight: 48
m_FlexibleWidth: 0
m_FlexibleHeight: 0
m_LayoutPriority: 1
@@ -81276,8 +81276,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0}
m_AnchorMax: {x: 0.5, y: 1}
m_AnchoredPosition: {x: -79, y: 0}
m_SizeDelta: {x: 395, y: -20}
m_AnchoredPosition: {x: -96.5, y: 0}
m_SizeDelta: {x: 430, y: -20}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!1 &574323926
GameObject:
@@ -85441,7 +85441,7 @@ RectTransform:
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: -702}
m_SizeDelta: {x: 0, y: 324}
m_SizeDelta: {x: 0, y: 288}
m_Pivot: {x: 1, y: 1}
--- !u!114 &604968800
MonoBehaviour:
@@ -127452,8 +127452,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 650, y: -265.5}
m_SizeDelta: {x: 1290, y: 35}
m_AnchoredPosition: {x: 650, y: -264}
m_SizeDelta: {x: 1290, y: 48}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &897090470
MonoBehaviour:
@@ -180946,8 +180946,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 580, y: -17.5}
m_SizeDelta: {x: 890, y: 35}
m_AnchoredPosition: {x: 580, y: -24}
m_SizeDelta: {x: 890, y: 48}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1305016253
MonoBehaviour:
@@ -224378,8 +224378,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 1092.5, y: -17.5}
m_SizeDelta: {x: 125, y: 35}
m_AnchoredPosition: {x: 1092.5, y: -24}
m_SizeDelta: {x: 125, y: 48}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1601044727
MonoBehaviour:
@@ -224395,9 +224395,9 @@ MonoBehaviour:
m_EditorClassIdentifier:
m_IgnoreLayout: 0
m_MinWidth: 125
m_MinHeight: 35
m_MinHeight: 48
m_PreferredWidth: 125
m_PreferredHeight: 35
m_PreferredHeight: 48
m_FlexibleWidth: 0
m_FlexibleHeight: 0
m_LayoutPriority: 1
@@ -240119,7 +240119,7 @@ RectTransform:
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: -2, y: 0}
m_SizeDelta: {x: -14, y: 0}
m_Pivot: {x: 0, y: 1}
--- !u!114 &1703297659
MonoBehaviour:
@@ -249518,8 +249518,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 1222.5, y: -17.5}
m_SizeDelta: {x: 125, y: 35}
m_AnchoredPosition: {x: 1222.5, y: -24}
m_SizeDelta: {x: 125, y: 48}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1769007338
MonoBehaviour:
@@ -249535,9 +249535,9 @@ MonoBehaviour:
m_EditorClassIdentifier:
m_IgnoreLayout: 0
m_MinWidth: 125
m_MinHeight: 35
m_MinHeight: 48
m_PreferredWidth: 125
m_PreferredHeight: 35
m_PreferredHeight: 48
m_FlexibleWidth: 0
m_FlexibleHeight: 0
m_LayoutPriority: 1
@@ -1747,7 +1747,16 @@ namespace Shardok {
SetDisplayedCommandGroup(0);
}
_selectedGridIndex = selectedIndex;
RedrawCommandOverlays(selectedIndex, selectedIndex);
if (Model.GameStatus != null &&
Model.GameStatus.State == GameStatus.Types.State.GameRunning &&
CommandsForSelectedCoords.Any()) {
SelectPreferredCommandGroupForSelectedUnit();
if (_displayedCommandGroup == 0) {
RedrawCommandOverlays(selectedIndex, selectedIndex);
}
} else {
RedrawCommandOverlays(selectedIndex, selectedIndex);
}
// Notify tutorial system of unit selection
TutorialManager.Instance?.TriggerRegistry?.OnUnitSelected(selectedIndex);
@@ -27,6 +27,7 @@ namespace common {
public RowClickedEvent RowRightClickedHandler;
public HoverRowChangedEvent HoverRowChangedHandler;
public HoverRowChangedEvent LongHoverRowChangedHandler;
public bool WireRuntimeRowClickHandlers;
private Color _evenColor;
private Color _oddColor;
@@ -71,6 +72,10 @@ namespace common {
public T AddRowWithComponent<T>() { return AddRow().GetComponent<T>(); }
public void RefreshRowEventHandlers() {
foreach (var row in _rows) { SetUpClickDetector(row); }
}
private void ColorRows() {
if (ShadeAlternateRows) {
_rows.Each((row, idx) => {
@@ -94,12 +99,13 @@ namespace common {
clickDetector.onRightUp.RemoveAllListeners();
}
if (RowClickedHandler != null && RowClickedHandler.GetPersistentEventCount() > 0) {
if (RowClickedHandler != null && clickDetector != null &&
(WireRuntimeRowClickHandlers || RowClickedHandler.GetPersistentEventCount() > 0)) {
clickDetector.onClick.AddListener(() => RowClicked(row));
clickDetector.onLeftDown.AddListener(() => RowLeftDown(row));
clickDetector.onLeftUp.AddListener(() => RowLeftUp(row));
}
if (RowRightClickedHandler != null &&
if (RowRightClickedHandler != null && clickDetector != null &&
RowRightClickedHandler.GetPersistentEventCount() > 0) {
clickDetector.onRightClick.AddListener(() => RowRightClicked(row));
clickDetector.onRightDown.AddListener(() => RowRightDown(row));
@@ -247,4 +253,4 @@ namespace common {
ColorRows();
}
}
}
}
@@ -3,6 +3,7 @@ using UnityEngine;
#if ENABLE_INPUT_SYSTEM
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.EnhancedTouch;
#endif
public static class GameInput {
@@ -39,6 +40,31 @@ public static class GameInput {
#endif
}
public static bool HasTouchscreen() {
#if ENABLE_INPUT_SYSTEM
return Touchscreen.current != null;
#else
return false;
#endif
}
public static bool HasMousePointer() {
#if ENABLE_INPUT_SYSTEM
return Mouse.current != null;
#else
return false;
#endif
}
public static bool HasSimulatedTouchscreen() {
#if ENABLE_INPUT_SYSTEM
return TouchSimulation.instance != null &&
TouchSimulation.instance.simulatedTouchscreen != null;
#else
return false;
#endif
}
public static bool PrimaryPointerPressedThisFrame() {
#if ENABLE_INPUT_SYSTEM
return Mouse.current != null && Mouse.current.leftButton.wasPressedThisFrame;
@@ -50,6 +50,7 @@ scala_library(
"//src/main/protobuf/net/eagle0/eagle/internal:client_text_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
"//src/main/scala/net/eagle0/common:proto_parser",
"//src/main/scala/net/eagle0/common:simple_timed_logger",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
@@ -100,6 +101,7 @@ scala_library(
":text_generation_result",
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
"//src/main/scala/net/eagle0/common:jfr_events",
"//src/main/scala/net/eagle0/common:simple_timed_logger",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
@@ -125,6 +127,7 @@ scala_library(
":text_generation_result",
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
"//src/main/scala/net/eagle0/common:jfr_events",
"//src/main/scala/net/eagle0/common:simple_timed_logger",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
@@ -7,7 +7,7 @@ import java.util.regex.Pattern
import scala.io.Source
import scala.util.Try
import net.eagle0.common.ProtoParser
import net.eagle0.common.{ProtoParser, SimpleTimedLogger}
import net.eagle0.eagle.{ClientTextId, FactionId}
import net.eagle0.eagle.internal.client_text.{IncompleteText, IncompleteTextStore, UnrequestedText}
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
@@ -43,10 +43,10 @@ case class ClientTextStoreImpl(
)
if completeTexts.contains(id) then {
println(s"Text with id $id already exists")
SimpleTimedLogger.printLogger.logLine(s"Text with id $id already exists")
this
} else if incompleteTexts.contains(id) then {
println(s"Incomplete text with id $id already exists")
SimpleTimedLogger.printLogger.logLine(s"Incomplete text with id $id already exists")
this
} else {
copy(
@@ -213,7 +213,7 @@ case class ClientTextStoreImpl(
)
}
.getOrElse {
println(
SimpleTimedLogger.printLogger.logLine(
s"Warning: Attempted to move non-incomplete text $id back to unrequested"
)
this
@@ -399,7 +399,7 @@ object ClientTextStoreImpl {
}
.recover {
case _: FileNotFoundException =>
println("No complete text store found")
SimpleTimedLogger.printLogger.logLine("No complete text store found")
Vector.empty
}
@@ -451,7 +451,7 @@ object ClientTextStoreImpl {
}
.recover {
case _: FileNotFoundException =>
println("No incomplete text store found")
SimpleTimedLogger.printLogger.logLine("No incomplete text store found")
(Vector.empty, Vector.empty)
}
@@ -474,7 +474,7 @@ object ClientTextStoreImpl {
Some(id -> accessibleToString.split(",").map(_.toInt).toVector)
case x =>
println(s"Unexpected entry text: ${x.mkString(", ")}")
SimpleTimedLogger.printLogger.logLine(s"Unexpected entry text: ${x.mkString(", ")}")
None
}
}
@@ -482,7 +482,7 @@ object ClientTextStoreImpl {
}
.recover {
case _: FileNotFoundException =>
println("No accessibleTo store found")
SimpleTimedLogger.printLogger.logLine("No accessibleTo store found")
Map.empty
case x => throw x
@@ -4,7 +4,7 @@ import java.sql.{Connection, DriverManager, ResultSet}
import scala.util.Try
import net.eagle0.common.JfrEvents
import net.eagle0.common.{JfrEvents, SimpleTimedLogger}
import net.eagle0.eagle.{ClientTextId, FactionId, GameId}
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
import net.eagle0.eagle.library.EagleInternalException
@@ -200,7 +200,7 @@ class PostgresClientTextStore private[client_text] (
)
if textExists(id) then {
println(s"Text with id $id already exists")
SimpleTimedLogger.printLogger.logLine(s"Text with id $id already exists")
this
} else {
val sql =
@@ -407,10 +407,14 @@ class PostgresClientTextStore private[client_text] (
override def withMovedBackToUnrequested(id: ClientTextId): ClientTextStore = {
val status = statusFor(id)
if status.isEmpty then {
println(s"Warning: Attempted to move non-existent text $id back to unrequested")
SimpleTimedLogger.printLogger.logLine(
s"Warning: Attempted to move non-existent text $id back to unrequested"
)
this
} else if !status.contains("incomplete") then {
println(s"Warning: Attempted to move non-incomplete text $id back to unrequested")
SimpleTimedLogger.printLogger.logLine(
s"Warning: Attempted to move non-incomplete text $id back to unrequested"
)
this
} else {
val sql =
@@ -449,7 +453,7 @@ class PostgresClientTextStore private[client_text] (
if ids.isEmpty then this
else {
println(
SimpleTimedLogger.printLogger.logLine(
s"Removing ${ids.size} generated text row(s) after rewound history count $maxHistoryCount: ${ids.mkString(", ")}"
)
@@ -5,7 +5,7 @@ import java.sql.{Connection, DriverManager}
import scala.util.Try
import net.eagle0.common.JfrEvents
import net.eagle0.common.{JfrEvents, SimpleTimedLogger}
import net.eagle0.eagle.{ClientTextId, FactionId}
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
import net.eagle0.eagle.library.EagleInternalException
@@ -238,7 +238,7 @@ class SqliteClientTextStore private[client_text] (
existingStatus match {
case Some(status) =>
println(s"Text with id $id already exists (status=$status)")
SimpleTimedLogger.printLogger.logLine(s"Text with id $id already exists (status=$status)")
this
case None =>
// Insert new text as unrequested
@@ -493,10 +493,14 @@ class SqliteClientTextStore private[client_text] (
status match {
case None =>
println(s"Warning: Attempted to move non-existent text $id back to unrequested")
SimpleTimedLogger.printLogger.logLine(
s"Warning: Attempted to move non-existent text $id back to unrequested"
)
this
case Some(otherStatus) if otherStatus != "incomplete" =>
println(s"Warning: Attempted to move non-incomplete text $id back to unrequested")
SimpleTimedLogger.printLogger.logLine(
s"Warning: Attempted to move non-incomplete text $id back to unrequested"
)
this
case Some(_) =>
// Update to unrequested, reset text
@@ -529,7 +533,7 @@ class SqliteClientTextStore private[client_text] (
if ids.isEmpty then this
else {
println(
SimpleTimedLogger.printLogger.logLine(
s"Removing ${ids.size} generated text row(s) after rewound history count $maxHistoryCount: ${ids.mkString(", ")}"
)
@@ -903,7 +907,9 @@ object SqliteClientTextStore {
}
java.nio.file.Files.copy(inputStream, dbFile.toPath)
inputStream.close()
println(s"Downloaded SQLite database from cloud storage: ${dbFile.getAbsolutePath}")
SimpleTimedLogger.printLogger.logLine(
s"Downloaded SQLite database from cloud storage: ${dbFile.getAbsolutePath}"
)
}
}
@@ -2,6 +2,7 @@ package net.eagle0.eagle.library
import scala.collection.mutable.ListBuffer
import net.eagle0.common.SimpleTimedLogger
import net.eagle0.eagle.library.actions.applier.ActionResultWithResultingState
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
import net.eagle0.eagle.library.util.view_filters.GameStateViewFilter
@@ -120,8 +121,10 @@ object ActionResultFilter {
} &&
!allowOpponentActionPhases.contains(startingState.currentPhase)
then {
println("Learned about somebody else's action!")
println(actionResult)
SimpleTimedLogger.printLogger.logLine(
s"Faction $factionId learned about opponent action ${actionResult.actionResultType} " +
s"from ${actionResult.actingFactionId} during ${startingState.currentPhase}"
)
}
Option.when(includeForPlayer(factionId, result) || gsDiff.nonEmpty) {
val filteredNotifications = actionResult.newNotifications
@@ -200,8 +203,10 @@ object ActionResultFilter {
) &&
!allowOpponentActionPhases.contains(startingState.currentPhase)
then {
println("Learned about somebody else's action!")
println(actionResult)
SimpleTimedLogger.printLogger.logLine(
s"Faction $factionId learned about opponent action ${actionResult.actionResultType} " +
s"from ${actionResult.actingFactionId} during ${startingState.currentPhase}"
)
}
Option.when(includeForPlayer(Some(factionId), result) || gsDiff.nonEmpty) {
@@ -55,6 +55,7 @@ scala_library(
],
deps = [
":action_result_filter",
":eagle_client_exception",
":engine",
":game_history",
":phase_advancement_result",
@@ -102,6 +103,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/view/action_result:action_result_view",
],
deps = [
"//src/main/scala/net/eagle0/common:simple_timed_logger",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
"//src/main/scala/net/eagle0/eagle/library/util:game_state_view_differ",
@@ -338,8 +338,9 @@ case class EngineImpl(
val scalaAvailableCommand = scalaAvailableCommandOpt match {
case Some(scalaAvailableCommand) => scalaAvailableCommand
case None =>
commandRequire(false, s"No matching available command for selected command $selectedCommand")
throw new IllegalStateException("unreachable")
throw new EagleCommandException(
s"requirement failed: No matching available command for selected command $selectedCommand"
)
}
val commandType = selectedCommand.commandType
@@ -360,11 +361,6 @@ case class EngineImpl(
// Validate that the first result has an acting faction set (required for player commands)
val firstResult = sequencer.actionResults.newValue.headOption
if !firstResult.forall(_.actingFactionId.isDefined) then {
print(
"Result with type " + firstResult.map(_.actionResultType).getOrElse("unknown") + " did not have a player set"
)
}
internalRequire(
firstResult.forall(_.actingFactionId.isDefined),
s"Result with type ${firstResult.map(_.actionResultType).getOrElse("unknown")} did not have a player set"
@@ -128,6 +128,7 @@ case class ActionResultC(
newBattalionTypes = newBattalionTypes,
destroyedBattalionIds = destroyedBattalionIds,
removedHeroIds = removedHeroes,
removedFactionIds = removedFactionIds,
clientTextVisibilityExtensions = clientTextVisibilityExtensions,
newGeneratedTextRequests = newGeneratedTextRequests,
newNotifications = newNotifications,
@@ -14,6 +14,7 @@ scala_library(
"//src/test/scala/net/eagle0/eagle/library:__pkg__",
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/util:__subpackages__",
"//src/test/scala/net/eagle0/eagle/model/action_result/concrete:__pkg__",
"//src/test/scala/net/eagle0/eagle/service:__pkg__",
"//src/test/scala/net/eagle0/eagle/service/controller:__pkg__",
"//src/test/scala/net/eagle0/eagle/service/new_game_creation:__pkg__",
@@ -200,22 +200,10 @@ object SelectedCommandConverter {
)
case cmd: proto.ResolveRansomOfferSelectedCommand =>
val prisonerHeroId = cmd.ransomOffer.flatMap { offer =>
offer.offerDetails match {
case net.eagle0.eagle.common.diplomacy_offer.RansomOfferDetails(
prisonerToBeRansomed,
_,
_,
_,
_
) =>
prisonerToBeRansomed.map(_.prisonerHeroId)
case _ => None
}
}
val prisonerHeroId = prisonerHeroIdFromRansomOffer(cmd.ransomOffer)
ResolveRansomOfferSelected(
offeringFactionId = cmd.offeringFactionId,
prisonerHeroId = prisonerHeroId.getOrElse(-1),
prisonerHeroId = prisonerHeroId,
resolution = statusFromProto(cmd.resolution)
)
@@ -398,6 +386,21 @@ object SelectedCommandConverter {
goldOffered = details.goldOffered
)
private def prisonerHeroIdFromRansomOffer(
offer: Option[net.eagle0.eagle.common.diplomacy_offer.DiplomacyOffer]
): Int = {
val ransomOffer = offer.getOrElse(
throw new ProtoConversionException("ResolveRansomOfferSelectedCommand missing ransomOffer")
)
ransomOffer.offerDetails match {
case net.eagle0.eagle.common.diplomacy_offer.RansomOfferDetails(Some(prisonerToBeRansomed), _, _, _, _) =>
prisonerToBeRansomed.prisonerHeroId
case _ =>
throw new ProtoConversionException("ResolveRansomOfferSelectedCommand ransomOffer missing prisonerToBeRansomed")
}
}
private def prisonerToBeRansomedFromProto(p: PrisonerToBeRansomedProto): PrisonerToBeRansomed =
PrisonerToBeRansomed(
prisonerHeroId = p.prisonerHeroId,
@@ -35,6 +35,7 @@ scala_library(
"@maven//:org_postgresql_postgresql",
],
deps = [
":game_controller_registry",
":games_manager",
":service",
":sqlite_history",
@@ -119,6 +120,7 @@ scala_library(
deps = [
":authorization_utils",
":custom_battle_manager",
":game_controller_registry",
":games_manager",
":post_results",
":sync_response_observer",
@@ -205,6 +207,7 @@ scala_library(
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/api:eagle_scala_grpc",
"//src/main/scala/net/eagle0/common:simple_timed_logger",
"@maven//:io_sentry_sentry",
],
)
@@ -221,6 +224,41 @@ scala_library(
],
)
scala_library(
name = "game_controller_registry",
srcs = ["GameControllerRegistry.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/service:__pkg__",
"//src/test/scala/net/eagle0/eagle/service:__pkg__",
],
exports = [
"//src/main/scala/net/eagle0/eagle/service/controller:game_controller",
],
deps = [
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle:user_id",
"//src/main/scala/net/eagle0/eagle/service/controller:game_controller",
],
)
scala_library(
name = "games_manager_state",
srcs = ["GamesManagerState.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/service:__pkg__",
"//src/test/scala/net/eagle0/eagle/service:__pkg__",
],
deps = [
":game_controller_registry",
"//src/main/protobuf/net/eagle0/common:tutorial_battle_config_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:hero_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:running_games_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle:user_id",
"//src/main/scala/net/eagle0/eagle/service/new_game_creation:game_parameters_utils",
],
)
scala_library(
name = "games_manager",
srcs = ["GamesManager.scala"],
@@ -243,7 +281,9 @@ scala_library(
],
deps = [
":free_hero_backstory_visibility_recovery",
":game_controller_registry",
":game_persister_creation",
":games_manager_state",
":hero_name_cache",
":llm_resolver",
":llm_update_queuing_proxy",
@@ -592,6 +632,7 @@ scala_library(
"//src/main/protobuf/net/eagle0/shardok/api:command_descriptor_scala_proto",
"//src/main/protobuf/net/eagle0/shardok/storage:action_result_scala_proto",
"//src/main/scala/net/eagle0/common:jfr_events",
"//src/main/scala/net/eagle0/common:simple_timed_logger",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier_impl",
@@ -1157,7 +1198,9 @@ scala_library(
],
deps = [
":custom_battle_manager",
":game_controller_registry",
":games_manager",
":games_manager_state",
":hero_name_cache",
"//src/main/protobuf/net/eagle0/common:shardok_internal_interface_scala_grpc",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
@@ -2,6 +2,7 @@ package net.eagle0.eagle.service
import io.grpc.*
import io.sentry.Sentry
import net.eagle0.common.SimpleTimedLogger
class ExceptionInterceptor extends ServerInterceptor {
override def interceptCall[ReqT, RespT](
@@ -10,7 +11,7 @@ class ExceptionInterceptor extends ServerInterceptor {
next: ServerCallHandler[ReqT, RespT]
): ServerCall.Listener[ReqT] = {
def handleThrowable(t: Throwable): Unit = {
println(s"Caught throwable $t")
SimpleTimedLogger.printLogger.logLine(s"Caught throwable $t")
Sentry.captureException(t)
call.close(Status.fromThrowable(t), headers)
}
@@ -20,7 +21,7 @@ class ExceptionInterceptor extends ServerInterceptor {
next.startCall(call, headers)
catch {
case e: Throwable =>
println(s"Caught exception $e")
SimpleTimedLogger.printLogger.logLine(s"Caught exception $e")
Sentry.captureException(e)
throw e
}
@@ -11,7 +11,7 @@ import net.eagle0.eagle.model.state.game_state.GameState
* Generated-pool heroes arrive with a pre-authored backstory and historically had no client-text visibility set at
* placement (the all-faction request is only emitted for freshly spawned heroes with an empty backstory). Such heroes
* could end up with their backstory visible only to a faction they happened to pass through, never to the faction
* actually ruling the province they sit in the [BACKSTORY-VISIBILITY] diagnostic in GameController.
* actually ruling the province they sit in.
*
* Going forward this is fixed at the appearance event (UnaffiliatedHeroAppearedAction) and on movement
* (PerformUnaffiliatedHeroesAction.heroMovedResult). This recovery repairs games that were already saved in the broken
@@ -0,0 +1,78 @@
package net.eagle0.eagle.service
import net.eagle0.eagle.{GameId, UserId}
import net.eagle0.eagle.service.controller.GameController
case class ControllerInfo(
controller: GameController,
isAiGame: Boolean,
maxPlayers: Int,
lastPlayedByUserId: Map[UserId, Long] = Map.empty,
createdTimestampMillis: Long = 0L
)
final case class GameControllerRegistry(
controllers: Map[GameId, ControllerInfo]
) {
def snapshot: Map[GameId, ControllerInfo] = controllers
def contains(gameId: GameId): Boolean =
controllers.contains(gameId)
def get(gameId: GameId): Option[ControllerInfo] =
controllers.get(gameId)
def apply(gameId: GameId): ControllerInfo =
controllers(gameId)
def size: Int =
controllers.size
def keySet: Set[GameId] =
controllers.keySet
def values: Iterable[ControllerInfo] =
controllers.values
def map[B](f: ((GameId, ControllerInfo)) => B): Iterable[B] =
controllers.map(f)
def put(gameId: GameId, controllerInfo: ControllerInfo): GameControllerRegistry =
copy(controllers = controllers + (gameId -> controllerInfo))
def put(
controller: GameController,
isAiGame: Boolean,
maxPlayers: Int,
createdTimestampMillis: Long = 0L
): GameControllerRegistry =
put(
controller.gameId,
ControllerInfo(
controller = controller,
isAiGame = isAiGame,
maxPlayers = maxPlayers,
createdTimestampMillis = createdTimestampMillis
)
)
def remove(gameId: GameId): (GameControllerRegistry, Option[ControllerInfo]) =
(copy(controllers = controllers - gameId), controllers.get(gameId))
def clear: (GameControllerRegistry, Iterable[ControllerInfo]) =
(copy(controllers = Map.empty), controllers.values)
def withUpdatedController(gameId: GameId, controller: GameController): GameControllerRegistry =
put(gameId, controllers(gameId).copy(controller = controller))
def withUpdatedController(gameId: GameId, update: GameController => GameController): GameControllerRegistry =
withUpdatedController(gameId, update(controllers(gameId).controller))
def withUpdatedLastPlayed(gameId: GameId, userId: UserId, millis: Long): GameControllerRegistry =
put(
gameId,
controllers(gameId).copy(
lastPlayedByUserId = controllers(gameId).lastPlayedByUserId.updated(userId, millis)
)
)
}
@@ -2,6 +2,7 @@ package net.eagle0.eagle.service
import java.io.File
import net.eagle0.common.SimpleTimedLogger
import net.eagle0.eagle.library.actions.applier.ActionResultWithResultingState
import net.eagle0.eagle.library.EagleInternalException
import net.eagle0.eagle.model.action_result.ActionResultT
@@ -57,13 +58,13 @@ private[service] object GameHistoryFactory {
try PostgresHistory.loaded(gameId)
catch {
case e: Exception =>
println(
SimpleTimedLogger.printLogger.logLine(
s"GameHistoryFactory: failed to load Postgres history for game ${gameId.toHexString}; trying SQLite fallback: ${e.getMessage}"
)
None
}
if postgres.isEmpty then
println(
SimpleTimedLogger.printLogger.logLine(
s"GameHistoryFactory: no Postgres history found for game ${gameId.toHexString}; trying SQLite fallback"
)
val fallback = postgres.orElse(sqlite)
@@ -8,7 +8,6 @@ import scala.util.{Failure, Random, Success}
import io.sentry.Sentry
import net.eagle0.common.{ProtoParser, SeededRandom, SimpleTimedLogger, ZipUtils}
import net.eagle0.common.shardok_internal_interface.ShardokInternalInterfaceGrpc.ShardokInternalInterfaceStub
import net.eagle0.common.tutorial_battle_config.TutorialBattleConfig
import net.eagle0.eagle.{ClientTextId, FactionId, GameId, ProvinceId, ShardokGameId, UserId}
import net.eagle0.eagle.admin.game_admin.{ConvertAiToHumanResponse, ConvertHumanToAiResponse, ReassignFactionResponse}
import net.eagle0.eagle.api.eagle.*
@@ -75,46 +74,6 @@ case class GamePlayerInfo(
gameType: GameTypeProto
)
case class WaitingGamePlayerInfo(
userId: UserId,
leaderNameTextId: String
)
case class GameAwaitingPlayers(
expandedGameParameters: ExpandedGameParameters,
gameId: GameId,
existingHumanPlayers: Vector[WaitingGamePlayerInfo],
totalPlayerCount: Int,
humanPlayerCount: Int,
greatPeople: Vector[Hero]
) {
private def claimedNameTextIds: Vector[String] =
existingHumanPlayers.map(_.leaderNameTextId)
def availableNameTextIds: Vector[String] =
greatPeople
.map(_.nameTextId)
.filterNot(claimedNameTextIds.contains)
def withNewWaitingPlayerInfo(
waitingGamePlayerInfo: WaitingGamePlayerInfo
): GameAwaitingPlayers =
GameAwaitingPlayers(
expandedGameParameters = expandedGameParameters,
gameId = gameId,
existingHumanPlayers = existingHumanPlayers :+ waitingGamePlayerInfo,
totalPlayerCount = totalPlayerCount,
humanPlayerCount = humanPlayerCount,
greatPeople = greatPeople
)
}
case class ControllerInfo(
var controller: GameController,
isAiGame: Boolean,
maxPlayers: Int,
var lastPlayedByUserId: Map[UserId, Long] = Map.empty,
createdTimestampMillis: Long = 0L
)
object GamesManager {
private[service] val DroppedGamesKey: String = "dropped_games.e0es"
private[service] val DroppedGameRetentionMillis: Long = TimeUnit.DAYS.toMillis(14)
@@ -239,8 +198,8 @@ object GamesManager {
new GamesManager(
shardokInternalInterface = shardokInternalInterface,
gameControllerInfos = Map.empty, // Start empty - games load on demand
gamesAwaitingPlayers = Vector.empty,
initialGameControllerInfos = Map.empty, // Start empty - games load on demand
initialGamesAwaitingPlayers = Vector.empty,
randomGenerator = randomGenerator,
persister = persister,
gameCreation = gameCreation,
@@ -453,8 +412,8 @@ object GamesManager {
class GamesManager(
shardokInternalInterface: ShardokInternalInterfaceStub,
var gameControllerInfos: Map[GameId, ControllerInfo],
var gamesAwaitingPlayers: Vector[GameAwaitingPlayers],
initialGameControllerInfos: Map[GameId, ControllerInfo],
initialGamesAwaitingPlayers: Vector[GameAwaitingPlayers],
randomGenerator: Random,
persister: Persister,
gameCreation: NewGameCreation,
@@ -467,20 +426,56 @@ class GamesManager(
) extends BattleUpdateReceiver
with LlmUpdateReceiver {
// Tracks games that have been explicitly removed (archived/deleted) to prevent
// them from being written back to games.e0es during save(). With lazy loading,
// save() merges in-memory games with unloaded games from storage. Without this
// tracking, a removed game would appear as "unloaded" and be preserved.
private var removedGameIds: Set[GameId] = Set.empty
private var lastDroppedGameCleanupCheckMillis: Long = 0L
private var loadingGameIds: Set[GameId] = Set.empty
private var tutorialBattleConfigs: Map[GameId, TutorialBattleConfig] = Map.empty
// Tracks mutable service state in one immutable value. GamesManager remains the
// synchronization/effect boundary while state transitions move toward copy-returning helpers.
private var state = GamesManagerState(
controllerRegistry = GameControllerRegistry(initialGameControllerInfos),
gamesAwaitingPlayers = initialGamesAwaitingPlayers
)
// Per-game locks to prevent concurrent loading of the same game and to allow
// game loading to happen without holding the main GamesManager.this lock.
private val gameLoadingLocks = new ConcurrentHashMap[GameId, AnyRef]()
def gameControllerInfos: Map[GameId, ControllerInfo] = this.synchronized {
state.controllerRegistry.snapshot
}
def gamesAwaitingPlayers: Vector[GameAwaitingPlayers] = this.synchronized {
state.gamesAwaitingPlayers
}
private def loadingGameIds: Set[GameId] =
state.loadingGameIds
private def updateState(update: GamesManagerState => GamesManagerState): Unit = this.synchronized {
state = update(state)
}
private def putControllerInfo(gameId: GameId, controllerInfo: ControllerInfo): Unit =
updateState(_.withControllerInfo(gameId, controllerInfo))
private def updateController(gameId: GameId, controller: GameController): Unit =
updateState(state =>
state.withControllerRegistry(
state.controllerRegistry.withUpdatedController(gameId, controller)
)
)
private def updateController(gameId: GameId, update: GameController => GameController): Unit =
updateState(state =>
state.withControllerRegistry(
state.controllerRegistry.withUpdatedController(gameId, update)
)
)
private def updateLastPlayed(gameId: GameId, userId: UserId): Unit =
updateState(state =>
state.withControllerRegistry(
state.controllerRegistry.withUpdatedLastPlayed(gameId, userId, System.currentTimeMillis())
)
)
val shardokClient =
new ShardokInterfaceGrpcClient(shardokInternalInterface, this)
@@ -501,17 +496,16 @@ class GamesManager(
try info.controller.clientTextStore.close()
finally GamesManager.extractFullHistory(info.controller.engine).close()
private def unloadGame(gameId: GameId): Option[ControllerInfo] = {
val existing = gameControllerInfos.get(gameId)
gameControllerInfos -= gameId
private def unloadGame(gameId: GameId): Option[ControllerInfo] =
val (updatedRegistry, existing) = state.controllerRegistry.remove(gameId)
updateState(_.withControllerRegistry(updatedRegistry))
existing.foreach(closeControllerInfo)
existing
}
private def unloadAllGames(): Unit = {
gameControllerInfos.values.foreach(closeControllerInfo)
gameControllerInfos = Map.empty
}
private def unloadAllGames(): Unit =
val (updatedRegistry, existing) = state.controllerRegistry.clear
updateState(_.withControllerRegistry(updatedRegistry))
existing.foreach(closeControllerInfo)
def recreateLlmCallers(): Unit =
llmResolver.recreateLlmCallers()
@@ -523,18 +517,6 @@ class GamesManager(
shardokClient.connect()
}
// Track the flush marker mod time to detect when it's been updated
@volatile private var lastFlushMarkerCheck: Long = 0L
@volatile private var flushMarkerModTime: Long = 0L
/**
* Cached snapshot of games.e0es. Read paths share this so we don't re-parse the file on every lobby update,
* heartbeat-driven refresh, save() merge, or game-load. Invalidated on flush-marker bump (covers blue-green
* deployments where the active server has flushed newer state) and refreshed at the end of save() (which is the only
* path through this process that can change the file's contents).
*/
@volatile private var cachedRunningGames: Option[Vector[RunningGame]] = None
/**
* Checks if the flush marker has been updated since we last loaded games. If so, invalidates the cache so games
* reload from fresh disk state.
@@ -545,25 +527,23 @@ class GamesManager(
private def invalidateCacheIfFlushMarkerUpdated(): Unit = {
val now = System.currentTimeMillis()
// Only check every 100ms to avoid excessive file system access
if now - lastFlushMarkerCheck >= 100 then {
lastFlushMarkerCheck = now
if now - state.lastFlushMarkerCheck >= 100 then {
updateState(_.withLastFlushMarkerCheck(now))
val flushMarker = java.nio.file.Paths.get(SaveDirectory.saveDirectory, ".flush_complete")
if java.nio.file.Files.exists(flushMarker) then
try {
val currentModTime = java.nio.file.Files.getLastModifiedTime(flushMarker).toMillis
if currentModTime > flushMarkerModTime then {
if currentModTime > state.flushMarkerModTime then {
val cachedCount = gameControllerInfos.size
if cachedCount > 0 then {
SimpleTimedLogger.printLogger.logLine(
s"[DEPLOY] Flush marker updated, invalidating $cachedCount cached games"
)
unloadAllGames()
gamesAwaitingPlayers = Vector.empty
removedGameIds = Set.empty
cachedRunningGames = None
updateState(_.withoutLoadedGames)
}
flushMarkerModTime = currentModTime
updateState(_.withFlushMarkerModTime(currentModTime))
}
} catch {
case _: Exception => // Ignore errors checking marker
@@ -580,9 +560,9 @@ class GamesManager(
*/
private def runningGamesFromStorage(): Vector[RunningGame] = {
invalidateCacheIfFlushMarkerUpdated()
cachedRunningGames.getOrElse {
state.cachedRunningGames.getOrElse {
val games = readRunningGamesFromStorage()
cachedRunningGames = Some(games)
updateState(_.withCachedRunningGames(games))
games
}
}
@@ -655,8 +635,8 @@ class GamesManager(
}
private def cleanupExpiredDroppedGames(nowMillis: Long): Unit =
if nowMillis - lastDroppedGameCleanupCheckMillis >= GamesManager.DroppedGameCleanupIntervalMillis then {
lastDroppedGameCleanupCheckMillis = nowMillis
if nowMillis - state.lastDroppedGameCleanupCheckMillis >= GamesManager.DroppedGameCleanupIntervalMillis then {
updateState(_.withLastDroppedGameCleanupCheck(nowMillis))
val droppedGames = readDroppedGamesFromStorage()
val (expired, retained) =
droppedGames.partition(droppedGame =>
@@ -830,7 +810,7 @@ class GamesManager(
}
this.synchronized {
loadingGameIds += gameId
updateState(_.withLoadingGame(gameId))
}
// Wait for flush marker before loading from disk to ensure we get fresh data
@@ -881,12 +861,15 @@ class GamesManager(
// Add to in-memory map BEFORE text handling so isGameValid returns true
// during LLM request processing. Brief lock acquisition only.
this.synchronized {
gameControllerInfos += gameId -> ControllerInfo(
controller = loadedControllerWithResults.gameController,
isAiGame = false,
maxPlayers = 2,
lastPlayedByUserId = lastPlayedByUserId,
createdTimestampMillis = runningGame.createdTimestampMillis
putControllerInfo(
gameId,
ControllerInfo(
controller = loadedControllerWithResults.gameController,
isAiGame = false,
maxPlayers = 2,
lastPlayedByUserId = lastPlayedByUserId,
createdTimestampMillis = runningGame.createdTimestampMillis
)
)
}
@@ -912,12 +895,15 @@ class GamesManager(
// Update with initialized controller. Brief lock acquisition only.
this.synchronized {
gameControllerInfos += gameId -> ControllerInfo(
controller = withLiveHumanClientsFromCurrentController(initializedController),
isAiGame = false,
maxPlayers = 2,
lastPlayedByUserId = lastPlayedByUserId,
createdTimestampMillis = runningGame.createdTimestampMillis
putControllerInfo(
gameId,
ControllerInfo(
controller = withLiveHumanClientsFromCurrentController(initializedController),
isAiGame = false,
maxPlayers = 2,
lastPlayedByUserId = lastPlayedByUserId,
createdTimestampMillis = runningGame.createdTimestampMillis
)
)
}
@@ -938,7 +924,7 @@ class GamesManager(
}
finally {
this.synchronized {
loadingGameIds -= gameId
updateState(_.withoutLoadingGame(gameId))
}
gameLoadingLocks.remove(gameId, lock): Unit
}
@@ -1328,7 +1314,7 @@ class GamesManager(
// Start with existing games that haven't been loaded (preserve them),
// but exclude games that were explicitly removed (archived/deleted)
val unloadedGames =
existingGames.filterNot(rg => loadedGameIds.contains(rg.gameId) || removedGameIds.contains(rg.gameId))
existingGames.filterNot(rg => loadedGameIds.contains(rg.gameId) || state.removedGameIds.contains(rg.gameId))
// Add in-memory games (these have the freshest state)
val loadedGames = gameControllerInfos.map {
@@ -1374,10 +1360,10 @@ class GamesManager(
// Refresh the cache from the just-written state. The on-disk file and the cache now agree, so subsequent
// read paths can hit the cache instead of re-parsing.
cachedRunningGames = Some(rg.runningGames.toVector)
updateState(_.withCachedRunningGames(rg.runningGames.toVector))
// Clear removed games after successful save
removedGameIds = Set.empty
updateState(_.withoutRemovedGames)
()
}
@@ -1387,11 +1373,15 @@ class GamesManager(
maxPlayers: Int,
createdTimestampMillis: Long = 0L
): GameController = this.synchronized {
gameControllerInfos += controller.gameId -> ControllerInfo(
controller = controller,
isAiGame = isAiGame,
maxPlayers = maxPlayers,
createdTimestampMillis = createdTimestampMillis
updateState(state =>
state.withControllerRegistry(
state.controllerRegistry.put(
controller = controller,
isAiGame = isAiGame,
maxPlayers = maxPlayers,
createdTimestampMillis = createdTimestampMillis
)
)
)
controller
}
@@ -1441,7 +1431,7 @@ class GamesManager(
battle = ShardokBattleConverter.toProto(battle),
eagleGameState = gameState,
humanFactions = humanFactions,
tutorialBattleConfig = tutorialBattleConfigs.get(battle.eagleGameId)
tutorialBattleConfig = state.tutorialBattleConfigs.get(battle.eagleGameId)
)
}
@@ -1491,8 +1481,7 @@ class GamesManager(
)
): Unit
// Record last played time for this user
val ci = gameControllerInfos(gameId)
ci.lastPlayedByUserId = ci.lastPlayedByUserId.updated(userId, System.currentTimeMillis())
updateLastPlayed(gameId, userId)
save()
}
@@ -1519,8 +1508,7 @@ class GamesManager(
)
): Unit
// Record last played time for this user
val ci = gameControllerInfos(gameId)
ci.lastPlayedByUserId = ci.lastPlayedByUserId.updated(userId, System.currentTimeMillis())
updateLastPlayed(gameId, userId)
save()
}
@@ -1580,8 +1568,7 @@ class GamesManager(
)
}
// Record last played time for this user
val ci = gameControllerInfos(eagleGameId)
ci.lastPlayedByUserId = ci.lastPlayedByUserId.updated(userId, System.currentTimeMillis())
updateLastPlayed(eagleGameId, userId)
save()
}
@@ -1653,7 +1640,7 @@ class GamesManager(
withLiveHumanClientsFromCurrentController(afterUnrequested)
}
gameControllerInfos(cwrt.gameController.gameId).controller = newController
updateController(cwrt.gameController.gameId, newController)
GameControllerWithPostResults(
gameController = newController,
@@ -1686,8 +1673,9 @@ class GamesManager(
)
val filterStart = System.currentTimeMillis()
gameControllerInfos(gameId).controller = gameControllerInfos(gameId).controller
.streamUpdates(
updateController(
gameId,
_.streamUpdates(
userId = userId,
clientId = clientId,
knownResultCount = knownResultCount,
@@ -1695,6 +1683,7 @@ class GamesManager(
responseObserver = responseObserver,
streamingTextStatuses = streamingTextStatuses
)
)
val filterEnd = System.currentTimeMillis()
if filterEnd - filterStart > 500 then
SimpleTimedLogger.printLogger.logLine(
@@ -1713,8 +1702,7 @@ class GamesManager(
gameId: GameId
): Unit =
this.synchronized {
gameControllerInfos(gameId).controller = gameControllerInfos(gameId).controller
.stopStreaming(userId = userId)
updateController(gameId, _.stopStreaming(userId = userId))
}
def joinGame(
@@ -1733,16 +1721,14 @@ class GamesManager(
)
if newGap.existingHumanPlayers.size == newGap.humanPlayerCount then {
gamesAwaitingPlayers = gamesAwaitingPlayers.diff(Vector(gap))
updateState(_.withGamesAwaitingPlayers(gamesAwaitingPlayers.diff(Vector(gap))))
synchronizedCreateGame(
expandedGameParameters = gap.expandedGameParameters,
gameId = newGap.gameId,
humanPlayers = newGap.existingHumanPlayers,
aiPlayerCount = newGap.totalPlayerCount - newGap.existingHumanPlayers.size
): Unit
} else
gamesAwaitingPlayers = gamesAwaitingPlayers
.diff(Vector(gap)) :+ newGap
} else updateState(_.withGamesAwaitingPlayers(gamesAwaitingPlayers.diff(Vector(gap)) :+ newGap))
JoinGameResponse(
result = JoinGameResult.SUCCESS_JOIN_GAME_RESULT,
@@ -1778,19 +1764,23 @@ class GamesManager(
desiredLeaderTextId: String,
maxHumanPlayers: Int,
totalPlayers: Int
): CreateGameResponse =
): CreateGameResponse = this.synchronized {
if maxHumanPlayers > totalPlayers || totalPlayers > MaxSupportedPlayers.intValue
then CreateGameResponse(INVALID_OPTIONS_RESULT, 0)
else {
val newGameId = randomGenerator.nextLong()
gamesAwaitingPlayers = gamesAwaitingPlayers :+ GameAwaitingPlayers(
expandedGameParameters = expandedGameParameters,
gameId = newGameId,
existingHumanPlayers = Vector.empty,
totalPlayerCount = totalPlayers,
humanPlayerCount = maxHumanPlayers,
greatPeople = expandedGameParameters.greatPeopleHeroProtos
updateState(
_.withGamesAwaitingPlayers(
gamesAwaitingPlayers :+ GameAwaitingPlayers(
expandedGameParameters = expandedGameParameters,
gameId = newGameId,
existingHumanPlayers = Vector.empty,
totalPlayerCount = totalPlayers,
humanPlayerCount = maxHumanPlayers,
greatPeople = expandedGameParameters.greatPeopleHeroProtos
)
)
)
val result = joinGame(
@@ -1805,12 +1795,13 @@ class GamesManager(
save()
result
}
}
def createTutorialGameForUser(
userId: UserId,
gameId: GameId,
startPhase: TutorialPhase = TutorialPhase.OpeningBattle
): CreateGameResponse = {
): CreateGameResponse = this.synchronized {
val (engineAndResults, tutorialConfig) =
TutorialGameCreation.createTutorialGame(
gameId,
@@ -1818,8 +1809,7 @@ class GamesManager(
GamesManager.battalionTypes
)
if startPhase == TutorialPhase.OpeningBattle then
tutorialBattleConfigs = tutorialBattleConfigs + (gameId -> tutorialConfig)
if startPhase == TutorialPhase.OpeningBattle then updateState(_.withTutorialBattleConfig(gameId, tutorialConfig))
val initialTextStore = GamesManager.createClientTextStoreWithData(
gameId = gameId,
@@ -1909,7 +1899,7 @@ class GamesManager(
else {
shardokClient.cancelBattlesForGame(gameId)
val (newController, resyncedCount) = controller.rewindTo(targetActionCount)
gameControllerInfos(gameId).controller = newController
updateController(gameId, newController)
val afterContinuation = synchronizedHandlePostResults(
GameController.performAutomaticContinuation(
GameControllerWithPostResults(
@@ -1931,7 +1921,7 @@ class GamesManager(
gameId: GameId,
humanPlayers: Vector[WaitingGamePlayerInfo],
aiPlayerCount: Int
): GameControllerWithPostResults = {
): GameControllerWithPostResults = this.synchronized {
val logger = SimpleTimedLogger.printLogger
val gameIdHex = gameId.toHexString
logger.logLine(s"[CREATE:$gameIdHex] Starting game creation")
@@ -2051,13 +2041,15 @@ class GamesManager(
val remainingPlayers = gap.existingHumanPlayers.filterNot(_.userId == userId)
if remainingPlayers.isEmpty then
// Remove the waiting game entirely
gamesAwaitingPlayers = gamesAwaitingPlayers.filterNot(_.gameId == gameId)
updateState(_.withGamesAwaitingPlayers(gamesAwaitingPlayers.filterNot(_.gameId == gameId)))
else
// Keep the game but remove this user
gamesAwaitingPlayers = gamesAwaitingPlayers.map { g =>
if g.gameId == gameId then g.copy(existingHumanPlayers = remainingPlayers)
else g
}
updateState(
_.withGamesAwaitingPlayers(gamesAwaitingPlayers.map { g =>
if g.gameId == gameId then g.copy(existingHumanPlayers = remainingPlayers)
else g
})
)
save()
DropGameResponse(
result = DropGameResult.SUCCESS_DROP_GAME_RESULT,
@@ -2090,7 +2082,7 @@ class GamesManager(
// the lobby update reflects the removal right away. Keep the underlying game data in
// its current backend so old games remain inspectable without storage-format conversion.
unloadGame(gameId): Unit
removedGameIds += gameId // Track removal to prevent re-adding from storage
updateState(_.withRemovedGame(gameId)) // Track removal to prevent re-adding from storage
save()
recordDroppedGame(gameId, System.currentTimeMillis())
Future {
@@ -2107,7 +2099,7 @@ class GamesManager(
} else {
// Other humans remain - convert to AI
val updatedController = controller.dropUser(userId)
gameControllerInfos(gameId).controller = updatedController
updateController(gameId, updatedController)
// Trigger AI commands if it's now their turn
val currentState = updatedController.engine.currentState
@@ -2121,7 +2113,7 @@ class GamesManager(
)
)
)
gameControllerInfos(gameId).controller = gcwpr.gameController
updateController(gameId, gcwpr.gameController)
save()
DropGameResponse(
result = DropGameResult.SUCCESS_DROP_GAME_RESULT,
@@ -2180,7 +2172,7 @@ class GamesManager(
)
else {
val updatedController = controller.promoteAiToHuman(factionId, newUserId)
gameControllerInfos(gameId).controller = updatedController
updateController(gameId, updatedController)
save()
ConvertAiToHumanResponse(result = Result.SUCCESS)
}
@@ -2232,7 +2224,7 @@ class GamesManager(
)
)
)
gameControllerInfos(gameId).controller = gcwpr.gameController
updateController(gameId, gcwpr.gameController)
save()
ConvertHumanToAiResponse(result = Result.SUCCESS)
}
@@ -2283,7 +2275,7 @@ class GamesManager(
)
case Some((previousUserId, updatedController)) =>
gameControllerInfos(gameId).controller = updatedController
updateController(gameId, updatedController)
save()
ReassignFactionResponse(
result = Result.SUCCESS,
@@ -2364,7 +2356,7 @@ class GamesManager(
// Track removal and save if the game was running or had save files deleted.
// This ensures the game is removed from games.e0es even if it wasn't loaded.
if wasRunning || deleteSaveFiles then {
removedGameIds += gameId
updateState(_.withRemovedGame(gameId))
save()
}
@@ -0,0 +1,93 @@
package net.eagle0.eagle.service
import net.eagle0.common.tutorial_battle_config.TutorialBattleConfig
import net.eagle0.eagle.{GameId, UserId}
import net.eagle0.eagle.internal.hero.Hero
import net.eagle0.eagle.internal.running_games.RunningGame
import net.eagle0.eagle.service.new_game_creation.ExpandedGameParameters
case class WaitingGamePlayerInfo(
userId: UserId,
leaderNameTextId: String
)
case class GameAwaitingPlayers(
expandedGameParameters: ExpandedGameParameters,
gameId: GameId,
existingHumanPlayers: Vector[WaitingGamePlayerInfo],
totalPlayerCount: Int,
humanPlayerCount: Int,
greatPeople: Vector[Hero]
) {
private def claimedNameTextIds: Vector[String] =
existingHumanPlayers.map(_.leaderNameTextId)
def availableNameTextIds: Vector[String] =
greatPeople
.map(_.nameTextId)
.filterNot(claimedNameTextIds.contains)
def withNewWaitingPlayerInfo(
waitingGamePlayerInfo: WaitingGamePlayerInfo
): GameAwaitingPlayers =
copy(existingHumanPlayers = existingHumanPlayers :+ waitingGamePlayerInfo)
}
case class GamesManagerState(
controllerRegistry: GameControllerRegistry,
gamesAwaitingPlayers: Vector[GameAwaitingPlayers],
removedGameIds: Set[GameId] = Set.empty,
lastDroppedGameCleanupCheckMillis: Long = 0L,
loadingGameIds: Set[GameId] = Set.empty,
tutorialBattleConfigs: Map[GameId, TutorialBattleConfig] = Map.empty,
lastFlushMarkerCheck: Long = 0L,
flushMarkerModTime: Long = 0L,
cachedRunningGames: Option[Vector[RunningGame]] = None
) {
def withControllerInfo(gameId: GameId, controllerInfo: ControllerInfo): GamesManagerState =
copy(controllerRegistry = controllerRegistry.put(gameId, controllerInfo))
def withControllerRegistry(updatedControllerRegistry: GameControllerRegistry): GamesManagerState =
copy(controllerRegistry = updatedControllerRegistry)
def withLoadingGame(gameId: GameId): GamesManagerState =
copy(loadingGameIds = loadingGameIds + gameId)
def withoutLoadingGame(gameId: GameId): GamesManagerState =
copy(loadingGameIds = loadingGameIds - gameId)
def withGamesAwaitingPlayers(updatedGamesAwaitingPlayers: Vector[GameAwaitingPlayers]): GamesManagerState =
copy(gamesAwaitingPlayers = updatedGamesAwaitingPlayers)
def withTutorialBattleConfig(gameId: GameId, tutorialConfig: TutorialBattleConfig): GamesManagerState =
copy(tutorialBattleConfigs = tutorialBattleConfigs + (gameId -> tutorialConfig))
def withLastDroppedGameCleanupCheck(millis: Long): GamesManagerState =
copy(lastDroppedGameCleanupCheckMillis = millis)
def withLastFlushMarkerCheck(millis: Long): GamesManagerState =
copy(lastFlushMarkerCheck = millis)
def withFlushMarkerModTime(millis: Long): GamesManagerState =
copy(flushMarkerModTime = millis)
def withRemovedGame(gameId: GameId): GamesManagerState =
copy(removedGameIds = removedGameIds + gameId)
def withoutRemovedGames: GamesManagerState =
copy(removedGameIds = Set.empty)
def withoutLoadedGames: GamesManagerState =
copy(
controllerRegistry = GameControllerRegistry(Map.empty),
gamesAwaitingPlayers = Vector.empty,
removedGameIds = Set.empty,
cachedRunningGames = None
)
def withCachedRunningGames(runningGames: Vector[RunningGame]): GamesManagerState =
copy(cachedRunningGames = Some(runningGames))
def withoutCachedRunningGames: GamesManagerState =
copy(cachedRunningGames = None)
}
@@ -1041,8 +1041,6 @@ final case class GameController(
case gc => gc
}
debugCheckFreeHeroBackstoryVisibility(newGc)
GameControllerWithPostResults(
gameController = newGc,
postResults = PostResults(
@@ -1053,42 +1051,6 @@ final case class GameController(
)
}
// TEMPORARY: Log warnings when a free hero's backstory is not visible to the province's ruling faction.
// This helps find cases where visibility isn't being extended properly.
private def debugCheckFreeHeroBackstoryVisibility(gc: GameController): Unit = {
val gs = gc.engine.currentState
val cts = gc.clientTextStore
val freeHeroBackstories = gs.provinces.values.toVector.flatMap { province =>
province.rulingFactionId.toVector.flatMap { rulingFactionId =>
province.unaffiliatedHeroes.flatMap { uh =>
gs.heroes.get(uh.heroId).toVector.flatMap { hero =>
hero.backstoryVersions.lastOption.toVector.map { latestVersion =>
(province, rulingFactionId, uh, latestVersion.textId)
}
}
}
}
}
if freeHeroBackstories.nonEmpty then {
val visibilityByTextId = cts.accessibleToForIds(
freeHeroBackstories.map(_._4).toSet
)
for {
(province, rulingFactionId, uh, textId) <- freeHeroBackstories
} {
val accessibleTo = visibilityByTextId.getOrElse(textId, Vector.empty)
if accessibleTo.nonEmpty && !accessibleTo.contains(rulingFactionId) then
SimpleTimedLogger.printLogger.logLine(
s"[BACKSTORY-VISIBILITY] game=${gs.gameId} historyCount=${gc.engine.history.count}" +
s" round=${gs.currentRoundId} phase=${gs.currentPhase}" +
s" | Hero ${uh.heroId} in province ${province.id} (ruled by fid $rulingFactionId)" +
s" has backstory '$textId' visible only to fids [${accessibleTo.mkString(", ")}]" +
s" — ruling faction CANNOT see it. UH type: ${uh.unaffiliatedHeroType}"
)
}
}
}
def streamUpdates(
userId: UserId,
clientId: String,
@@ -173,6 +173,7 @@ scala_library(
"//src/main/protobuf/net/eagle0/common:tutorial_battle_config_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:shardok_battle_scala_proto",
"//src/main/scala/net/eagle0/common:grpc_retrier",
"//src/main/scala/net/eagle0/common:simple_timed_logger",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:vigor_to_constitution_xp_multiplier",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
@@ -7,6 +7,7 @@ import scala.util.{Failure, Success}
import io.grpc.{Status as GrpcStatus, StatusRuntimeException}
import io.grpc.stub.StreamObserver
import io.sentry.Sentry
import net.eagle0.common.{GrpcRetrier, SimpleTimedLogger}
import net.eagle0.common.common_unit.*
import net.eagle0.common.game_setup_info.{GameSetupInfo, NewGameRequest}
import net.eagle0.common.player_info.{AlliedPlayer, PlayerSetupInfo}
@@ -15,7 +16,6 @@ import net.eagle0.common.shardok_internal_interface.*
import net.eagle0.common.shardok_internal_interface.GameStatusResponse.Specific
import net.eagle0.common.shardok_internal_interface.ShardokInternalInterfaceGrpc.ShardokInternalInterfaceStub
import net.eagle0.common.tutorial_battle_config.TutorialBattleConfig
import net.eagle0.common.GrpcRetrier
import net.eagle0.eagle.{FactionId, GameId, ShardokGameId}
import net.eagle0.eagle.internal.army.MovingArmy
import net.eagle0.eagle.internal.shardok_battle.{ShardokBattle, ShardokPlayer}
@@ -118,13 +118,12 @@ class ShardokInterfaceGrpcClient(
}
catch {
case e: Exception =>
println(s"Exception processing stream response for $shardokGameId: $e")
e.printStackTrace()
SimpleTimedLogger.printLogger.logLine(s"Exception processing stream response for $shardokGameId: $e")
Sentry.captureException(e): Unit
}
override def onError(t: Throwable): Unit = {
println(s"Stream error for $shardokGameId: $t")
SimpleTimedLogger.printLogger.logLine(s"Stream error for $shardokGameId: $t")
t match {
case sre: StatusRuntimeException
if sre.getStatus.getCode == GrpcStatus.Code.UNAVAILABLE ||
@@ -137,7 +136,7 @@ class ShardokInterfaceGrpcClient(
}
override def onCompleted(): Unit =
println(s"Stream completed for $shardokGameId")
SimpleTimedLogger.printLogger.logLine(s"Stream completed for $shardokGameId")
}
ShardokLatencyTrace.trace(
@@ -164,11 +163,11 @@ class ShardokInterfaceGrpcClient(
pendingBattles.synchronized {
pendingBattles.get(shardokGameId) match {
case Some((_, storedRequest)) =>
println(s"Reconnecting stream for $shardokGameId (delay was ${delayMs}ms)")
SimpleTimedLogger.printLogger.logLine(s"Reconnecting stream for $shardokGameId (delay was ${delayMs}ms)")
try subscribeToGame(eagleGameId, shardokGameId, Some(storedRequest))
catch {
case e: Exception =>
println(s"Reconnect failed for $shardokGameId: $e")
SimpleTimedLogger.printLogger.logLine(s"Reconnect failed for $shardokGameId: $e")
Sentry.captureException(e): Unit
// Exponential backoff with max 10 seconds
val nextDelay = Math.min(delayMs * 2, 10000)
@@ -284,7 +283,7 @@ class ShardokInterfaceGrpcClient(
// Empty response - nothing to process
}
case None =>
println(s"No pending battle found for ${update.gameId}")
SimpleTimedLogger.printLogger.logLine(s"No pending battle found for ${update.gameId}")
}
}
@@ -309,7 +308,7 @@ class ShardokInterfaceGrpcClient(
humanFactions: Set[FactionId],
tutorialBattleConfig: Option[TutorialBattleConfig] = None
): Unit = {
println(
SimpleTimedLogger.printLogger.logLine(
s"${battle.shardokGameId} sending armies " +
s"${battle.players.map(sp => sp.getArmyGroup.armies.flatMap(_.getArmy.units).map(_.heroId).sorted.mkString(" ")).mkString(", ")}"
)
@@ -412,11 +411,11 @@ class ShardokInterfaceGrpcClient(
"error" -> e.getClass.getName
)
)
println(s"Failed to post command: $e")
SimpleTimedLogger.printLogger.logLine(s"Failed to post command: $e")
Sentry.captureException(e): Unit
throw e
}
else print("Invalid token\n")
else SimpleTimedLogger.printLogger.logLine("Invalid token")
override def postPlacementCommands(
eagleGameId: GameId,
@@ -486,11 +485,11 @@ class ShardokInterfaceGrpcClient(
"error" -> e.getClass.getName
)
)
println(s"Failed to post placement command: $e")
SimpleTimedLogger.printLogger.logLine(s"Failed to post placement command: $e")
Sentry.captureException(e): Unit
throw e
}
else print("Invalid token\n")
else SimpleTimedLogger.printLogger.logLine("Invalid token")
override def getHexMap(mapName: ShardokGameId): Future[HexMap] =
retrier
@@ -511,11 +510,15 @@ class ShardokInterfaceGrpcClient(
toRemove.foreach { shardokGameId =>
val _ = pendingBattles.remove(shardokGameId)
println(s"Cancelled pending Shardok battle $shardokGameId for deleted Eagle game $eagleGameId")
SimpleTimedLogger.printLogger.logLine(
s"Cancelled pending Shardok battle $shardokGameId for deleted Eagle game $eagleGameId"
)
}
if toRemove.nonEmpty then
println(s"Cancelled ${toRemove.size} Shardok battle(s) for deleted Eagle game $eagleGameId")
SimpleTimedLogger.printLogger.logLine(
s"Cancelled ${toRemove.size} Shardok battle(s) for deleted Eagle game $eagleGameId"
)
}
private def isArcheryCapable(eu: EagleUnit, gs: GameState): Boolean =
@@ -1,6 +1,18 @@
load("@rules_cc//cc:defs.bzl", "cc_test")
load("//tools:copts.bzl", "TEST_COPTS")
cc_test(
name = "byte_vector_test",
srcs = ["ByteVector_test.cpp"],
copts = TEST_COPTS,
linkstatic = True,
deps = [
"//src/main/cpp/net/eagle0/common:byte_vector",
"@googletest//:gtest",
"@googletest//:gtest_main",
],
)
cc_test(
name = "iterable_bitset_test",
srcs = ["IterableBitset_test.cpp"],
@@ -0,0 +1,59 @@
#include <cstdint>
#include <sstream>
#include <string>
#include <vector>
#include "gtest/gtest.h"
#include "src/main/cpp/net/eagle0/common/byte_vector.hpp"
TEST(ByteVectorTest, fromHex_parsesMixedCasePairs) {
const byte_vector bytes = byte_vector::FromHex("00aF10");
EXPECT_EQ(bytes, std::vector<uint8_t>({0x00, 0xAF, 0x10}));
}
TEST(ByteVectorTest, fromHex_acceptsOptionalHexPrefixes) {
EXPECT_EQ(byte_vector::FromHex("0x0123"), std::vector<uint8_t>({0x01, 0x23}));
EXPECT_EQ(byte_vector::FromHex("0X0123"), std::vector<uint8_t>({0x01, 0x23}));
EXPECT_EQ(byte_vector::FromHex("x0123"), std::vector<uint8_t>({0x01, 0x23}));
EXPECT_EQ(byte_vector::FromHex("X0123"), std::vector<uint8_t>({0x01, 0x23}));
}
TEST(ByteVectorTest, fromHex_emptyStringReturnsEmptyVector) {
EXPECT_TRUE(byte_vector::FromHex("").empty());
}
TEST(ByteVectorTest, fromHex_rejectsInvalidHexDigit) {
EXPECT_THROW(byte_vector::FromHex("12GG"), byte_vector::HexException);
}
TEST(ByteVectorTest, fromHex_rejectsOddLengthInput) {
EXPECT_THROW(byte_vector::FromHex("F"), byte_vector::HexException);
EXPECT_THROW(byte_vector::FromHex("0x123"), byte_vector::HexException);
}
TEST(ByteVectorTest, exposesOwnedByteAccessors) {
byte_vector bytes({0x01, 0x02});
bytes.reserve(4);
bytes += static_cast<uint8_t>(0x03);
bytes.append("A", 1);
EXPECT_EQ(bytes.size(), 4U);
EXPECT_FALSE(bytes.empty());
EXPECT_EQ(bytes[0], 0x01);
EXPECT_EQ(bytes[3], 'A');
EXPECT_EQ(
std::vector<uint8_t>(bytes.begin(), bytes.end()),
std::vector<uint8_t>({1, 2, 3, 65}));
}
TEST(ByteVectorTest, convertsToStringAndStreamsBytes) {
const byte_vector bytes(std::string("abc"));
EXPECT_EQ(static_cast<std::string>(bytes), "abc");
std::ostringstream stream;
stream << bytes;
EXPECT_EQ(stream.str(), "abc");
}
@@ -8,6 +8,8 @@
#include "src/main/cpp/net/eagle0/common/SequenceRandomGenerator.hpp"
#include <limits>
#include "gtest/gtest.h"
TEST(SequenceRandomGeneratorTests, testSequence) {
@@ -22,3 +24,16 @@ TEST(SequenceRandomGeneratorTests, testSequence) {
EXPECT_EQ(generator.Percentile(), 25.0);
EXPECT_EQ(generator.Percentile(), 75.0);
}
TEST(SequenceRandomGeneratorTests, rejectsEmptySequences) {
EXPECT_THROW(SequenceRandomGenerator({}), std::invalid_argument);
EXPECT_THROW(SequenceRandomGenerator::WithSequence({}), std::invalid_argument);
}
TEST(SequenceRandomGeneratorTests, rejectsValuesOutsideProbabilityRange) {
EXPECT_THROW(SequenceRandomGenerator({-0.01}), std::invalid_argument);
EXPECT_THROW(SequenceRandomGenerator({1.01}), std::invalid_argument);
EXPECT_THROW(
SequenceRandomGenerator({std::numeric_limits<double>::quiet_NaN()}),
std::invalid_argument);
}
@@ -18,6 +18,16 @@
using std::vector;
class ConstantRandomGenerator final : public RandomGenerator {
private:
double value;
auto DoubleZeroToOne() -> double override { return value; }
public:
explicit ConstantRandomGenerator(const double v) : value(v) {}
};
TEST(StdLibraryGenerator, Percentile) {
StdLibraryGenerator generator;
@@ -181,6 +191,42 @@ TEST(StdLibraryGenerator, IntBelow) {
}
}
TEST(RandomGenerator, IntBetweenClampsEndpointOneToExclusiveUpperBound) {
ConstantRandomGenerator generator(1.0);
EXPECT_EQ(9, generator.IntBetween(0, 10));
EXPECT_EQ(4, generator.IntBetween(-5, 5));
EXPECT_EQ(0, generator.IntBelow(1));
}
TEST(RandomGenerator, RandomElementHandlesEndpointOneForSingleElementVector) {
ConstantRandomGenerator generator(1.0);
EXPECT_EQ(42, generator.RandomElement(std::vector{42}));
}
TEST(RandomGenerator, IntBetweenRejectsEmptyRanges) {
ConstantRandomGenerator generator(0.5);
EXPECT_THROW(generator.IntBetween(3, 3), std::invalid_argument);
EXPECT_THROW(generator.IntBetween(4, 3), std::invalid_argument);
EXPECT_THROW(generator.IntBelow(0), std::invalid_argument);
}
TEST(StdLibraryGenerator, IntBetweenRejectsEmptyRanges) {
StdLibraryGenerator generator;
EXPECT_THROW(generator.IntBetween(3, 3), std::invalid_argument);
EXPECT_THROW(generator.IntBetween(4, 3), std::invalid_argument);
EXPECT_THROW(generator.IntBelow(0), std::invalid_argument);
}
TEST(RandomGenerator, RandomElementRejectsEmptyVector) {
ConstantRandomGenerator generator(0.5);
EXPECT_THROW(generator.RandomElement(std::vector<int>{}), std::invalid_argument);
}
TEST(RandomGenerator, chanceAtOrAbove) {
EXPECT_DOUBLE_EQ(0.50, RandomGenerator::ChanceOpenEndedPercentileAtOrAbove(50.0));
EXPECT_DOUBLE_EQ(0.10, RandomGenerator::ChanceOpenEndedPercentileAtOrAbove(90.0));
@@ -81,8 +81,8 @@ protected:
TEST_F(AIAttackerStrategySelectorTest,
CanClaimCastles_defenderOccupiesOne_returnsAttackCastlesStrategy) {
CoordsSet criticalTileCoords(6, 6);
criticalTileCoords.AddIndex(0);
criticalTileCoords.AddIndex(1);
criticalTileCoords.AddMapIndexUnchecked(0);
criticalTileCoords.AddMapIndexUnchecked(1);
// 2 critical tiles, 3 attackers
// defender in critical tile
@@ -108,8 +108,8 @@ TEST_F(AIAttackerStrategySelectorTest,
TEST_F(AIAttackerStrategySelectorTest, DefenderNotOnCastle_returnsAttackUnitsStrategy) {
CoordsSet criticalTileCoords(6, 6);
criticalTileCoords.AddIndex(0);
criticalTileCoords.AddIndex(1);
criticalTileCoords.AddMapIndexUnchecked(0);
criticalTileCoords.AddMapIndexUnchecked(1);
// 2 critical tiles, 3 attackers
// defender not in critical tile - they're scattering, so chase them
@@ -135,8 +135,8 @@ TEST_F(AIAttackerStrategySelectorTest, DefenderNotOnCastle_returnsAttackUnitsStr
TEST_F(AIAttackerStrategySelectorTest, NoDefendersRemaining_returnsHoldCastlesStrategy) {
CoordsSet criticalTileCoords(6, 6);
criticalTileCoords.AddIndex(0);
criticalTileCoords.AddIndex(1);
criticalTileCoords.AddMapIndexUnchecked(0);
criticalTileCoords.AddMapIndexUnchecked(1);
// 2 critical tiles, 3 attackers
// defender has fled - no defenders remaining
@@ -162,10 +162,10 @@ TEST_F(AIAttackerStrategySelectorTest, NoDefendersRemaining_returnsHoldCastlesSt
TEST_F(AIAttackerStrategySelectorTest, TooManyCastles_returnsAttackUnitsStrategy) {
CoordsSet criticalTileCoords(6, 6);
criticalTileCoords.AddIndex(0);
criticalTileCoords.AddIndex(1);
criticalTileCoords.AddIndex(2);
criticalTileCoords.AddIndex(3);
criticalTileCoords.AddMapIndexUnchecked(0);
criticalTileCoords.AddMapIndexUnchecked(1);
criticalTileCoords.AddMapIndexUnchecked(2);
criticalTileCoords.AddMapIndexUnchecked(3);
// 4 critical tiles, 3 attackers
EXPECT_EQ(
@@ -79,10 +79,10 @@ protected:
TEST_F(AIStrategySelectorTest, NotEnoughAttackerUnits_returnsScatterStrategy) {
CoordsSet criticalTileCoords(6, 6);
criticalTileCoords.AddIndex(0);
criticalTileCoords.AddIndex(1);
criticalTileCoords.AddIndex(2);
criticalTileCoords.AddIndex(3);
criticalTileCoords.AddMapIndexUnchecked(0);
criticalTileCoords.AddMapIndexUnchecked(1);
criticalTileCoords.AddMapIndexUnchecked(2);
criticalTileCoords.AddMapIndexUnchecked(3);
// 4 critical tiles, 3 attackers
EXPECT_EQ(
@@ -100,10 +100,10 @@ TEST_F(AIStrategySelectorTest, NotEnoughAttackerUnits_returnsScatterStrategy) {
TEST_F(AIStrategySelectorTest, EnoughAttackerUnitsOnlyIncludingUndead_returnsScatterStrategy) {
CoordsSet criticalTileCoords(6, 6);
criticalTileCoords.AddIndex(0);
criticalTileCoords.AddIndex(1);
criticalTileCoords.AddIndex(2);
criticalTileCoords.AddIndex(3);
criticalTileCoords.AddMapIndexUnchecked(0);
criticalTileCoords.AddMapIndexUnchecked(1);
criticalTileCoords.AddMapIndexUnchecked(2);
criticalTileCoords.AddMapIndexUnchecked(3);
// 4 critical tiles, 3 attackers
extraUnit0->mutate_player_id(0);
@@ -125,10 +125,10 @@ TEST_F(AIStrategySelectorTest, EnoughAttackerUnitsOnlyIncludingUndead_returnsSca
TEST_F(AIStrategySelectorTest, EnoughAttackerUnits_returnsHoldCastlesStrategy) {
CoordsSet criticalTileCoords(6, 6);
criticalTileCoords.AddIndex(0);
criticalTileCoords.AddIndex(1);
criticalTileCoords.AddIndex(2);
criticalTileCoords.AddIndex(3);
criticalTileCoords.AddMapIndexUnchecked(0);
criticalTileCoords.AddMapIndexUnchecked(1);
criticalTileCoords.AddMapIndexUnchecked(2);
criticalTileCoords.AddMapIndexUnchecked(3);
// 4 critical tiles, 3 attackers
extraUnit0->mutate_player_id(0);
@@ -151,10 +151,10 @@ TEST_F(AIStrategySelectorTest, EnoughAttackerUnits_returnsHoldCastlesStrategy) {
TEST_F(AIStrategySelectorTest,
NotEnoughAttackerUnits_defenderOver60PercentOfAttacker_returnsHoldCastlesStrategy) {
CoordsSet criticalTileCoords(6, 6);
criticalTileCoords.AddIndex(0);
criticalTileCoords.AddIndex(1);
criticalTileCoords.AddIndex(2);
criticalTileCoords.AddIndex(3);
criticalTileCoords.AddMapIndexUnchecked(0);
criticalTileCoords.AddMapIndexUnchecked(1);
criticalTileCoords.AddMapIndexUnchecked(2);
criticalTileCoords.AddMapIndexUnchecked(3);
// 4 critical tiles, 3 attackers
// 900 attackers, 541 defenders
@@ -180,9 +180,9 @@ TEST_F(AIStrategySelectorTest,
TEST_F(AIStrategySelectorTest,
DefenderUnder15PercentOfAttacker_enoughAttackerUnits_returnsFleeStrategy) {
CoordsSet criticalTileCoords(6, 6);
criticalTileCoords.AddIndex(0);
criticalTileCoords.AddIndex(1);
criticalTileCoords.AddIndex(2);
criticalTileCoords.AddMapIndexUnchecked(0);
criticalTileCoords.AddMapIndexUnchecked(1);
criticalTileCoords.AddMapIndexUnchecked(2);
// 3 critical tiles, 3 attackers
// 900 attackers, 134 defenders
@@ -208,10 +208,10 @@ TEST_F(AIStrategySelectorTest,
TEST_F(AIStrategySelectorTest,
DefenderUnder15PercentOfAttacker_notEnoughAttackerUnits_returnsScatterStrategy) {
CoordsSet criticalTileCoords(6, 6);
criticalTileCoords.AddIndex(0);
criticalTileCoords.AddIndex(1);
criticalTileCoords.AddIndex(2);
criticalTileCoords.AddIndex(3);
criticalTileCoords.AddMapIndexUnchecked(0);
criticalTileCoords.AddMapIndexUnchecked(1);
criticalTileCoords.AddMapIndexUnchecked(2);
criticalTileCoords.AddMapIndexUnchecked(3);
// 4 critical tiles, 3 attackers
// 900 attackers, 134 defenders
@@ -237,9 +237,9 @@ TEST_F(AIStrategySelectorTest,
TEST_F(AIStrategySelectorTest,
DefenderUnder15PercentOfAttacker_enoughAttackerUnits_cannotFlee_returnsScatterStrategy) {
CoordsSet criticalTileCoords(6, 6);
criticalTileCoords.AddIndex(0);
criticalTileCoords.AddIndex(1);
criticalTileCoords.AddIndex(2);
criticalTileCoords.AddMapIndexUnchecked(0);
criticalTileCoords.AddMapIndexUnchecked(1);
criticalTileCoords.AddMapIndexUnchecked(2);
// 3 critical tiles, 3 attackers
// 900 attackers, 134 defenders
@@ -266,9 +266,9 @@ TEST_F(AIStrategySelectorTest,
TEST_F(AIStrategySelectorTest,
DefenderUnder15PercentOfAttacker_enoughAttackerUnits_lateGame_returnsScatterStrategy) {
CoordsSet criticalTileCoords(6, 6);
criticalTileCoords.AddIndex(0);
criticalTileCoords.AddIndex(1);
criticalTileCoords.AddIndex(2);
criticalTileCoords.AddMapIndexUnchecked(0);
criticalTileCoords.AddMapIndexUnchecked(1);
criticalTileCoords.AddMapIndexUnchecked(2);
// 3 critical tiles, 3 attackers
// 900 attackers, 134 defenders
@@ -19,7 +19,8 @@ namespace shardok {
* @param defenderToggle If true, AI is defender. If false, AI is attacker.
* @return GameStateW in SETUP phase with 6 units per player in reserve
*/
auto CreatePerfTestGameState(const GameSettingsSPtr& settings, bool defenderToggle) -> GameStateW;
[[nodiscard]] auto CreatePerfTestGameState(const GameSettingsSPtr& settings, bool defenderToggle)
-> GameStateW;
} // namespace shardok
@@ -111,6 +111,7 @@ cc_library(
copts = COPTS,
visibility = ["//src/test/cpp/net/eagle0/shardok:__subpackages__"],
deps = [
"//src/main/cpp/net/eagle0/shardok/library/util:action_result_flatbuffer_helpers",
"//src/main/flatbuffer/net/eagle0/shardok/storage:unit_cc_fbs",
"//src/main/protobuf/net/eagle0/shardok/common:hex_map_cc_proto",
"//src/main/protobuf/net/eagle0/shardok/storage:action_result_cc_proto",
@@ -26,10 +26,11 @@ using Coords = net::eagle0::shardok::storage::fb::Coords;
using shardok::PlayerId;
using shardok::UnitId;
auto AddPlayerInfo(flatbuffers::FlatBufferBuilder &fbb, int playerId, bool isDefender, int food)
[[nodiscard]] auto
AddPlayerInfo(flatbuffers::FlatBufferBuilder &fbb, int playerId, bool isDefender, int food)
-> flatbuffers::Offset<net::eagle0::shardok::storage::fb::PlayerInfo>;
auto AddPlayerInfo(
[[nodiscard]] auto AddPlayerInfo(
flatbuffers::FlatBufferBuilder &fbb,
int playerId,
bool isDefender,
@@ -37,7 +38,7 @@ auto AddPlayerInfo(
std::vector<int8_t> victoryConditions)
-> flatbuffers::Offset<net::eagle0::shardok::storage::fb::PlayerInfo>;
auto AddPlayerInfo(
[[nodiscard]] auto AddPlayerInfo(
flatbuffers::FlatBufferBuilder &fbb,
int playerId,
bool isDefender,
@@ -46,30 +47,30 @@ auto AddPlayerInfo(
std::vector<int> allyPids)
-> flatbuffers::Offset<net::eagle0::shardok::storage::fb::PlayerInfo>;
auto AddGenericUnit(
[[nodiscard]] auto AddGenericUnit(
PlayerId playerId,
UnitId unitId,
const net::eagle0::shardok::storage::fb::Coords &location) -> Unit;
auto AddGenericUnit(
[[nodiscard]] auto AddGenericUnit(
PlayerId playerId,
UnitId unitId,
const net::eagle0::shardok::storage::fb::Coords &location,
void (*modifier)(net::eagle0::shardok::storage::fb::Unit &)) -> Unit;
auto AddGenericUnit(
[[nodiscard]] auto AddGenericUnit(
PlayerId playerId,
UnitId unitId,
const net::eagle0::shardok::storage::fb::Coords &location,
net::eagle0::shardok::storage::fb::Profession profession,
void (*unitModifier)(net::eagle0::shardok::storage::fb::Unit &)) -> Unit;
auto AddGenericUndead(
[[nodiscard]] auto AddGenericUndead(
PlayerId playerId,
UnitId unitId,
const net::eagle0::shardok::storage::fb::Coords &location) -> Unit;
auto MakeGenericUnit(
[[nodiscard]] auto MakeGenericUnit(
PlayerId playerId,
UnitId unitId,
net::eagle0::shardok::storage::fb::Coords location) -> Unit;
@@ -4,6 +4,10 @@
#include "GtestExtensions.hpp"
#include <unordered_map>
#include "src/main/cpp/net/eagle0/shardok/library/util/ActionResultFlatbufferHelpers.hpp"
class NotFoundException : public std::exception {
private:
const std::string message;
@@ -21,22 +25,37 @@ auto ResultWithType(const std::vector<ActionResult>& results, ActionType type) -
return *result;
}
auto DecodeUnitBytesForTest(const std::string& unitBytes) -> Unit {
return shardok::UnitFromBytes(unitBytes);
}
auto DecodeResolvedUnitForTest(const ResolvedUnit& resolvedUnit) -> Unit {
return DecodeUnitBytesForTest(resolvedUnit.unit_bytes());
}
auto DecodedChangedUnitsForTest(const ActionResult& result) -> std::vector<Unit>& {
thread_local std::unordered_map<const ActionResult*, std::vector<Unit>> decodedByResult;
auto& units = decodedByResult[&result];
units.clear();
units.reserve(result.changed_units_fb_size());
for (const auto& unitBytes : result.changed_units_fb()) {
units.push_back(DecodeUnitBytesForTest(unitBytes));
}
return units;
}
auto ChangedUnitFbWithId(const ActionResult& result, const int id) -> Unit* {
const int changedUnitCount = result.changed_units_fb_size();
for (int i = 0; i < changedUnitCount; ++i) {
auto* unit =
const_cast<Unit*>(reinterpret_cast<const Unit*>(result.changed_units_fb(i).data()));
if (unit->unit_id() == id) return unit;
auto& units = DecodedChangedUnitsForTest(result);
for (auto& unit : units) {
if (unit.unit_id() == id) return &unit;
}
throw NotFoundException("No such changed id " + std::to_string(id));
}
auto MaybeChangedUnitFbWithId(const ActionResult& result, const int id) -> Unit* {
const int changedUnitCount = result.changed_units_fb_size();
for (int i = 0; i < changedUnitCount; ++i) {
auto* unit =
const_cast<Unit*>(reinterpret_cast<const Unit*>(result.changed_units_fb(i).data()));
if (unit->unit_id() == id) return unit;
auto& units = DecodedChangedUnitsForTest(result);
for (auto& unit : units) {
if (unit.unit_id() == id) return &unit;
}
return nullptr;
}
@@ -13,6 +13,7 @@
#include <iostream>
#include <optional>
#include <ranges>
#include <string>
#include <vector>
#include "gtest/gtest.h"
@@ -25,22 +26,26 @@
using net::eagle0::shardok::storage::fb::Unit;
auto operator==(
[[nodiscard]] auto operator==(
const net::eagle0::shardok::common::TileModifier_SingleModifier& lhs,
const net::eagle0::shardok::common::TileModifier_SingleModifier& rhs) -> bool;
auto operator==(
[[nodiscard]] auto operator==(
const net::eagle0::shardok::common::TileModifierWithCoords& lhs,
const net::eagle0::shardok::common::TileModifierWithCoords& rhs) -> bool;
using net::eagle0::shardok::common::ActionType;
using net::eagle0::shardok::storage::ActionResult;
using ResolvedUnit = net::eagle0::shardok::storage::ResolvedUnit;
auto ResultWithType(const std::vector<ActionResult>& results, ActionType type) -> ActionResult;
auto ChangedUnitFbWithId(const ActionResult& result, int id) -> Unit*;
auto MaybeChangedUnitFbWithId(const ActionResult& result, int id) -> Unit*;
[[nodiscard]] auto ResultWithType(const std::vector<ActionResult>& results, ActionType type)
-> ActionResult;
[[nodiscard]] auto DecodeUnitBytesForTest(const std::string& unitBytes) -> Unit;
[[nodiscard]] auto DecodeResolvedUnitForTest(const ResolvedUnit& resolvedUnit) -> Unit;
[[nodiscard]] auto ChangedUnitFbWithId(const ActionResult& result, int id) -> Unit*;
[[nodiscard]] auto MaybeChangedUnitFbWithId(const ActionResult& result, int id) -> Unit*;
template<class T, class VecT>
auto CHECK_CONTAINS(const VecT& vec, const T& obj) -> bool {
[[nodiscard]] auto CHECK_CONTAINS(const VecT& vec, const T& obj) -> bool {
// Use member function Contains if available (for custom types like CoordsSet)
if constexpr (requires { vec.Contains(obj); }) {
return vec.Contains(obj);
@@ -51,7 +56,7 @@ auto CHECK_CONTAINS(const VecT& vec, const T& obj) -> bool {
}
template<class T, class VecT>
auto CHECK_NOCONTAINS(const VecT& vec, const T& obj) -> bool {
[[nodiscard]] auto CHECK_NOCONTAINS(const VecT& vec, const T& obj) -> bool {
return !CHECK_CONTAINS(vec, obj);
}
@@ -66,7 +71,7 @@ void EXPECT_CONTAINS(const VecT& vec, const T& obj) {
}
template<class VecT, class F>
auto CHECK_NOCONTAINS_WHERE(const VecT& vec, F where) -> bool {
[[nodiscard]] auto CHECK_NOCONTAINS_WHERE(const VecT& vec, F where) -> bool {
for (size_t i = 0; i < vec.size(); ++i) {
if (where(vec[i])) {
std::cout << "Found at index " << i << '\n';
@@ -77,12 +82,12 @@ auto CHECK_NOCONTAINS_WHERE(const VecT& vec, F where) -> bool {
}
template<class T, class F>
auto GET_RESULT_WHERE(const std::vector<T>& vec, F where) -> T {
[[nodiscard]] auto GET_RESULT_WHERE(const std::vector<T>& vec, F where) -> T {
return *std::ranges::find_if(vec, where);
}
template<class T, class F>
auto CHECK_CONTAINS_WHERE(T& vec, F where) -> bool {
[[nodiscard]] auto CHECK_CONTAINS_WHERE(T& vec, F where) -> bool {
return std::ranges::find_if(vec, where) != vec.end();
}
@@ -34,7 +34,7 @@ constexpr int8_t HERO1_BRV = 90.0;
constexpr net::eagle0::shardok::storage::fb::Profession HEROT1_PROFESSION =
net::eagle0::shardok::storage::fb::Profession_NO_PROFESSION;
static inline auto GetHeroT1() -> net::eagle0::shardok::storage::fb::Hero {
[[nodiscard]] static inline auto GetHeroT1() -> net::eagle0::shardok::storage::fb::Hero {
net::eagle0::shardok::storage::fb::Hero hero;
hero.mutate_eagle_hero_id(1);
@@ -74,7 +74,7 @@ constexpr int8_t HERO2_BRV = 100.0;
constexpr net::eagle0::shardok::storage::fb::Profession HEROT2_PROFESSION =
net::eagle0::shardok::storage::fb::Profession_NO_PROFESSION;
inline auto GetHeroT2() -> net::eagle0::shardok::storage::fb::Hero {
[[nodiscard]] inline auto GetHeroT2() -> net::eagle0::shardok::storage::fb::Hero {
net::eagle0::shardok::storage::fb::Hero hero;
hero.mutate_eagle_hero_id(2);
@@ -23,15 +23,17 @@ using Terrain = net::eagle0::shardok::storage::fb::Terrain;
using TerrainProto = net::eagle0::shardok::common::Terrain;
static inline auto MakeCoordsSet(const HexMap* map, const std::initializer_list<Coords> list)
-> CoordsSet {
[[nodiscard]] static inline auto MakeCoordsSet(
const HexMap* map,
const std::initializer_list<Coords> list) -> CoordsSet {
CoordsSet cs(map);
for (const Coords& c : list) { cs.Add(c); }
return cs;
}
static inline auto MakeCoordsSet(const HexMapProto& map, const std::initializer_list<Coords> list)
-> CoordsSet {
[[nodiscard]] static inline auto MakeCoordsSet(
const HexMapProto& map,
const std::initializer_list<Coords> list) -> CoordsSet {
CoordsSet cs(map.row_count(), map.column_count());
for (const Coords& c : list) { cs.Add(c); }
return cs;
@@ -62,13 +64,14 @@ static vector<vector<Coords>> ATTACKER_STARTING_POSITIONS = {
/* 3 */ {Coords(0, 5), Coords(0, 4), Coords(1, 5)},
/* 4 */ {}};
static auto MakeSunnyWeather() -> MonthlyWeatherProto {
[[nodiscard]] static auto MakeSunnyWeather() -> MonthlyWeatherProto {
MonthlyWeatherProto mw;
mw.set_sun_chance(100);
mw.set_average_temperature(60);
return mw;
}
static auto MakeSunnyWeatherFb() -> net::eagle0::shardok::storage::fb::MonthlyWeather {
[[nodiscard]] static auto MakeSunnyWeatherFb()
-> net::eagle0::shardok::storage::fb::MonthlyWeather {
net::eagle0::shardok::storage::fb::MonthlyWeather mw;
mw.mutate_sun_chance(100);
mw.mutate_average_temperature(60);
@@ -80,13 +83,14 @@ static vector<MonthlyWeatherProto> SUNNY_MONTHLY_WEATHERS =
static vector<net::eagle0::shardok::storage::fb::MonthlyWeather> SUNNY_MONTHLY_WEATHERS_FB =
vector<net::eagle0::shardok::storage::fb::MonthlyWeather>(12, MakeSunnyWeatherFb());
static inline auto MutableTerrain(HexMap* map, const Coords& position) -> Terrain* {
[[nodiscard]] static inline auto MutableTerrain(HexMap* map, const Coords& position) -> Terrain* {
// const_cast is safe because we're accessing through a mutable HexMap pointer
return const_cast<Terrain*>(map->mutable_terrain()->GetMutableObject(
position.row() * map->column_count() + position.column()));
}
static inline auto MutableTerrain(HexMapProto& map, const Coords& position) -> TerrainProto* {
[[nodiscard]] static inline auto MutableTerrain(HexMapProto& map, const Coords& position)
-> TerrainProto* {
return map.mutable_terrain(position.row() * map.column_count() + position.column());
}
@@ -106,7 +110,8 @@ static auto MAP_WITH_STARTING_POSITIONS_FB = MakeHexMap(
ATTACKER_STARTING_POSITIONS,
SUNNY_MONTHLY_WEATHERS_FB);
static inline auto AddBasicMap(flatbuffers::FlatBufferBuilder& fbb) -> flatbuffers::Offset<HexMap> {
[[nodiscard]] static inline auto AddBasicMap(flatbuffers::FlatBufferBuilder& fbb)
-> flatbuffers::Offset<HexMap> {
return AddHexMap(
fbb,
BASIC_MAP_FB->row_count(),
@@ -28,10 +28,10 @@ using HexMapProto = net::eagle0::shardok::common::HexMap;
using MonthlyWeatherProto = net::eagle0::shardok::common::MonthlyWeather;
using TerrainTypeProto = net::eagle0::shardok::common::Terrain::Type;
auto AddHexMap(flatbuffers::FlatBufferBuilder &fbb, const HexMap *hm)
[[nodiscard]] auto AddHexMap(flatbuffers::FlatBufferBuilder &fbb, const HexMap *hm)
-> flatbuffers::Offset<HexMap>;
auto AddHexMap(
[[nodiscard]] auto AddHexMap(
flatbuffers::FlatBufferBuilder &fbb,
MapIndex rowCount,
MapIndex columnCount,
@@ -43,7 +43,7 @@ auto AddHexMap(
void EmplaceHexMap(GameStateW &gameState, const HexMap *hexMap);
auto MakeHexMap(
[[nodiscard]] auto MakeHexMap(
MapIndex rowCount,
MapIndex columnCount,
const std::vector<net::eagle0::shardok::storage::fb::Terrain_::Type> &terrainTypes,
@@ -52,7 +52,7 @@ auto MakeHexMap(
const std::vector<net::eagle0::shardok::storage::fb::MonthlyWeather> &monthlyWeathers)
-> HexMapWrapper;
auto MakeHexMapProto(
[[nodiscard]] auto MakeHexMapProto(
int rowCount,
int columnCount,
const std::vector<TerrainTypeProto> &terrainTypes,
@@ -241,7 +241,7 @@ auto GameWithHiddenDefenders(
allStartingUnits,
weather,
fireTiles,
CoordsSet(map.row_count(), map.column_count()),
CoordsSet(map.column_count(), map.row_count()),
{},
gen);
}
@@ -45,10 +45,10 @@ extern PlayerInfoProto ATTACKER_INFO;
extern PlayerInfoProto DEFENDER_INFO;
extern std::vector<PlayerInfoProto> PLAYER_INFOS;
auto GameWithUnplacedUnits(const HexMapProto &map, std::vector<Unit> &unplacedUnits)
[[nodiscard]] auto GameWithUnplacedUnits(const HexMapProto &map, std::vector<Unit> &unplacedUnits)
-> std::shared_ptr<ShardokEngine>;
auto GameWithHiddenDefenders(
[[nodiscard]] auto GameWithHiddenDefenders(
const HexMapProto &map,
std::vector<Unit> &attackingUnits,
std::vector<Unit> &hiddenDefenders,
@@ -57,7 +57,7 @@ auto GameWithHiddenDefenders(
const CoordsSet &fireTiles = CoordsSet(6, 6),
const std::shared_ptr<RandomGenerator> &gen = nullptr) -> std::shared_ptr<ShardokEngine>;
auto GameWithUnits(
[[nodiscard]] auto GameWithUnits(
const HexMapProto &map,
std::vector<Unit> &attackingUnitsWithDefaultIds,
std::vector<Unit> &defendingUnitsWithDefaultIds,
@@ -69,7 +69,7 @@ auto GameWithUnits(
const std::vector<std::pair<Coords, double>> &bridgedTiles = {})
-> std::shared_ptr<ShardokEngine>;
auto GameWithUnitsAndReservedSlots(
[[nodiscard]] auto GameWithUnitsAndReservedSlots(
const HexMapProto &map,
std::vector<Unit> &attackingUnitsWithDefaultIds,
std::vector<Unit> &defendingUnitsWithDefaultIds,
@@ -132,6 +132,6 @@ auto postEndTurnCommand(
std::shared_ptr<RandomGenerator> generator) -> std::vector<ActionResultView>;
// Load a game state from a binary file dumped by ShardokAIClient
auto LoadGameStateFromFile(const std::string &path) -> GameStateW;
[[nodiscard]] auto LoadGameStateFromFile(const std::string &path) -> GameStateW;
#endif // EAGLE0_TEST_SHARDOK_LIBRARY_SHARDOK_ENGINE_BASED_TEST_DATA_HPP
@@ -291,7 +291,7 @@ TEST_F(ShardokEngineTests, GetEligibleCharger_afterCharging_returnsAbsent) {
postCommandWithTypeAndTarget(game, 0, 0, MOVE_COMMAND, Coords(2, 1));
vector<ActionResultView> results;
results = postCommandWithTypeAndTarget(game, 0, 0, CHARGE_COMMAND, Coords(2, 0));
EXPECT_NE(0u, results.size());
EXPECT_NE(0U, results.size());
EXPECT_EQ(-1, game->GetCurrentGameState()->eligible_charger_id());
}
@@ -312,11 +312,11 @@ TEST_F(ShardokEngineTests, GetEligibleCharger_afterRestingHero_returnsAbsent) {
vector<ActionResultView> results;
results = postCommandWithTypeAndTarget(game, 0, 0, MOVE_COMMAND, Coords(2, 1));
EXPECT_NE(0u, results.size());
EXPECT_NE(0U, results.size());
EXPECT_NE(-1, game->GetCurrentGameState()->eligible_charger_id());
results = postCommandWithType(game, 0, 0, UNIT_REST_COMMAND);
EXPECT_NE(0u, results.size());
EXPECT_NE(0U, results.size());
EXPECT_EQ(-1, game->GetCurrentGameState()->eligible_charger_id());
}
@@ -339,11 +339,11 @@ TEST_F(ShardokEngineTests, GetEligibleCharger_afterStoppingHero_returnsAbsent) {
vector<ActionResultView> results;
results = postCommandWithTypeAndTarget(game, 0, 0, MOVE_COMMAND, Coords(2, 1));
EXPECT_NE(0u, results.size());
EXPECT_NE(0U, results.size());
EXPECT_NE(-1, game->GetCurrentGameState()->eligible_charger_id());
results = postCommandWithType(game, 0, 0, UNIT_STOP_COMMAND);
EXPECT_NE(0u, results.size());
EXPECT_NE(0U, results.size());
EXPECT_EQ(-1, game->GetCurrentGameState()->eligible_charger_id());
}
@@ -364,7 +364,7 @@ TEST_F(ShardokEngineTests, placeHero_placesHero) {
0,
PLACE_UNIT_COMMAND,
ATTACKER_STARTING_POSITIONS[0][0]);
EXPECT_EQ(1u, results.size());
EXPECT_EQ(1U, results.size());
const ActionResultView result = results[0];
@@ -399,14 +399,14 @@ TEST_F(ShardokEngineTests, placeLastHero_switchesPlayer) {
0,
PLACE_UNIT_COMMAND,
ATTACKER_STARTING_POSITIONS[0][0]);
EXPECT_NE(0u, results.size());
EXPECT_NE(0U, results.size());
results = postCommandWithTypeAndTarget(
game,
0,
1,
PLACE_UNIT_COMMAND,
ATTACKER_STARTING_POSITIONS[0][1]);
EXPECT_NE(0u, results.size());
EXPECT_NE(0U, results.size());
// post the end setup command
postWithResults(game, 0, 0);
@@ -432,14 +432,14 @@ TEST_F(ShardokEngineTests, placeLastHeroBothPlayers_startsGame) {
0,
PLACE_UNIT_COMMAND,
ATTACKER_STARTING_POSITIONS[0][0]);
EXPECT_NE(0u, results.size());
EXPECT_NE(0U, results.size());
results = postCommandWithTypeAndTarget(
game,
0,
1,
PLACE_UNIT_COMMAND,
ATTACKER_STARTING_POSITIONS[0][1]);
EXPECT_NE(0u, results.size());
EXPECT_NE(0U, results.size());
// post the end setup command
postWithResults(game, 0, 0);
@@ -450,7 +450,7 @@ TEST_F(ShardokEngineTests, placeLastHeroBothPlayers_startsGame) {
2,
PLACE_UNIT_COMMAND,
DEFENDER_STARTING_POSITIONS[0]);
EXPECT_NE(0u, results.size());
EXPECT_NE(0U, results.size());
// post the end setup command
results = postWithResults(game, 1, 0);
@@ -663,7 +663,7 @@ TEST_F(ShardokEngineTests, GetGameHistory_startMeteorLowKnowledge_filteredOut) {
const auto game = GameWithUnits(BASIC_MAP, attackerUnits, defenderUnits);
const auto results = postCommandWithType(game, 0, 0, METEOR_START_COMMAND);
EXPECT_NE(0u, results.size());
EXPECT_NE(0U, results.size());
auto history = game->GetFilteredGameHistory(0);
EXPECT_CONTAINS_WHERE(history, [](const ActionResultView &result) {

Some files were not shown because too many files have changed in this diff Show More