@@ -3,9 +3,10 @@ use crate::{error, planning::AssistKind, GameService};
33use gfa_api_types:: {
44 AnalysisRequest , AnalysisResult , ApiError , OpponentConfig , OpponentLevel , OpponentSpec ,
55} ;
6- use gfa_core:: { DynGame , GameSpec , Information , LegalAction , Observation , TurnStructure , Viewer } ;
6+ use gfa_core:: { DynGame , Information , LegalAction , Observation , TurnStructure , Viewer } ;
77use gfa_opponents:: {
8- ActionChoice , Algorithm , Clock , Opponent , PlayerTurn , Random , SearchLimits , SearchOpponent ,
8+ ActionChoice , Algorithm , Clock , Opponent , PlayerTurn , Random , ReferenceOpponent , SearchLimits ,
9+ SearchOpponent ,
910} ;
1011use serde_json:: Value ;
1112use std:: { future:: Future , pin:: Pin , sync:: Arc } ;
@@ -17,7 +18,7 @@ pub type OpponentFuture<'a> =
1718/// Select installed players without exposing live state or transport details.
1819pub trait OpponentFactory : Send + Sync {
1920 /// Stable catalog for this game.
20- fn catalog ( & self , game : & GameSpec ) -> Vec < OpponentSpec > ;
21+ fn catalog ( & self , game : & dyn DynGame ) -> Vec < OpponentSpec > ;
2122 /// Construct one player with validated options and limits.
2223 fn create (
2324 & self ,
@@ -82,8 +83,12 @@ fn spec(id: &str, name: &str, levels: bool) -> OpponentSpec {
8283}
8384
8485impl OpponentFactory for BuiltinOpponentFactory {
85- fn catalog ( & self , game : & GameSpec ) -> Vec < OpponentSpec > {
86+ fn catalog ( & self , game : & dyn DynGame ) -> Vec < OpponentSpec > {
8687 let mut catalog = vec ! [ spec( "random" , "Uniform random" , false ) ] ;
88+ if game. supports_reference_advice ( ) {
89+ catalog. push ( spec ( "reference" , "Reference solver" , false ) ) ;
90+ }
91+ let game = game. spec ( ) ;
8792 if game. information == Information :: Perfect
8893 && !game. stochastic
8994 && game. turn_structure == TurnStructure :: Sequential
@@ -105,7 +110,7 @@ impl OpponentFactory for BuiltinOpponentFactory {
105110 seed : u64 ,
106111 ) -> Result < ( Arc < dyn Opponent > , SearchLimits ) , ApiError > {
107112 if !self
108- . catalog ( & game. spec ( ) )
113+ . catalog ( game. as_ref ( ) )
109114 . iter ( )
110115 . any ( |spec| spec. id == opponent. id )
111116 {
@@ -115,11 +120,11 @@ impl OpponentFactory for BuiltinOpponentFactory {
115120 "Select an installed opponent from the game's catalog." ,
116121 ) ) ;
117122 }
118- if opponent. id == "random" && opponent. level . is_some ( ) {
123+ if matches ! ( opponent. id. as_str ( ) , "random" | "reference" ) && opponent. level . is_some ( ) {
119124 return Err ( ApiError :: new (
120125 "INVALID_CONFIG" ,
121- "Random has no difficulty levels" ,
122- "Omit level for the random opponent ." ,
126+ "This opponent has no difficulty levels" ,
127+ "Omit level for random and reference opponents ." ,
123128 ) ) ;
124129 }
125130 let mut limits =
@@ -136,6 +141,7 @@ impl OpponentFactory for BuiltinOpponentFactory {
136141 limits. validate ( ) . map_err ( error:: engine) ?;
137142 let player: Arc < dyn Opponent > = match opponent. id . as_str ( ) {
138143 "random" => Arc :: new ( Random ) ,
144+ "reference" => Arc :: new ( ReferenceOpponent :: new ( game, config) . map_err ( error:: engine) ?) ,
139145 "minimax" => Arc :: new (
140146 SearchOpponent :: new ( game, config, Algorithm :: Minimax ) . map_err ( error:: engine) ?,
141147 ) ,
@@ -171,7 +177,7 @@ impl GameService {
171177 Ok ( self
172178 . opponents
173179 . as_ref ( )
174- . map_or_else ( Vec :: new, |( factory, _) | factory. catalog ( & game. spec ( ) ) ) )
180+ . map_or_else ( Vec :: new, |( factory, _) | factory. catalog ( game. as_ref ( ) ) ) )
175181 }
176182
177183 /// Recommend legal moves without modifying events, clocks, or idempotency receipts.
@@ -244,6 +250,7 @@ impl GameService {
244250 ) ) ;
245251 }
246252 Ok ( AnalysisResult {
253+ advice : choice. info . advice ,
247254 game_id : request. game_id ,
248255 opponent : request. opponent . id ,
249256 seed,
0 commit comments