Spaces:
Sleeping
Sleeping
File size: 8,142 Bytes
7d46a91 1c9c13f 7d46a91 1c9c13f 7d46a91 6e20b9b 7d46a91 1c9c13f 7d46a91 1c9c13f 7d46a91 1c9c13f 7d46a91 1c9c13f 7d46a91 1c9c13f 7d46a91 6e20b9b 7d46a91 6e20b9b 7d46a91 6e20b9b 7d46a91 6e20b9b 7d46a91 6e20b9b 7d46a91 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | """νμ΄μ§ λ³ν© β μ¬λ¬ λΉνΈμΉ΄λλ₯Ό ν νλ©΄(νμ΄μ§)μΌλ‘ λ¬Άλ κ²°μ μ νν°μ
.
μμΉ:
- μμ¬ μ§νΒ·μ¨μ μνμ κΆμλ StoryEngineμ΄ μ μ§νλ€. Pagerλ engine.advance()λ₯Ό
νμ΄μ§ κ²½κ³κΉμ§ λ°λ³΅ νΈμΆν΄ μ§λ¬Έμ λ³ν©ν λΏ, λΆμν¨κ³Όλ₯Ό μ§μ λ§λ€μ§ μλλ€
(μΉ΄λλΉ μ νν 1ν β μ΄μ€/λλ½ μ μ© κΈμ§)
- νν°μ
μ μ½ν
μΈ (μ μ μΉ΄λ μμ)μ μμ ν¨μ β μ μ κ²½λ‘Β·μνμ 무κ΄ν΄
νμ΄μ§ μ΄μ(μ§νλ λΆλͺ¨)κ° μμ μ μ΄κ³ , 볡μ μμλ κ°μ νμ΄μ§κ° μ¬κ΅¬μ±λλ€
- λ³ν©μ μ ν λ§ν¬(card.next == μ μ λ€μ μΉ΄λ)λ‘ μ΄μ΄μ§ ꡬκ°(run)μμλ§. κ°μ λΆλ¦¬:
Β· μ νμ§/λΆκΈ°(next_options) μΉ΄λ λ€ (κ²°μ μ§μ )
Β· solo μΉ΄λ(μ°μΆ μ»·Β·μ±ν
μ΅μ»€β νΈμΆμκ° μ§μ ) μλ€
Β· μνΌμλ κ²½κ³
- λ°λ κ· ν: runλ³ μ΄ κΈμμλ‘ νμ΄μ§ μ k = round(μ΄/target)λ₯Ό λ¨Όμ μ νκ³ ,
λμ κΈμμκ° κ· λ± κ²½κ³(μ΄Β·i/k)μ κ°μ₯ κ°κΉμ΄ μΉ΄λ λ€μμ μ λ¨ β 그리λ κ³Όμ λ°©μ§
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Optional
from engine.repositories.content import ContentRepository
from engine.schemas.beatcard import BeatCard, Choice, NarrationBlock
from engine.schemas.chapter import EndingRule
from engine.services.story import StepResult, StoryEngine
@dataclass
class PageView:
"""ν νμ΄μ§μ λ λ μΆλ ₯ β λΌμ°ν°κ° κ·Έλλ‘ JSONμΌλ‘ λ§λ λ€."""
first: Optional[BeatCard] = None # νμ΄μ§ μ λͺ©Β·μνΌμλΒ·μ°μΆ μ»·μ κΈ°μ€
last: Optional[BeatCard] = None # μ νμ§Β·μ±ν
Β·μ§ν μμΉ(current_id)μ κΈ°μ€
narration: list[NarrationBlock] = field(default_factory=list)
narration_cards: list[str] = field(default_factory=list) # λΈλ‘λ³ μμ μΉ΄λ (λ³λ ¬)
choices: list[Choice] = field(default_factory=list)
can_chat: bool = False
ending: Optional[EndingRule] = None
class Pager:
def __init__(self, repo: ContentRepository, solo_ids: set[str],
cut_ids: set[str], target_chars: int | None = None):
"""solo_ids: λ¨λ
νμ΄μ§ κ°μ μΉ΄λ(μ°μΆ μ»·+μ±ν
μ΅μ»€) Β· cut_ids: μ°μΆ μ»·(νμ 2νμ΄μ§)."""
self.repo = repo
self.solo = set(solo_ids)
self.cuts = set(cut_ids)
self.target = target_chars or repo.chapter.page_target_chars
self._build()
# ββ μ μ νν°μ
βββββββββββββββββββββββββββββββββββββββββ
def _build(self) -> None:
# 1) ꡬ쑰μ λΈλ μ΄ν¬λ‘ run(λ³ν© κ°λ₯ν μ΅λ ꡬκ°)μ λ§λ€κ³
runs: list[list[BeatCard]] = []
cur: list[BeatCard] = []
prev: Optional[BeatCard] = None
for card in self.repo.all_cards():
if cur and self._structural_break(prev, card):
runs.append(cur)
cur = []
cur.append(card)
prev = card
if cur:
runs.append(cur)
# 2) runλ³λ‘ κ· ν λΆν
parts: list[list[str]] = []
for run in runs:
parts += [[c.id for c in part] for part in self._split_run(run)]
self.partitions = parts
self.part_of = {cid: i for i, part in enumerate(parts) for cid in part}
# νμ λ²νΈ: νν°μ
= 1νμ΄μ§, μ°μΆ μ»· νν°μ
= 2νμ΄μ§(μ»·+λ³Έλ¬Έ)
self._cuts_before = []
seen = 0
for part in parts:
self._cuts_before.append(seen)
if part[0] in self.cuts:
seen += 1
self._total = len(parts) + seen
def _structural_break(self, prev: BeatCard, card: BeatCard) -> bool:
if prev.choices or prev.next_options:
return True # κ²°μ μ§μ β νμ΄μ§λ μ νμ§λ‘ λ«νλ€
if prev.next != card.id:
return True # λΉμ ν(λΆκΈ° λ³νΒ·μ νΒ·μνΌμλ μ°Έμ‘°) β κ²½λ‘ μμ μ μν΄ λΆλ¦¬
if prev.id in self.solo or card.id in self.solo:
return True
return prev.episode_id != card.episode_id
@staticmethod
def _chars(card: BeatCard) -> int:
return sum(len(b.text) for b in card.narration)
def _split_run(self, run: list[BeatCard]) -> list[list[BeatCard]]:
"""runμ k = round(μ΄ κΈμμ/target)κ°μ μ°μ ꡬκ°μΌλ‘ κ· λ± λΆν (κ²°μ μ )."""
total = sum(self._chars(c) for c in run)
k = max(1, min(len(run), round(total / self.target)))
if k == 1:
return [run]
bounds = [total * i / k for i in range(1, k)] # κ· λ± μ λ¨ λͺ©νμ
parts: list[list[BeatCard]] = []
cur: list[BeatCard] = []
acc = 0
bi = 0
for idx, card in enumerate(run):
cur.append(card)
acc += self._chars(card)
remaining_cards = len(run) - idx - 1
remaining_cuts = (k - 1) - bi
if bi >= len(bounds) or remaining_cards == 0:
continue
next_acc = acc + self._chars(run[idx + 1])
# μ§κΈ μλ₯΄λ κ² λ€μ μΉ΄λκΉμ§ λκ³ κ°λ κ²λ³΄λ€ κ²½κ³μ κ°κΉκ±°λ,
# λ¨μ μ λ¨ μλ§νΌμ μΉ΄λλ°μ μ λ¨μμΌλ©΄ μ¬κΈ°μ μ λ¨
if (abs(acc - bounds[bi]) <= abs(next_acc - bounds[bi])
or remaining_cards == remaining_cuts):
parts.append(cur)
cur = []
bi += 1
if cur:
parts.append(cur)
return parts
# ββ νμ λ²νΈ (μ§νλ) ββββββββββββββββββββββββββββββββββ
@property
def display_total(self) -> int:
return self._total
def display_index(self, card_id: str) -> int:
"""μΉ΄λκ° μν νμ΄μ§μ λ³Έλ¬Έ νμ λ²νΈ. μ»· νμ΄μ§ λ²νΈλ μ΄ κ° - 1 (κΈ°μ‘΄ κ³μ½ μ μ§)."""
p = self.part_of[card_id]
own_cut = 1 if self.partitions[p][0] in self.cuts else 0
return p + self._cuts_before[p] + own_cut + 1
# ββ λ°νμ: νμ΄μ§ μ±μ°κΈ° (λΆμν¨κ³Όλ engine.advanceκ° μΉ΄λλΉ 1ν) ββ
def complete(self, engine: StoryEngine, step: StepResult) -> PageView:
"""advance/choose/start μ§νμ step(νμ΄μ§ 첫 μΉ΄λ)μ νν°μ
λκΉμ§ μ±μ΄λ€."""
if step.ending:
return PageView(ending=step.ending)
first = step.card
part = self.partitions[self.part_of[first.id]]
narration = list(step.narration)
cards = [first.id] * len(step.narration)
while engine.current_id != part[-1]:
step = engine.advance()
if step.ending: # νν°μ
λ΄ λ§ν¬λ μ±ν° μ β λ°©μ΄μ μ²λ¦¬
return PageView(ending=step.ending)
narration += step.narration
cards += [step.card.id] * len(step.narration)
last = step.card
return PageView(first=first, last=last, narration=narration,
narration_cards=cards,
choices=list(last.choices), can_chat=last.can_chat)
# ββ λ°νμ: 볡μ λ λ (λΆμν¨κ³Ό μμ) βββββββββββββββββββ
def render_current(self, engine: StoryEngine) -> PageView:
"""νμ¬ μΉ΄λ(νμ νν°μ
λ)κ° μν νμ΄μ§ μ 체λ₯Ό μ¬κ΅¬μ± β μ΄μ΄ μ½κΈ° νλ©΄."""
if engine.ending:
return PageView(ending=engine.ending)
part = self.partitions[self.part_of[engine.current_id]]
cards = [self.repo.get_card(cid) for cid in part]
narration, ncards = [], []
for c in cards:
vis = engine.visible_narration(c)
narration += vis
ncards += [c.id] * len(vis)
return PageView(first=cards[0], last=cards[-1], narration=narration,
narration_cards=ncards,
choices=list(cards[-1].choices), can_chat=cards[-1].can_chat)
|