I am trying to scaffold a project which offers generic infrastructure and common interfaces for the implementation of many different abstract strategy games. While I am not necessarily porting this from C (since I am doing many things differently from the original project), I am struggling to find my way around in sufficiently generic ways without doing disallowed things.
I have only been writing Rust for a couple months, so I was hoping someone more experienced could help guide me in the early design stages of this project. Here is the problem:
Context
The project includes five noteworthy module sets:
- Solving algorithms, which operate on games
- Games, which implement traits representing a few common game archetypes
- Database writers, which are used by solvers to persist game solutions
- Analyzers, which do data analysis on solution databases (this is the most independent module)
- Interfaces, including a CLI, TUI, GUI, and perhaps some networked APIs
...and the project core, which is where everything is called from. As such, I have structured the project as follows based on how interdependent the modules are:
- Library 1: Solvers, DB writers, Analyzers, and game archetype traits
- Library 2: Games
- Library 3: Interfaces
- Crate: Execution (project core)
Problem
My ambition with this project is to allow for choosing the dynamic-ish execution of different (game, solver) configurations during a single interface session. For example, a user of the CLI might command <name> solve -t "some game" -s "some solver"
.
The thing to know here is that not all games can be solved by all solvers, which is why I have set up traits and blanket implementations as follows:
/* EXAMPLE SOLVER INFRASTRUCTURE */
trait SolverTypeBSolvable { ... }
impl<G> SolverTypeBSolvable for G
where
G: GameTypeA,
G: GameTypeB,
G: GameTypeC,
{
fn solve_type_B(&self) { ... }
}
/* ANOTHER SOLVER */
trait SolverTypeASolvable { ... }
impl<G> SolverTypeASolvable for G
where
G: GameTypeA,
G: GameTypeB,
{
fn solve_type_A(&self) { ... }
}
Where game archetype traits, which include only behavior possible for specific types of games, are defined in the following way:
trait GameTypeX where Self: Game { ... }
I work on a team, so this results in it being very easy to implement a game: "Just write the implementation for the relevant GameType
trait (which is known through research), and you will not only get the most efficient solver possible, but also all the solvers that can solve your game."
This is where the problem lies. I need a way to allow for the choice of not only different games, but once a game is chosen, I need to let a user choose among the solvers which are available for that game (which reduces to getting a collection of the right solver function pointers).
We are more than happy to maintain a list of available games and a map of "what is solved by what", but I don't know how to turn a string into a function signature in a legal and automatic way, without needing to add branch logic for every single possible solver that we make (I want to stray away from doing something like if input=="solverA" { game.solve_type_A(); } else if { ... }
).
I would appreciate any advice, expert or not!