244 lines
5.5 KiB
C++
244 lines
5.5 KiB
C++
// this is a naive-ish implementation
|
|
// this is not pure naive, since it implements the less-branch Criterion compare variant discussed in the valve talk
|
|
|
|
#include <algorithm>
|
|
#include <string>
|
|
#include <map>
|
|
#include <set>
|
|
#include <vector>
|
|
#include <functional>
|
|
#include <optional>
|
|
#include <limits>
|
|
#include <random>
|
|
|
|
#include <cassert>
|
|
|
|
// context aka, kv-dict
|
|
|
|
// criterion, predicate that returns true or false based on state and context(query)
|
|
// equal: value == min == max
|
|
// smaller: value == max -e; min == -inf
|
|
// greater: value == min +e; max == +inf
|
|
struct Criterion {
|
|
std::string key;
|
|
float min, max;
|
|
|
|
bool compare(const float x) const {
|
|
return x >= min && x <= max;
|
|
}
|
|
|
|
bool operator<(const Criterion& other) const {
|
|
return key < other.key && min < other.min && max < other.max;
|
|
}
|
|
};
|
|
|
|
// rule, list of criterions (obv &&)
|
|
struct Rules {
|
|
std::set<Criterion> crits;
|
|
};
|
|
|
|
// query, ...?
|
|
struct Query {
|
|
// sorted?
|
|
std::map<std::string, float> dict;
|
|
|
|
Query& add(const std::string& key, float value) {
|
|
dict[key] = value;
|
|
return *this;
|
|
}
|
|
|
|
Query& add(const std::string& key, const std::string& value) {
|
|
// lol
|
|
dict[key] = std::hash<std::string>{}(value);
|
|
return *this;
|
|
}
|
|
|
|
bool match_criterion(const Criterion& crit) const {
|
|
// TODO: lookup is performed 2x, optimize
|
|
return dict.count(crit.key) && crit.compare(dict.at(crit.key));
|
|
}
|
|
|
|
bool match_rules(const Rules& rules) const {
|
|
for (const auto& crit : rules.crits) {
|
|
if (!match_criterion(crit)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
};
|
|
|
|
// response -> action (list?)
|
|
struct Response {
|
|
Rules rules;
|
|
|
|
// normally you would give it the caller(entity f is called on?)
|
|
std::function<void(void)> f;
|
|
|
|
Response(void) = default;
|
|
Response(const Rules& r, std::function<void(void)>&& _f) : rules(r), f(_f) {}
|
|
|
|
size_t criterion_size(void) const {
|
|
return rules.crits.size();
|
|
}
|
|
};
|
|
|
|
// special keys (usually bucketalbe):
|
|
// - concept: onHit, onIdle, onResponse, onObject?
|
|
// - "who": which char talked. gonna name this differently
|
|
|
|
struct ResponseDB {
|
|
std::vector<Response> responses;
|
|
|
|
// returns index
|
|
template<typename RNG>
|
|
std::optional<size_t> match(const Query& q, RNG& rng) const {
|
|
size_t crit_count_for_match = 0;
|
|
|
|
std::vector<size_t> match_list;
|
|
|
|
//for (const auto& r : responses) {
|
|
for (size_t i = 0; i < responses.size(); i++) {
|
|
const auto& r = responses[i];
|
|
|
|
// skip rules with less criteria matching
|
|
if (r.criterion_size() < crit_count_for_match) {
|
|
continue;
|
|
}
|
|
|
|
if (q.match_rules(r.rules)) {
|
|
if (crit_count_for_match < r.criterion_size()) {
|
|
match_list.clear();
|
|
crit_count_for_match = r.criterion_size();
|
|
}
|
|
|
|
match_list.push_back(i);
|
|
}
|
|
}
|
|
|
|
if (match_list.empty()) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::shuffle(match_list.begin(), match_list.end(), rng);
|
|
|
|
return match_list.front();
|
|
}
|
|
|
|
template<typename RNG>
|
|
bool match_and_run(const Query& q, RNG& rng) const {
|
|
auto idx = match(q, rng);
|
|
if (idx.has_value()) {
|
|
responses.at(*idx).f();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
};
|
|
|
|
int main(void) {
|
|
std::map<float, std::string> reverse_string_map;
|
|
reverse_string_map[std::hash<std::string>{}("who")] = "who";
|
|
|
|
// crit test
|
|
|
|
Criterion hp_not_critical {
|
|
"hp",
|
|
// have at least 15% of max hp
|
|
0.15f,
|
|
std::numeric_limits<float>::infinity()
|
|
};
|
|
|
|
assert(hp_not_critical.compare(0.0f) == false);
|
|
assert(hp_not_critical.compare(0.15f) == true);
|
|
assert(hp_not_critical.compare(0.15f - std::numeric_limits<float>::epsilon()) == false);
|
|
assert(hp_not_critical.compare(1.0f) == true);
|
|
|
|
Criterion is_hero {
|
|
"who",
|
|
// tmp
|
|
0.f, 0.f
|
|
};
|
|
// key "who" is value "hero"
|
|
is_hero.min = std::hash<std::string>{}("hero");
|
|
is_hero.max = std::hash<std::string>{}("hero");
|
|
|
|
assert(is_hero.compare(std::hash<std::string>{}("hero")) == true);
|
|
assert(is_hero.compare(std::hash<std::string>{}("enemy")) == false);
|
|
assert(is_hero.compare(std::hash<std::string>{}("ally")) == false);
|
|
|
|
Criterion on_idle {
|
|
"concept",
|
|
// tmp
|
|
0.f, 0.f
|
|
};
|
|
// key "concept" is value "idle"
|
|
on_idle.min = std::hash<std::string>{}("idle");
|
|
on_idle.max = std::hash<std::string>{}("idle");
|
|
|
|
assert(on_idle.compare(std::hash<std::string>{}("idle")) == true);
|
|
assert(on_idle.compare(std::hash<std::string>{}("hero")) == false);
|
|
assert(on_idle.compare(std::hash<std::string>{}("hit")) == false);
|
|
|
|
Rules rules_hero_idle_chatter {{
|
|
on_idle, is_hero, hp_not_critical
|
|
}};
|
|
|
|
// query test
|
|
|
|
Query q1{};
|
|
q1
|
|
.add("concept", "idle")
|
|
.add("who", "hero")
|
|
.add("hp", 0.843122f)
|
|
.add("ally_near", 1.f)
|
|
.add("enemy_near", 0.f)
|
|
.add("in_danger", 0.f)
|
|
;
|
|
|
|
Query q2{};
|
|
q2
|
|
.add("concept", "hit")
|
|
.add("who", "enemy")
|
|
.add("hp", 0.12333f)
|
|
.add("ally_near", 0.f)
|
|
.add("enemy_near", 1.f)
|
|
.add("in_danger", 1.f)
|
|
;
|
|
|
|
// add junk to q1
|
|
// 100'000 -> >200ms
|
|
// 10'000 -> >ms
|
|
for (int i = 0; i < 10'000; i++) {
|
|
q1.add(std::to_string(i) + "asdf", (i - 155) * 12399.3249f);
|
|
}
|
|
|
|
assert(q1.match_criterion(hp_not_critical) == true);
|
|
assert(q1.match_criterion(is_hero) == true);
|
|
|
|
assert(q2.match_criterion(hp_not_critical) == false);
|
|
assert(q2.match_criterion(is_hero) == false);
|
|
|
|
assert(q1.match_rules(rules_hero_idle_chatter) == true);
|
|
assert(q2.match_rules(rules_hero_idle_chatter) == false);
|
|
|
|
// response DB
|
|
|
|
std::mt19937 rng{1337*433};
|
|
|
|
ResponseDB db;
|
|
size_t fn_called = 0;
|
|
db.responses.emplace_back(rules_hero_idle_chatter, [&fn_called](){ fn_called++; });
|
|
|
|
assert(fn_called == 0);
|
|
assert(db.match_and_run(q1, rng) == true);
|
|
assert(fn_called == 1);
|
|
assert(db.match_and_run(q2, rng) == false);
|
|
assert(fn_called == 1);
|
|
|
|
return 0;
|
|
}
|
|
|