1212from __future__ import annotations
1313
1414import typing
15- from collections .abc import Callable , Mapping , Sequence
15+ from collections import Counter
16+ from collections .abc import Callable , Collection , Mapping , Sequence
1617from typing import Any , NamedTuple , Protocol
1718
1819from mhfu_port import behaviour , sequence
19- from mhfu_port .behaviour import KINDS , Block , MoveNode , Param , Path
20+ from mhfu_port .behaviour import KINDS , Block , MoveNode , Param , Path , Trigger
2021from mhfu_port .manifest import Manifest , ManifestError
2122
2223from mhfu_studio .monster .document import PortDocument
2930"""A move node's ports: the input, then the two outputs."""
3031ALL = "all"
3132"""The scope that is the whole canvas, as stored; any other is a trigger's key."""
33+ INSERTABLE = ("condition" , "modifier" , "effect" )
34+ """The roles of the blocks a rule holds besides its first."""
3235ROLE_TITLES = {
3336 "event" : "Events" ,
3437 "state" : "State" ,
@@ -204,9 +207,10 @@ def options(m: Manifest, kind: str) -> dict[str, list[tuple[str, object]]]:
204207 return out
205208
206209
207- def palette () -> list [tuple [str , list [tuple [str , str , str ]]]]:
208- """The kinds a block can be, a group per role: (kind, title, tip). All from `KINDS`."""
209- roles = typing .get_args (behaviour .Role )
210+ def palette (only : Collection [str ] | None = None ) -> list [tuple [str , list [tuple [str , str , str ]]]]:
211+ """The kinds a block can be, a group per role (only those in `only`, if given): (kind,
212+ title, tip). All from `KINDS`."""
213+ roles = [r for r in typing .get_args (behaviour .Role ) if only is None or r in only ]
210214 groups = [
211215 (
212216 ROLE_TITLES .get (role , role .capitalize () + "s" ),
@@ -217,6 +221,11 @@ def palette() -> list[tuple[str, list[tuple[str, str, str]]]]:
217221 return [g for g in groups if g [1 ]]
218222
219223
224+ def insertable () -> list [tuple [str , list [tuple [str , str , str ]]]]:
225+ """The palette of what goes inside a rule: not the event or state that starts it."""
226+ return palette (INSERTABLE )
227+
228+
220229def start (p : Param ) -> object :
221230 """A new block's value for `p`: the default, else the least, else the first choice; none
222231 for an optional param."""
@@ -465,3 +474,208 @@ def scale(m: Manifest) -> None:
465474
466475 doc .edit (scale )
467476 return f"{ len (b .blocks ) + len (b .moves )} nodes spread out"
477+
478+
479+ # rules: what a trigger's rows edit
480+
481+ #: the gap that puts a new rule's first block below every other block
482+ FRESH = float (behaviour .ROW )
483+
484+
485+ def open_rules (m : Manifest ) -> list [Path ]:
486+ """The chains from a source that stop at a block that plays nothing and feeds nothing: rules
487+ that have no move yet, which the brain does not check. First block's place first."""
488+ b = m .behaviour
489+ done = {p .blocks for p in behaviour .paths (m )}
490+ out : list [Path ] = []
491+
492+ def walk (trail : tuple [str , ...], during : str | None ) -> None :
493+ blk = b .blocks [trail [- 1 ]]
494+ if not blk .play and not blk .next and trail not in done :
495+ out .append (Path (trail , "" , during ))
496+ for n in blk .next :
497+ if n not in trail :
498+ walk ((* trail , n ), during )
499+
500+ for i , during in behaviour .sources (m ):
501+ walk ((i ,), during )
502+ return sorted (out , key = lambda p : (b .blocks [p .blocks [0 ]].at [1 ], b .blocks [p .blocks [0 ]].at [0 ]))
503+
504+
505+ def _values (kind : str , by : str = "" ) -> dict [str , object ]:
506+ """A new block's params: valid ones, the trigger's `by` value among them."""
507+ params = {p .name : v for p in KINDS [kind ].params if (v := start (p )) is not None }
508+ if by and KINDS [kind ].by :
509+ params [KINDS [kind ].by ] = by
510+ return params
511+
512+
513+ def add_rule (doc : Edits , t : Trigger ) -> str :
514+ """A rule for trigger `t`: its first block (the event or state kind with the trigger's
515+ value; for a move's while-playing port a Played for block fed by it), below every other
516+ block, so it is checked last. It plays no move yet."""
517+ m = doc .manifest
518+ b = m .behaviour
519+ if t .section == "move" :
520+ if t .name not in b .moves :
521+ raise ManifestError (f"no move { t .name !r} " )
522+ kind = "played_for"
523+ else :
524+ kind = t .kind
525+ if kind not in KINDS :
526+ raise ManifestError (f"{ kind !r} is not a kind of block" )
527+ params = _values (kind , t .name if t .section == "event" else "" )
528+ i = behaviour .new_id (b )
529+ y = max ((blk .at [1 ] for blk in b .blocks .values ()), default = - FRESH ) + FRESH
530+ at = _at ((float (behaviour .LEFT ), y ))
531+
532+ def add (m : Manifest ) -> None :
533+ m .behaviour .blocks [i ] = Block (kind , at , params )
534+ if t .section == "move" :
535+ m .behaviour .moves [t .name ].during .append (i )
536+
537+ doc .edit (add )
538+ return f"{ KINDS [kind ].title } : a rule added, checked last"
539+
540+
541+ def insert_block (doc : Edits , after : str , kind : str , to : str | None , play : str | None ) -> str :
542+ """A block of `kind` in a rule after block `after`. `to` is the block the rule goes on to
543+ from `after`, and `play` the move it ends in when it ends there: the new block takes that
544+ place, so `after` feeds it and it feeds `to` or plays `play`."""
545+ m = doc .manifest
546+ here = _block (m , after )
547+ if kind not in KINDS or KINDS [kind ].role not in INSERTABLE :
548+ raise ManifestError (f"{ kind !r} is not a block a rule holds" )
549+ if to is not None and to not in here .next :
550+ raise ManifestError (f"{ _say (m , after )} does not feed { _say (m , to )} " )
551+ if play is not None and play not in here .play :
552+ raise ManifestError (f"{ _say (m , after )} does not play { play } " )
553+ i = behaviour .new_id (m .behaviour )
554+ x = (here .at [0 ] + _block (m , to ).at [0 ]) / 2 if to is not None else here .at [0 ] + behaviour .COLUMN
555+ at , params = _at ((x , here .at [1 ])), _values (kind )
556+
557+ def add (m : Manifest ) -> None :
558+ blocks = m .behaviour .blocks
559+ a , new = blocks [after ], Block (kind , at , params )
560+ if to is not None :
561+ new .next = [to ]
562+ a .next = [i if n == to else n for n in a .next ]
563+ else :
564+ if play is not None :
565+ new .play = [play ]
566+ a .play = [p for p in a .play if p != play ]
567+ a .next = [* a .next , i ]
568+ blocks [i ] = new
569+
570+ doc .edit (add )
571+ return f"{ KINDS [kind ].title } inserted after { _say (m , after )} "
572+
573+
574+ def drop_block (doc : Edits , block : str ) -> str :
575+ """A block out of every rule that holds it, the chain closing up: what fed it feeds what it
576+ fed, and moves it played are played by what fed it. Not the first block of a rule."""
577+ m = doc .manifest
578+ _block (m , block )
579+ if not any (block in blk .next for blk in m .behaviour .blocks .values ()):
580+ raise ManifestError ("the first block of a rule goes with the rule" )
581+
582+ def close (m : Manifest ) -> None :
583+ blocks = m .behaviour .blocks
584+ gone = blocks .pop (block )
585+ for blk in blocks .values ():
586+ if block in blk .next :
587+ blk .next = list (
588+ dict .fromkeys (n for x in blk .next for n in (gone .next if x == block else [x ]))
589+ )
590+ blk .play = list (dict .fromkeys ([* blk .play , * gone .play ]))
591+
592+ doc .edit (close )
593+ return f"{ _say (m , block )} deleted"
594+
595+
596+ def drop_rule (doc : Edits , blocks : Sequence [str ], play : str | None ) -> str :
597+ """A rule out: its move, then each block from its end back that has nothing left to feed or
598+ play. Blocks other rules still run through stay."""
599+ m = doc .manifest
600+ for i in blocks :
601+ _block (m , i )
602+
603+ def drop (m : Manifest ) -> None :
604+ b = m .behaviour
605+ if play is not None :
606+ b .blocks [blocks [- 1 ]].play = [p for p in b .blocks [blocks [- 1 ]].play if p != play ]
607+ for i in reversed (blocks ):
608+ if i not in b .blocks or b .blocks [i ].next or b .blocks [i ].play :
609+ break
610+ del b .blocks [i ]
611+ for other in b .blocks .values ():
612+ other .next = [n for n in other .next if n != i ]
613+ for node in b .moves .values ():
614+ node .during = [n for n in node .during if n != i ]
615+
616+ doc .edit (drop )
617+ return "rule deleted"
618+
619+
620+ def set_play (doc : Edits , block : str , old : str | None , new : str | None ) -> str :
621+ """The move `block` plays: `old` becomes `new`; no `old` adds one, no `new` takes it away."""
622+ m = doc .manifest
623+ here = _block (m , block )
624+ if new is not None :
625+ _move (m , new )
626+ if old is not None and old not in here .play :
627+ raise ManifestError (f"{ _say (m , block )} no longer plays { old } " )
628+ if old == new :
629+ return f"{ _say (m , block )} : unchanged"
630+
631+ def change (m : Manifest ) -> None :
632+ plays = m .behaviour .blocks [block ].play
633+ if old is None and new is not None :
634+ plays .append (new )
635+ elif old is not None and new is None :
636+ plays .remove (old )
637+ elif old is not None and new is not None :
638+ plays [plays .index (old )] = new
639+
640+ doc .edit (change )
641+ return (
642+ f"{ _say (m , block )} : plays { new } " if new is not None else f"{ _say (m , block )} : plays nothing"
643+ )
644+
645+
646+ def move_rule (doc : Edits , rules : Sequence [Path ], moved : int , to : int ) -> str :
647+ """Of one trigger's `rules`, in the order they are checked, the one at `moved` takes place
648+ `to`. The places of the rules' first blocks are handed round among them, so every other rule
649+ keeps its place in the whole order; a rule's own blocks move with its first block, a move
650+ node never. Refused if the rule would keep its place."""
651+ m = doc .manifest
652+ roots = [p .blocks [0 ] for p in rules ]
653+ if len (set (roots )) != len (roots ):
654+ raise ManifestError ("rules that start from the same block cannot be put in another order" )
655+ if moved == to :
656+ return "unchanged"
657+ order = list (range (len (rules )))
658+ order .insert (to , order .pop (moved ))
659+ heights = sorted (m .behaviour .blocks [r ].at [1 ] for r in roots )
660+ new_y = {roots [old ]: heights [k ] for k , old in enumerate (order )}
661+ held = Counter (i for p in [* behaviour .paths (m ), * open_rules (m )] for i in set (p .blocks ))
662+ key = behaviour .trigger_key (m , rules [0 ])
663+ want = [roots [old ] for old in order ]
664+
665+ def shift (m : Manifest ) -> None :
666+ blocks = m .behaviour .blocks
667+ for old , root in enumerate (roots ):
668+ dy = new_y [root ] - blocks [root ].at [1 ]
669+ for i in rules [old ].blocks :
670+ if dy and (i == root or held [i ] == 1 ):
671+ x , y = blocks [i ].at
672+ blocks [i ].at = _at ((x , y + dy ))
673+ got = next (t for t in behaviour .triggers (m ) if t .key == key )
674+ if [p .blocks [0 ] for _ , p in got .rules ] != want :
675+ raise ManifestError (
676+ "that would not change the rule's rank: rules at the same height are checked"
677+ " left to right, and rules that play nothing go first"
678+ )
679+
680+ doc .edit (shift )
681+ return f"rule moved to place { to + 1 } of its trigger"
0 commit comments