@@ -12,7 +12,7 @@ pub(crate) mod sequence_features;
1212use base64:: { Engine as _, engine:: general_purpose:: STANDARD as BASE64 } ;
1313use serde:: { Deserialize , Serialize } ;
1414
15- use crate :: action:: { Action , ActionEncoder } ;
15+ use crate :: action:: { Action , ActionEncoder , ActionType } ;
1616use crate :: errors:: { RiichiError , RiichiResult } ;
1717use crate :: types:: Meld ;
1818
@@ -116,16 +116,23 @@ impl Observation {
116116
117117 pub fn find_action ( & self , action_id : usize ) -> Option < Action > {
118118 let encoder = ActionEncoder :: FourPlayer ;
119- self . _legal_actions
120- . iter ( )
121- . find ( |a| {
122- if let Ok ( idx) = encoder. encode ( a) {
123- ( idx as usize ) == action_id
124- } else {
125- false
126- }
127- } )
128- . cloned ( )
119+ // Prefer non-red-five candidates so that 5m/5p/5s discards do not
120+ // accidentally drop the akadora when a normal 5 is also legal.
121+ let mut fallback: Option < & Action > = None ;
122+ for action in & self . _legal_actions {
123+ let Ok ( idx) = encoder. encode ( action) else {
124+ continue ;
125+ } ;
126+ if ( idx as usize ) != action_id {
127+ continue ;
128+ }
129+ if is_red_five_discard ( action) {
130+ fallback. get_or_insert ( action) ;
131+ } else {
132+ return Some ( action. clone ( ) ) ;
133+ }
134+ }
135+ fallback. cloned ( )
129136 }
130137
131138 /// Return absolute player indices in relative order: [self, shimocha, toimen, kamicha].
@@ -159,3 +166,82 @@ impl Observation {
159166 Ok ( obs)
160167 }
161168}
169+
170+ fn is_red_five_discard ( action : & Action ) -> bool {
171+ matches ! ( action. action_type, ActionType :: Discard )
172+ && matches ! ( action. tile, Some ( 16 ) | Some ( 52 ) | Some ( 88 ) )
173+ }
174+
175+ #[ cfg( test) ]
176+ mod tests {
177+ use super :: * ;
178+
179+ fn obs_with_actions ( actions : Vec < Action > ) -> Observation {
180+ Observation :: new (
181+ 0 ,
182+ [ vec ! [ ] , vec ! [ ] , vec ! [ ] , vec ! [ ] ] ,
183+ [ vec ! [ ] , vec ! [ ] , vec ! [ ] , vec ! [ ] ] ,
184+ [ vec ! [ ] , vec ! [ ] , vec ! [ ] , vec ! [ ] ] ,
185+ vec ! [ ] ,
186+ [ 25000 , 25000 , 25000 , 25000 ] ,
187+ [ false , false , false , false ] ,
188+ actions,
189+ vec ! [ ] ,
190+ 0 ,
191+ 0 ,
192+ 0 ,
193+ 0 ,
194+ 0 ,
195+ vec ! [ ] ,
196+ false ,
197+ [ None , None , None , None ] ,
198+ [ None , None , None , None ] ,
199+ None ,
200+ None ,
201+ )
202+ }
203+
204+ fn discard ( tile : u8 ) -> Action {
205+ Action :: new ( ActionType :: Discard , Some ( tile) , vec ! [ ] , Some ( 0 ) )
206+ }
207+
208+ #[ test]
209+ fn find_action_prefers_non_red_5m ( ) {
210+ // 5m action_id = 16 / 4 = 4; legal actions list red 5m first.
211+ let obs = obs_with_actions ( vec ! [ discard( 16 ) , discard( 17 ) , discard( 18 ) , discard( 19 ) ] ) ;
212+ let chosen = obs. find_action ( 4 ) . expect ( "discard 5m should resolve" ) ;
213+ assert_eq ! ( chosen. tile, Some ( 17 ) , "non-red 5m must win over red 5m" ) ;
214+ }
215+
216+ #[ test]
217+ fn find_action_prefers_non_red_5p ( ) {
218+ // 5p action_id = 52 / 4 = 13.
219+ let obs = obs_with_actions ( vec ! [ discard( 52 ) , discard( 54 ) ] ) ;
220+ let chosen = obs. find_action ( 13 ) . expect ( "discard 5p should resolve" ) ;
221+ assert_eq ! ( chosen. tile, Some ( 54 ) ) ;
222+ }
223+
224+ #[ test]
225+ fn find_action_prefers_non_red_5s ( ) {
226+ // 5s action_id = 88 / 4 = 22.
227+ let obs = obs_with_actions ( vec ! [ discard( 88 ) , discard( 90 ) ] ) ;
228+ let chosen = obs. find_action ( 22 ) . expect ( "discard 5s should resolve" ) ;
229+ assert_eq ! ( chosen. tile, Some ( 90 ) ) ;
230+ }
231+
232+ #[ test]
233+ fn find_action_falls_back_to_red_when_only_red_legal ( ) {
234+ // Only the red 5m is in the hand; we still need a working discard.
235+ let obs = obs_with_actions ( vec ! [ discard( 16 ) ] ) ;
236+ let chosen = obs. find_action ( 4 ) . expect ( "red-only 5m must still resolve" ) ;
237+ assert_eq ! ( chosen. tile, Some ( 16 ) ) ;
238+ }
239+
240+ #[ test]
241+ fn find_action_unaffected_for_non_five_tiles ( ) {
242+ // 4m action_id = 12 / 4 = 3.
243+ let obs = obs_with_actions ( vec ! [ discard( 12 ) , discard( 13 ) ] ) ;
244+ let chosen = obs. find_action ( 3 ) . expect ( "discard 4m should resolve" ) ;
245+ assert_eq ! ( chosen. tile, Some ( 12 ) , "first match wins for non-red ties" ) ;
246+ }
247+ }
0 commit comments