载入中...
搜索中...
未找到
CardControl.cpp
浏览该文件的文档.
1#include "card/CardControl.h"
2
3#include "card/Card.h"
4#include "card/CardTypes.h"
5#include "common/Capability.h"
6
7#include <algorithm>
8#include <limits>
9#include <string>
10#include <utility>
11
12namespace eve::card {
13namespace {
14
15template <typename T>
16Result<T> failure(DiagnosticCode code, std::string message, std::string path) {
18}
19
20LogicalId id(std::string_view text) { return LogicalId::parse(text).value(); }
21
22Result<const Value::Object*> object(const Value& value) {
23 const auto* result = value.getIf<Value::Object>();
24 if (!result)
25 return failure<const Value::Object*>(DiagnosticCode::InvalidArgument, "card parameters must be an object",
26 "parameters");
28}
29
30Result<std::string> text(const Value::Object& value, std::string_view name, std::string fallback = {}) {
31 const auto found = value.find(std::string(name));
32 if (found == value.end()) return Result<std::string>::success(std::move(fallback));
33 if (!found->second.isString())
34 return failure<std::string>(DiagnosticCode::InvalidArgument, "card parameter must be a string",
35 "parameters." + std::string(name));
36 return Result<std::string>::success(found->second.asString());
37}
38
39Result<double> number(const Value::Object& value, std::string_view name, double fallback) {
40 const auto found = value.find(std::string(name));
41 if (found == value.end()) return Result<double>::success(fallback);
42 if (!found->second.isNumeric())
43 return failure<double>(DiagnosticCode::InvalidArgument, "card parameter must be numeric",
44 "parameters." + std::string(name));
45 return Result<double>::success(found->second.isInt64() ? static_cast<double>(found->second.asInt())
46 : found->second.asDouble());
47}
48
49const std::string kDraw{"card:draw"};
50const std::string kPlay{"card:play"};
51const std::string kSetAttribute{"card:set-attribute"};
52
61class UnboundPaymentAccount final : public eve::resource::IResourceAccount {
62public:
63 [[nodiscard]] eve::Result<eve::resource::Affordability> canAfford(const eve::resource::CostSpec&) const override {
65 }
66 [[nodiscard]] eve::Result<eve::resource::Reservation> reserve(const eve::resource::CostSpec&) override {
68 }
69 [[nodiscard]] eve::Result<eve::resource::Receipt> debit(const eve::resource::CostSpec&) override {
71 }
72 [[nodiscard]] eve::Result<eve::resource::Receipt> credit(const eve::resource::CostSpec&) override {
74 }
75 [[nodiscard]] eve::Result<eve::resource::Receipt> commit(const eve::resource::Reservation&) override {
77 }
78 [[nodiscard]] eve::Result<void> rollback(const eve::resource::Reservation&) override {
79 return eve::Result<void>::failure(diagnostic());
80 }
81
82private:
83 static eve::Diagnostic diagnostic() {
85 "this card instance has no payment account bound", "card.account");
86 }
87};
88
89eve::resource::IResourceAccount& unboundAccount() {
90 static UnboundPaymentAccount account;
91 return account;
92}
93
94} // namespace
95
96CardControl::CardControl(Card& module) : module_(&module) {
97 cap::addListener<IGameplayControlProvider>(this);
98 cap::addListener<IGameplayInstanceCatalog>(this);
99}
100
102 cap::removeListener<IGameplayInstanceCatalog>(this);
103 cap::removeListener<IGameplayControlProvider>(this);
104}
105
108 if (!instance.isValid() || !owner.isValid())
110 DiagnosticCode::InvalidArgument, "card publication needs valid instance and owner ids", "instance"));
111 if (find(instance) != nullptr)
113 DiagnosticCode::Conflict, "a gameplay control already owns that card instance", "instance"));
114 Entry entry;
115 entry.instance = instance;
116 entry.owner = owner;
117 entry.hand = &hand;
118 entry.account = account;
119 entries_.push_back(std::move(entry));
120 return Result<void>::success();
121}
122
124 const auto found = std::find_if(entries_.begin(), entries_.end(),
125 [instance](const Entry& entry) { return entry.instance == instance; });
126 if (found == entries_.end())
128 Diagnostic::error(DiagnosticCode::NotFound, "that card instance is not published", "instance"));
129 entries_.erase(found);
130 return Result<void>::success();
131}
132
133void CardControl::clear() { entries_.clear(); }
134
135int CardControl::count() const { return static_cast<int>(entries_.size()); }
136
137std::vector<SubjectRef> CardControl::instances() const {
138 std::vector<SubjectRef> result;
139 result.reserve(entries_.size());
140 for (const auto& entry : entries_) result.push_back(entry.instance);
141 return result;
142}
143
144std::string_view CardControl::gameplayDomain() const noexcept { return "card"; }
145
146std::vector<SubjectRef> CardControl::gameplayInstances() const { return instances(); }
147
148CardControl::Entry* CardControl::find(SubjectRef instance) {
149 const auto found = std::find_if(entries_.begin(), entries_.end(),
150 [instance](const Entry& entry) { return entry.instance == instance; });
151 return found == entries_.end() ? nullptr : &*found;
152}
153
154const CardControl::Entry* CardControl::find(SubjectRef instance) const {
155 const auto found = std::find_if(entries_.begin(), entries_.end(),
156 [instance](const Entry& entry) { return entry.instance == instance; });
157 return found == entries_.end() ? nullptr : &*found;
158}
159
160bool CardControl::controls(const GameplaySession& session, const Entry& entry) const {
161 return session.access != GameplayAccess::PlayerEquivalent ||
162 std::find(session.controlledSubjects.begin(), session.controlledSubjects.end(), entry.owner) !=
163 session.controlledSubjects.end();
164}
165
166Value CardControl::handState(const Entry& entry) const {
167 Value::Array hand;
168 if (entry.hand != nullptr) {
169 const int count = entry.hand->count();
170 for (int index = 0; index < count; ++index) {
171 CardData* card = entry.hand->get(index);
172 if (card == nullptr) continue;
173 hand.emplace_back(Value(Value::Object{
174 {"card", Value(card->identity()->id)},
175 {"definition", Value(card->identity()->definitionId)},
176 {"name", Value(card->identity()->name)},
177 {"kind", Value(card->identity()->kind)},
178 {"cost", Value(static_cast<std::int64_t>(card->stats()->cost))},
179 {"attack", Value(static_cast<std::int64_t>(card->stats()->attack))},
180 {"health", Value(static_cast<std::int64_t>(card->stats()->health))},
181 {"state",
182 Value(cardStateName(card->state()->phase) != nullptr ? std::string(cardStateName(card->state()->phase))
183 : std::string{})},
184 {"disabled", Value(card->visual()->disabled)},
185 }));
186 }
187 }
188 std::int64_t deckSize = 0;
189 Deck* deck = module_ != nullptr ? module_->getDeck() : nullptr;
190 if (deck != nullptr) deckSize = static_cast<std::int64_t>(deck->count());
191 return Value(Value::Object{
192 {"owner", Value(entry.hand != nullptr ? entry.hand->meta()->owner : std::string{})},
193 {"hand", Value(std::move(hand))},
194 {"handSize", Value(static_cast<std::int64_t>(entry.hand != nullptr ? entry.hand->count() : 0))},
195 {"deck", Value(deckSize)},
196 // `unbound` means this publication carries no payment account, so
197 // `card:play` is deliberately unavailable rather than silently absent.
198 {"payment", Value(entry.account != nullptr ? std::string("bound") : std::string("unbound"))},
199 });
200}
201
203 const Entry* entry = find(instance);
204 if (entry == nullptr)
205 return failure<GameplayObservation>(DiagnosticCode::NotFound, "card gameplay instance was not found",
206 "instance");
207 if (!controls(session, *entry))
208 return failure<GameplayObservation>(DiagnosticCode::PreconditionViolation,
209 "session does not control the card owner", "instance");
210 GameplayObservation observation;
211 observation.domain = id("gameplay:card");
212 observation.instance = entry->instance;
213 observation.tick = entry->tick;
214 observation.revision = entry->revision;
215 observation.state = handState(*entry);
216 return Result<GameplayObservation>::success(std::move(observation));
217}
218
220 SubjectRef instance,
221 SubjectRef subject) const {
222 auto observed = observeGameplay(session, instance);
223 if (!observed) return Result<std::vector<GameplayActionDescriptor>>::failure(observed.status());
224 const Entry* entry = find(instance);
225 if (entry == nullptr)
226 return failure<std::vector<GameplayActionDescriptor>>(DiagnosticCode::NotFound,
227 "card gameplay instance was not found", "instance");
228 if (subject != entry->owner)
229 return failure<std::vector<GameplayActionDescriptor>>(DiagnosticCode::PreconditionViolation,
230 "card action subject must be its owner", "subject");
231
232 const Value stringType(Value::Object{{"type", Value("string")}});
233 const Value numberType(Value::Object{{"type", Value("number")}});
234 std::vector<GameplayActionDescriptor> actions{
235 {id(kDraw), Value(Value::Object{})},
236 // Cost-free cards play through the unbound payment boundary; a charging
237 // card fails there with an explicit "no payment account bound" diagnostic,
238 // so the gap is reported instead of the action vanishing from discovery.
239 {id(kPlay), Value(Value::Object{{"card", stringType}})},
240 };
242 actions.push_back(
243 {id(kSetAttribute),
244 Value(Value::Object{{"card", stringType}, {"attribute", stringType}, {"value", numberType}})});
245 }
246 return Result<std::vector<GameplayActionDescriptor>>::success(std::move(actions));
247}
248
250 const GameplayCommand& command) {
251 Entry* entry = find(instance);
252 if (entry == nullptr)
253 return failure<GameplayCommandReceipt>(DiagnosticCode::NotFound, "card gameplay instance was not found",
254 "instance");
255 if (!controls(session, *entry) || command.subject != entry->owner)
256 return failure<GameplayCommandReceipt>(DiagnosticCode::PreconditionViolation,
257 "session does not control the card owner", "command.subject");
258 if (command.id.empty())
259 return failure<GameplayCommandReceipt>(DiagnosticCode::InvalidArgument, "command id must not be empty",
260 "command.id");
261 if (command.observedTick != entry->tick || command.expectedRevision != entry->revision)
262 return failure<GameplayCommandReceipt>(
263 DiagnosticCode::Conflict, "card command was based on a stale observation", "command.expectedRevision");
264 auto values = object(command.parameters);
266 const Value::Object& parameters = *values.value();
267 if (entry->hand == nullptr || module_ == nullptr)
268 return failure<GameplayCommandReceipt>(DiagnosticCode::Failed, "card instance lost its hand", "instance");
269
270 const std::string action = command.action.format();
271 std::string detail;
272 std::string eventType;
273 std::int64_t applied = 0;
274 if (action == kDraw) {
275 CardData* drawn = module_->drawCard(entry->hand->meta()->owner);
276 if (drawn == nullptr)
277 return failure<GameplayCommandReceipt>(DiagnosticCode::PreconditionViolation,
278 "the deck has no card to draw", "command.action");
279 detail = "drew " + drawn->identity()->definitionId;
280 eventType = "card.drawn";
281 applied = 1;
282 } else if (action == kPlay) {
283 auto cardId = text(parameters, "card");
284 if (!cardId) return Result<GameplayCommandReceipt>::failure(cardId.status());
285 CardData* card = cardId.value().empty() ? nullptr : entry->hand->find(cardId.value());
286 if (card == nullptr)
287 return failure<GameplayCommandReceipt>(DiagnosticCode::NotFound, "that card is not in the hand",
288 "command.parameters.card");
289 eve::resource::IResourceAccount& account = entry->account != nullptr ? *entry->account : unboundAccount();
290 const auto played = module_->play(*card, account, {}, command.id);
291 if (!played) return Result<GameplayCommandReceipt>::failure(played.status());
292 detail = "played " + card->identity()->definitionId;
293 eventType = "card.played";
294 applied = 1;
295 } else if (action == kSetAttribute) {
297 return failure<GameplayCommandReceipt>(DiagnosticCode::PreconditionViolation,
298 "rewriting card attributes requires the test-driver or "
299 "developer-cheat profile",
300 "session.access");
301 auto cardId = text(parameters, "card");
302 auto attribute = text(parameters, "attribute");
303 auto value = number(parameters, "value", 0.0);
304 if (!cardId) return Result<GameplayCommandReceipt>::failure(cardId.status());
305 if (!attribute) return Result<GameplayCommandReceipt>::failure(attribute.status());
307 if (cardId.value().empty() || attribute.value().empty())
308 return failure<GameplayCommandReceipt>(DiagnosticCode::InvalidArgument,
309 "set-attribute needs a card and an attribute", "command.parameters");
310 CardData* card = entry->hand->find(cardId.value());
311 if (card == nullptr)
312 return failure<GameplayCommandReceipt>(DiagnosticCode::NotFound, "that card is not in the hand",
313 "command.parameters.card");
314 const auto appliedAttribute = module_->setCardAttribute(*card, attribute.value(), value.value());
315 if (!appliedAttribute) return Result<GameplayCommandReceipt>::failure(appliedAttribute.status());
316 detail = "set " + attribute.value() + " of " + card->identity()->definitionId;
317 eventType = "card.attribute-changed";
318 applied = 1;
319 } else {
320 return failure<GameplayCommandReceipt>(DiagnosticCode::Unsupported, "unsupported card action",
321 "command.action");
322 }
323
324 ++entry->revision;
325 GameplayEvent event;
326 event.sequence = entry->nextEventSequence++;
327 event.tick = entry->tick;
328 event.type = eventType;
329 event.subject = entry->owner;
330 event.causationCommandId = command.id;
331 event.correlationId = command.id;
332 event.payload = Value(Value::Object{
333 {"instance", Value(entry->instance.format())}, {"detail", Value(detail)}, {"quantity", Value(applied)}});
334 entry->events.push_back(std::move(event));
335
337 receipt.commandId = command.id;
338 receipt.executionId = command.id + ".executed";
339 receipt.acceptedTick = entry->tick;
340 receipt.resultingRevision = entry->revision;
341 receipt.details =
342 Value(Value::Object{{"action", Value(action)}, {"detail", Value(detail)}, {"quantity", Value(applied)}});
344}
345
347 const SimulationStep& step) {
348 Entry* entry = find(instance);
349 if (entry == nullptr)
350 return failure<GameplayObservation>(DiagnosticCode::NotFound, "card gameplay instance was not found",
351 "instance");
352 if (step.tick <= entry->tick)
353 return failure<GameplayObservation>(DiagnosticCode::Conflict, "card simulation tick must increase",
354 "step.tick");
355 auto observed = observeGameplay(session, instance);
356 if (!observed) return Result<GameplayObservation>::failure(observed.status());
357 entry->tick = step.tick;
358 return observeGameplay(session, instance);
359}
360
362 std::uint64_t afterSequence) const {
363 auto observed = observeGameplay(session, instance);
364 if (!observed) return Result<std::vector<GameplayEvent>>::failure(observed.status());
365 const Entry* entry = find(instance);
366 if (entry == nullptr)
367 return failure<std::vector<GameplayEvent>>(DiagnosticCode::NotFound, "card gameplay instance was not found",
368 "instance");
369 std::vector<GameplayEvent> result;
370 for (const auto& event : entry->events)
371 if (event.sequence > afterSequence) result.push_back(event);
372 const bool empty = result.empty();
373 return Result<std::vector<GameplayEvent>>::success(std::move(result),
375}
376
377} // namespace eve::card
double value
int subject
Definition AnimSmr.cpp:163
eve::resource::IResourceAccount * account
std::map< std::string, Var > values
std::string message
DiagnosticCode code
std::int32_t second
std::string text
std::string name
std::string action
Definition PlayHost.cpp:117
std::string path
Definition PlayHost.cpp:110
std::vector< ActionSpec > actions
Definition PlayHost.cpp:126
std::string id
Definition PlayHost.cpp:108
int detail
double number
bool found
std::string string
std::optional< InteractionSession > session
Definition Tactics.cpp:39
Json object
float step
Definition TreeMesh.cpp:314
uint32_t index
A structured explanation of a failed, degraded, or noteworthy result.
Definition Diagnostic.h:94
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
const std::string & format() const noexcept
Returns the canonical namespace:name representation.
Definition Identity.h:436
static std::optional< LogicalId > parse(std::string_view text)
Parses a scoped logical name.
Definition Identity.cpp:36
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
Strong, domain-neutral reference to a runtime subject.
Definition SubjectRef.h:26
bool isValid() const noexcept
Return whether this reference is valid and non-nil.
Definition SubjectRef.h:38
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
std::map< std::string, Value > Object
Definition Value.h:34
std::vector< Value > Array
Definition Value.h:33
CardControl(Card &module)
Create an unpublished adapter.
Result< void > publish(SubjectRef instance, SubjectRef owner, Hand &hand, eve::resource::IResourceAccount *account)
Publish one borrowed hand under a stable instance identity.
void clear()
Drop every publication.
Result< std::vector< GameplayActionDescriptor > > availableGameplayActions(const GameplaySession &session, SubjectRef instance, SubjectRef subject) const override
Discover currently legal player-semantic actions for one subject.
int count() const
Number of published instances.
Result< GameplayObservation > advanceGameplay(const GameplaySession &session, SubjectRef instance, const SimulationStep &step) override
Advance one provider-owned instance using injected deterministic time.
~CardControl() override
Unpublish every hand without touching the borrowed authorities.
Result< GameplayObservation > observeGameplay(const GameplaySession &session, SubjectRef instance) const override
Observe one provider-owned gameplay instance without mutation.
Result< std::vector< GameplayEvent > > gameplayEvents(const GameplaySession &session, SubjectRef instance, std::uint64_t afterSequence) const override
Return events strictly newer than an instance-local sequence cursor.
std::vector< SubjectRef > gameplayInstances() const override
Instances currently served, in publication order.
std::vector< SubjectRef > instances() const
Published instance identities, in publication order.
Result< void > unpublish(SubjectRef instance)
Drop one publication; NotFound when that instance is not published.
std::string_view gameplayDomain() const noexcept override
Stable provider domain used for discovery and routing.
Result< GameplayCommandReceipt > submitGameplay(const GameplaySession &session, SubjectRef instance, const GameplayCommand &command) override
Validate authority and atomically submit one domain command.
单张卡牌:ECS 实体,数据拆成 Identity / Stats / Visual / Layout / State。
Definition CardTypes.h:94
卡牌模块入口(eve.Card):定义注册、对象工厂与每帧 update/render。
Definition Card.h:91
CardData * drawCard(const std::string &handOwner)
从牌库抽一张牌加入指定手牌;无牌时返回 nullptr。
Definition Card.cpp:504
eve::Result< void > setCardAttribute(CardData &card, std::string_view attribute, double value) const
Set one canonical combat attribute through the Card facade.
Definition Card.cpp:300
eve::Result< eve::transaction::TransactionReceipt > play(CardData &card, eve::resource::IResourceAccount &playerAccount, CardPlayComposition composition={}, std::string transactionId={})
Play a card through the facade's condition, container and payment transaction.
Definition Card.cpp:334
Deck * getDeck() const
Returns the deck.
Definition Card.cpp:502
手牌:扇形布局 + 悬浮 + 拖拽 + 落区判定 + 渲染。
Definition CardTypes.h:273
Immutable canonical multi-resource cost.
Provider-neutral resource account contract.
const char * cardStateName(CardState state)
状态枚举的字符串名(用于脚本/调试)。
Definition CardTypes.cpp:12
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
Definition Database.h:26
WidgetDesc card(std::vector< WidgetDesc > children, std::string id)
Bordered surface container with editor-friendly padding.
Definition Widget.cpp:502
DiagnosticCode
Stable machine-readable diagnostic codes.
Definition Diagnostic.h:47
Owning evidence that a gameplay authority accepted one command.
One player-semantic command submitted to a domain authority.
std::uint64_t expectedRevision
SimulationTick observedTick
Owning event projection safe across frames and process boundaries.
std::uint64_t sequence
Immutable owning observation of one authoritative gameplay instance.
Owning authorization context shared by player, script and automation adapters.
One deterministic fixed-step emitted by SimulationClock.
Definition Time.h:158
Reservation credential returned by a successful reserve operation.
std::string id
Definition Widget.h:17