I built a card game prototype, wondering about performance #2741
Replies: 3 comments 9 replies
-
Nice project, I also have used GDL to describe rules of a turn-based game in the past, but even state-of-the-art solvers couldn't deal with it in a timely manner. I haven't read your code yet, but the general suggestion is to reduce the search space by finding and eliminating symmetries. |
Beta Was this translation helpful? Give feedback.
-
As a good Prolog developer, you are just supposed to write good code and trust that the implementation will get better. However, as a game developer... profiling is import and appropriate. You can do some basic but useful profiling with the :- use_module(library(time)).
:- use_module(library(lists)).
:- use_module(library(charsio)).
?- time(findall(D, member(D, "0123456789"), Ds)).
%@ % CPU time: 0.000s, 12 inferences
%@ Ds = "0123456789".
?- time(findall(D, char_type(D, decimal_digit), Ds)).
%@ % CPU time: 0.629s, 7_784_568 inferences
%@ Ds = "0123456789". So I would start there to make sure that your predicates are meeting your expectations in terms of the number of inferences and time it takes. If you want to get more sophisticated with it, you can use I intend to use Prolog heavily in a Unity game I'm working on and I'm sure that sort of profiling will be very prominent, and I will most likely end up building out a profiling kit as well! |
Beta Was this translation helpful? Give feedback.
-
Happy New Year! Thank you a lot for sharing your work, very interesting! I noticed a few small things which I hope may help to improve the code, and eventually also its efficiency. I start with a very small aspect, which is in itself not—or at most only very slightly—relevant for performance. However, it helps to reduce the amount of code and therefore may help indirectly to better grasp and implement more important issues that are otherwise overshadowed and less visible. This point is about making more, and more direct, use of unification. For instance, instead of: get_assoc(game_phase, GameStateIn, GamePhase), GamePhase = playing, We can write: get_assoc(game_phase, GameStateIn, playing), We know that this usage mode is admissible from the signature of Likewise, instead of: \+ length(WinPile2, 0), we can write: WinPile2 = [_|_] It's shorter and also more general, and on top likely also more efficient (which is not the main reason why we are doing this). The same goes for: length(WinPile2, WinPile2Length), WinPile2Length #> 0, which can also be directly expressed by reasoning about the shape of the term itself. Another, more complex thing I noticed is: \+ member(Card, Deck1), This can be written more generally as: maplist(dif(Card), Deck1) We now have more freedom to move the goal around, and it also makes clear that we are reasoning here about a disequality. Seeing this, it may make sense to think about representing the problem in such a way that we can instead use CLP(ℤ) constraints, since domain information about integer variables can be very efficiently propagated. This would require representing the entities we want to reason about as integers, and a deck (maybe!) as the domain of an integer variable, expressing which cards can still be, or are, present in the deck. Another thing that stands out are long sequences of goals that have a common shape, like: maplist(any_card, VisibleDeck1), maplist(any_card, VisibleDeck2), maplist(any_card, VisibleWinPile1), maplist(any_card, VisibleWinPile2), It seems this can be expressed with a single goal such as (please verify!): maplist(maplist(any_card), [VisibleDeck1,VisibleDeck2,VisibleWinPile1,VisibleWinPile2]) Another such occurrence of goals that have a lot in common is: get_assoc(game_phase, GameStateIn, GamePhase), get_assoc(deck1, GameStateIn, Deck1), get_assoc(deck2, GameStateIn, Deck2), get_assoc(win_pile1, GameStateIn, WinPile1), get_assoc(win_pile2, GameStateIn, WinPile2), get_assoc(cards_on_table, GameStateIn, CardsOnTable), get_assoc(selected_category, GameStateIn, SelectedCategory), get_assoc(player_turn, GameStateIn, PlayerTurn), It seems such a sequence can be readily expressed with a lambda expression: maplist(GameStateIn+\Key^Value^get_assoc(Key,GameStateIn,Value), [game_phase,deck1,deck2,...], [GamePhase,Deck1,Deck2,...]). As mentioned, these changes are not expected to make a significant difference in performance by themselves. However, I hope they help to reduce the amount of code and make it easier to recognize more common patterns as candidates for further code reductions, and make it easier to change the remaining parts in order to improve performance. As soon as something seems tedious to write, we better stop and think about what we are doing: It may be a sign of redundancy in the code, and I suggest to look for ways to write it more compactly. Avoiding deconstructing terms may actually improve performance by itself. For instance, the following stands out as unusual: Player1Card =.. Player1CardDeconstructed, Player2Card =.. Player2CardDeconstructed,
I hope this helps, and if possible, please keep us posted! |
Beta Was this translation helpful? Give feedback.
-
Hello all, happy New Year!
I built a card game prototype using the new library interface:
init
rule that gives us the initial game staterandom_options
rule that gives us things to randomize based on the game statenext
rule to advance the game statesees
rule that tells us what each player can seeplayer_options
rule that gives a player the possible moves based on the game stateYou can clone the repo and run the game. It will print some game state and then the AI will start thinking about which category to pick. Once it figures it out you will be shown which cards were drawn. If the AI ever loses it's your turn to pick the category.
example output
This has all been a lot of fun but there is one problem: it seems very slow. Ideally I would like to let the AI play against the AI and tell me things about the game, is it balanced, etc. Then maybe change the Prolog and try again. Right now AI vs AI it never finishes (it actually runs out of memory on my laptop after an hour or so).
Any ideas for making this system more efficient? Given I am pretty new to Prolog and Rust I am sure there are lots of targets for optimization but I am wondering about big targets and systemic inefficiency, maybe something more fundamental.
I did install and run
cargo flamegraph
, here's the output, but it hasn't helped me figure out how to best make this faster.Anyway, all thoughts and suggestions are appreciated.
Beta Was this translation helpful? Give feedback.
All reactions