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