Rule Plugins
The engine supports an opt-in plugin pipeline that wraps base crossword rules. Plugins can:
- Reject or transform user actions (pre-validate/validate)
- Convert actions into a
MoveDraft(placements) consumed by base rules - Modify the computed score (
modify_scorehook) - Run post-commit side effects
Core types
Action:Place,Stack,SwapRack,RotateTile,SlideGroup,Custom(String, Value)UserMove { actions: Vec<Action> }RulePlugintrait with hooks:pre_validate,validate,to_draft,modify_score,commitPluginRules { base: CrosswordRules, plugins: Vec<Box<dyn RulePlugin>> }
Example: Basic actions + score bonus
use engine::*;
let tileset = Tileset { tile_kinds: vec![
TileKind { id: "A".into(), symbol: "A".into(), score: 1, is_blank: false, aliases: vec![] },
TileKind { id: "B".into(), symbol: "B".into(), score: 3, is_blank: false, aliases: vec![] },
]};
let mut counts = std::collections::HashMap::new(); counts.insert("A".to_string(), 10); counts.insert("B".to_string(), 10);
let cfg = GameConfig { tileset, rack_size: 7, board_layout: RectBoardLayout { width: 5, height: 5 }, ruleset_id: "cross".into(), dictionary_id: "en".into(), rng_seed: 1, tile_counts: counts };
let mut state = GameState::new(&cfg, 2).unwrap();
state.dictionary = Some(Box::new(FstDictionary::from_words(vec!["AB".to_string()], true)));
let base = CrosswordRules { free_word_mode: false, ..Default::default() };
let rules = PluginRules::new(base, vec![
Box::new(BasicActionsPlugin::default()),
Box::new(ScoreBonusPlugin { bonus: 5 }),
]);
let center = CrosswordRules::center_cell(&state.board.geom);
let cc = state.board.geom.from_cell_id(center).unwrap();
let mut mv = UserMove { actions: vec![
Action::Place { x: cc.x, y: cc.y, kind_id: "A".into(), mark: None },
Action::Place { x: cc.x + 1, y: cc.y, kind_id: "B".into(), mark: None },
]};
let v = rules.validate_user_move(&state, mv).unwrap();
let mut sc = rules.score_user_move(&state, &v);
assert_eq!(sc.total, 9); // 1+3 + bonus 5
rules.commit_user_move(&mut state, v, &sc).unwrap();
Lifecycle
UserMove (actions)
|
| pre_validate() — plugins may reject or rewrite actions
v
validate() — plugins may enforce constraints
|
| to_draft() — one plugin produces MoveDraft (placements)
v
CrosswordRules.validate() — base line/contiguity/anchor checks
|
v
score() — base scoring
|
| modify_score() — plugins adjust totals/breakdown
v
commit() — base commit (board/rack/bag updates)
|
v
plugin.commit() — optional side effects
Template plugin crate
- See
examples/plugin_template/for a minimal external crate that implements two plugins:ScoreBonusPlugin— adds a fixed bonus to each move.RejectFarPlacementsPlugin— rejects moves when placed tiles are too far apart.- Import the crate and add
Box::new(...)instances to yourPluginRuleslist.
Notes
- Plugins run in order. Be mindful of deterministic behavior.
- Use
Custom(String, Value)to pass structured configs to specialized plugins. - Upwords-style overlays are supported in base rules (see Stacking). A plugin can toggle these at runtime or pre-process actions.