11use bevy:: prelude:: * ;
22use bevy_vector_shapes:: prelude:: * ;
3- use solitaire_solver:: { Board , Dir } ;
43
5- use crate :: { BoardPosition , CurrentBoard , board:: MARKER_POS , solver:: FeasibleConstellations } ;
4+ use crate :: {
5+ BoardPosition , CurrentBoard ,
6+ board:: MARKER_POS ,
7+ solver:: { FeasibleConstellations , ForcedJumps } ,
8+ } ;
69
710pub struct HintsPlugin ;
811
@@ -12,7 +15,7 @@ impl Plugin for HintsPlugin {
1215 app. add_observer ( update_hints) ;
1316 app. add_systems (
1417 Update ,
15- draw_possible_moves. run_if (
18+ ( draw_possible_moves, draw_forced_jumps ) . run_if (
1619 resource_exists :: < ShowHints > . and_then ( resource_exists :: < FeasibleConstellations > ) ,
1720 ) ,
1821 ) ;
@@ -33,36 +36,89 @@ fn update_hints(_: On<ToggleHints>, mut commands: Commands, show_hints: Option<R
3336 }
3437}
3538
39+ /// Draws one marker per legal move: green if a win is still reachable after it, red if not.
40+ ///
41+ /// Within the feasible set, a successor being feasible is exactly it being able to reach the
42+ /// solved board, so this two-state colouring is already complete information about the moves
43+ /// available *now* - including "only one of these is green", which is what makes a move
44+ /// forced. Colouring that case specially was tried and dropped: it told the player nothing
45+ /// they could not see by counting green lines. What they cannot see is
46+ /// [`draw_forced_jumps`].
3647fn draw_possible_moves (
3748 mut painter : ShapePainter ,
3849 board : Res < CurrentBoard > ,
3950 feasible : Res < FeasibleConstellations > ,
4051) {
41- let feasible = & feasible. 0 ;
42- for y in 0 ..Board :: SIZE {
43- for x in 0 ..Board :: SIZE {
44- for dir in [ Dir :: North , Dir :: East , Dir :: South , Dir :: West ] {
45- if !board. 0 . occupied ( ( y, x) ) {
46- continue ;
47- }
48- if let Some ( mov) = board. 0 . get_legal_move ( ( y, x) , dir) {
49- let start = BoardPosition :: from ( mov. pos ) . to_world_space ( ) ;
50- let start = Vec3 :: from ( ( start, MARKER_POS ) ) ;
51- let target = BoardPosition :: from ( mov. target ) . to_world_space ( ) ;
52- let target = Vec3 :: from ( ( target, MARKER_POS ) ) ;
53- painter. set_color ( if feasible. contains ( & board. 0 . mov ( mov) . normalize ( ) ) {
54- Color :: srgba ( 0. , 1. , 0. , 1. )
55- } else {
56- Color :: srgba ( 1. , 0. , 0. , 1. )
57- } ) ;
58- painter. set_translation ( Vec3 :: new ( 0. , 0. , 0.1 ) ) ;
59- painter. thickness_type = ThicknessType :: World ;
60- painter. thickness = 0.075 ;
61- painter. line ( start, start + ( target - start) * 0.2 ) ;
62- painter. set_translation ( start. xyz ( ) ) ;
63- painter. circle ( 0.1 ) ;
64- }
65- }
52+ for mov in board. 0 . get_legal_moves ( ) {
53+ let start = BoardPosition :: from ( mov. pos ) . to_world_space ( ) ;
54+ let start = Vec3 :: from ( ( start, MARKER_POS ) ) ;
55+ let target = BoardPosition :: from ( mov. target ) . to_world_space ( ) ;
56+ let target = Vec3 :: from ( ( target, MARKER_POS ) ) ;
57+
58+ let winning = feasible. 0 . contains ( & board. 0 . mov ( mov) . normalize ( ) ) ;
59+ painter. set_color ( if winning {
60+ Color :: srgba ( 0. , 1. , 0. , 1. )
61+ } else {
62+ Color :: srgba ( 1. , 0. , 0. , 1. )
63+ } ) ;
64+ painter. thickness_type = ThicknessType :: World ;
65+ painter. thickness = 0.075 ;
66+ painter. set_translation ( Vec3 :: new ( 0. , 0. , 0.1 ) ) ;
67+ painter. line ( start, start + ( target - start) * 0.2 ) ;
68+ painter. set_translation ( start. xyz ( ) ) ;
69+ painter. circle ( 0.1 ) ;
70+ }
71+ }
72+
73+ /// Ghosts the jumps the player is already committed to making later on.
74+ ///
75+ /// This is the one thing the per-move colouring cannot show, because it is not about the
76+ /// moves available now: these jumps are not legal yet, and may not be for another dozen
77+ /// moves, but every winning continuation from the current position makes them. See
78+ /// [`solitaire_solver::dominators::forced_jumps`] - and in particular why it works in the
79+ /// player's own frame rather than the normalized quotient, which would claim the four
80+ /// symmetric opening moves are one forced step.
81+ ///
82+ /// Drawn unlike [`draw_possible_moves`]: the full span from origin to landing slot rather
83+ /// than a stub, amber, and translucent, fading with how far off the jump is. Nothing is drawn
84+ /// while the result is stale - it is recomputed off-thread on every move and near the opening
85+ /// that takes seconds, so a result for a position already left has to be suppressed rather
86+ /// than shown late.
87+ fn draw_forced_jumps (
88+ mut painter : ShapePainter ,
89+ board : Res < CurrentBoard > ,
90+ forced : Option < Res < ForcedJumps > > ,
91+ ) {
92+ let Some ( forced) = forced else {
93+ return ;
94+ } ;
95+ if forced. board != board. 0 {
96+ return ;
97+ }
98+
99+ let pegs = board. 0 . count_pegs ( ) ;
100+ for jump in & forced. jumps {
101+ // a forced jump *out of the current board* is exactly the single-green-line case, and
102+ // `draw_possible_moves` already draws it
103+ let ahead = pegs. saturating_sub ( jump. board . count_pegs ( ) ) ;
104+ if ahead == 0 {
105+ continue ;
66106 }
107+
108+ let start = BoardPosition :: from ( jump. mov . pos ) . to_world_space ( ) ;
109+ let start = Vec3 :: from ( ( start, MARKER_POS ) ) ;
110+ let target = BoardPosition :: from ( jump. mov . target ) . to_world_space ( ) ;
111+ let target = Vec3 :: from ( ( target, MARKER_POS ) ) ;
112+
113+ // the soonest forced jump reads strongest; twenty moves out is barely there
114+ let fade = 1.0 - ( ahead as f32 / 20.0 ) . clamp ( 0.0 , 1.0 ) ;
115+ let alpha = 0.15f32 . lerp ( 0.7 , fade) ;
116+ painter. set_color ( Color :: srgba ( 1.0 , 0.72 , 0.15 , alpha) ) ;
117+ painter. thickness_type = ThicknessType :: World ;
118+ painter. thickness = 0.05 ;
119+ painter. set_translation ( Vec3 :: new ( 0. , 0. , 0.1 ) ) ;
120+ painter. line ( start, target) ;
121+ painter. set_translation ( target. xyz ( ) ) ;
122+ painter. circle ( 0.06 ) ;
67123 }
68124}
0 commit comments