l4d2dialogtests/src/main1.cpp

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;
}