refactor(bot): 콤보 로직을 선언적 룰 엔진으로 - #468
Draft
dev-yunseong wants to merge 2 commits into
Draft
Conversation
BotVisibleObject carries Status so a rule can ask whether something is attacking; the loop thread already holds the object when it copies the snapshot, so this costs nothing. RecipeOption, RuleOutcome and the BotRuleEngine interface fix the shape of the rule layer before either half of it exists. RuleOutcome stops short of being a decision on purpose: the per-candidate tier noise stays in BotBrain, where it is applied exactly once per candidate today. RecipeMatcher extracts the hand-can-make-it check that three decision sites had each open-coded.
findSeedSpiritCombo and findMobClusterCombo were two bespoke methods on
the brain that each rebuilt the same scaffolding: filter the hand, price
the recipe, find a target, score it. The tactic itself was buried in
that scaffolding, so a new one meant writing a third copy of it.
Both are now sentences in BotRuleBook:
rule(SEED_SPIRIT_RULE_ID)
.castingMagic(VineTossMagic.class, OvergrowthMagic.class)
.when(ally(PrefabType.SeedSpirit).inCastRange())
.at(matchedTarget())
.scoring(ONE_PER_MANA)
A condition returns the objects it matched rather than a boolean, which
is what lets the aim point be the thing the condition found - without it
'a seed spirit is nearby' cannot become 'cast at that seed spirit'.
The engine returns every candidate of the highest-priority matching rule
and picks no winner: the tier noise stays in BotBrain, applied exactly
once per candidate, as BotBrainComboTest asserts.
Two behaviour notes. Rules are gated at INTRO or above, which is how the
hospitality bot is now excluded from combos - it used to be an if on the
director being null. And the cluster tactic now sees every crowd rather
than only the best one, so a tie between equally sized crowds is broken
by the tier noise instead of by proximity.
Refs #466
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #466
무엇을
BotBrain에 하드코딩돼 있던 두 콤보 전술(findSeedSpiritCombo,findMobClusterCombo)을 선언형 룰로 뽑아냈다.두 메서드는 각자 같은 발판을 다시 세우고 있었다. 손패 필터링, 레시피 가격 계산, 대상 찾기, 점수 매기기. 정작 전술 자체는 그 발판에 파묻혀 있어서, 새 전술 하나를 추가하려면 같은 발판을 세 번째로 쓰는 수밖에 없었다.
이제 둘 다
BotRuleBook의 문장이다.설계의 핵심
조건은 boolean 이 아니라 매칭된 대상을 반환한다.
BotCondition.match가Optional<ConditionMatch>를 내고, 거기 담긴 객체나 클러스터 중심점이 그대로 조준점이 된다. 이게 없으면 "씨앗정령이 근처에 있다" 를 "그 씨앗정령에게 시전한다" 로 이을 수가 없다.엔진은 우선순위가 가장 높은, 매칭에 성공한 한 룰의 모든 후보를 돌려주고 승자는 고르지 않는다. 티어별 난수 섭동은
BotBrain에 남아 후보마다 정확히 한 번 적용된다 —BotBrainComboTest.appliesTierNoiseExactlyOncePerScoredCandidate가Random.nextDouble()호출 횟수를 못박고 있다. 엔진이 승자를 골랐다면 그 노이즈가 경계 반대편으로 넘어가거나 사라졌을 것이다.구조
룰이 하나도 안 맞으면 기존 스코어러가 예전과 똑같이 돈다.
관측 데이터 확장
RigidBody는 매 프레임 적분 직후velocity.clear()를 하므로 스냅샷의 속도는 사실상 항상 0이다. 이전 think 패스와 위치를 차분하는 것이 유일하게 유효한 측정이다 (BotMemory).BotVisibleObject에Status추가. 루프 스레드가 이미 객체를 쥐고 복사하므로 비용이 없다.ClusterFinder가 기존 2패스 중심점 알고리즘을 그대로 이식했다. 앵커 주변을 모으고, 그 중심점 주변으로 다시 고르는 두 번째 패스가 조준점을 무리의 중심으로 만든다.TagRepository.getGameObjectTags는 호출마다 DB 를 친다.PrefabType은 유한 enum 이라 시작 시 전량 워밍업한다 (GameObjectTagService).행동 변화
두 가지 있다.
if (hospitalityDirector == null)에서 각 룰의 티어 게이트로 옮겨갔다.HOSPITALITY가BotTier에서INTRO아래라 티어 게이트가 걸린 룰은 자동으로 접대 봇을 배제한다.테스트
./gradlew test— 462개 통과, 실패 0.BotBrainComboTest가 동등성 하네스다. 룰 아이디, 카드, 조준점, 이유 문자열까지 리팩터링 전과 동일하게 유지된다.ClusterFinderTest(6),BotMemoryTest(9),BotWorldViewFactoryTest(9),BotConditionTest(23),AimSpecTest(6),PriorityBotRuleEngineTest(14),ExistingTacticsInRuleDslTest(5).PriorityBotRuleEngineTest는 엔진이 난수를 전혀 소비하지 않음을 확인한다(50회 평가 결과 동일).게임플레이 영향
ruleId와reason은BotThoughtInfoDto로 클라이언트에 방송되므로 프로토콜의 일부다. 기존 문자열combo.seed-spirit,combo.mob-cluster를 그대로 유지했다.GameObjectTagService가 앱 시작 시 태그 테이블을 워밍업한다. 실패해도 게임은 뜨고, 봇이 태그 없이 점수를 매긴다(BotCounterEvaluator와 같은 방식).버전 0.9.0 -> 0.10.0 (MINOR, 런타임 동작 변경).