Someone brought to my attention that Python Chess was using up a lot of CPU cycles, even when (actually, especially when) it was just sitting waiting for user input. I checked on my system and found it was using 30-40% of the CPU time…way too much for such a simple game!
Some googling revealed that I was using pygame’s event loop in a reckless manner. I was using pygame.event.get() to get user mouse clicks or other input — evidently, this will return a list of events that have queued up since the last time the get() function was called (not sure if there is a limit to the queue size….) Since the call to get() is basically a while(1) loop, it was being called just as fast as it could, which is pretty unecessary for a game like chess. Actually, probably for any game this is overkill. Luckily, there are a few easy solutions.
The option which I used for Python Chess is to use pygame.event.wait() rather than pygame.event.get(). The wait() function will cause the program to go into an idle state (ie. not hogging the processor) until it gets a single event. This is much more suited for the game of chess, where the user needs time to ponder their move, anyway. Replacing my event.get() calls to event.wait() makes the CPU usage of Python Chess go down to 0-1% on my system, with occassional spikes during the AI’s turn of up to 10% (these are just rough numbers seen via Windows Task Manager). Quite an improvement!
Another option for limiting CPU usage is to create a pygame.time.Clock() object, then calling the object’s tick(fps) function every time through your while(1) loop. This will limit the loop’s execution to fps (frames per second) times per second, allowing it to go idle in between loop processing. (Note that on slower systems, the desired fps may not be achievable, so it will still use up a healthy chunk of processing time if the value is too high.) Using time.Clock seems to be more suited to “action” style games, where there is screen movement going on every time through the loop, for instance.
No big changes in this version of Python Chess, just a few compatibility fixes. I decided version 0.5 was ready to be released to the world, so I posted it on the Pygame website: http://www.pygame.org/project/1099/?release_id=1960. And, at least one person actually downloaded and played! He pointed out some issues/fixes with running on Linux (I had just been developing with Windows).
So here’s the really, truly, cross-platform, Python Chess: PythonChess_v0.6.zip
I’ve added in some better AI functionality. In addition to version 0.4’s random AI, which just picks any random legal move, there is now a defense AI and an offense AI.
Defense AI’s GetMove() Priority
- Only make a “protected move” — don’t move a piece to a space where the enemy can capture it on the next turn.
- Put enemy king in check if possible.
- If a piece is in danger of being captured by the enemy next turn, move it to a safe place. There is a priority mechanism here — the AI will move its queen to safety before moving a pawn to safety, for instance.
- Pick a protected move that captures another piece. The same priority mechanism applies — capture the queen or other high-value piece before capturing a pawn.
- If no moves from prior steps, pick any remaining protected move.
- If no protected moves, revert to random AI and pick any legal move.
The offense AI is the same as the defense AI, except steps 3 and 4 are reversed. In other words, it will ignore its own pieces’ safety if there is an opportunity to harm the enemy. Maybe it should be called “berserk” mode. 🙂
In expanding the ChessAI module, I took the opportunity to play around with inheritance in Python. The ChessAI_defense class extends the ChessAI_random class, while the ChessAI_offense class extends ChessAI_defense. They each have a GetMove() functions, so I don’t have to worry which class my AI object is from when I need to get a move from it in the main game loop.
I also created a sideline program called PythonChessAIStats.py. Since I can pit one AI vs. a different AI, I thought it would be cool to see how well they do versus each other. This could be used in the future to rank new AI approaches. The AIs all rely on random numbers to some extent, so in order to make the tests repeatable, the script reads in numbers from a file and uses them to seed Python’s random number generator. (I used RANDOM.ORG to generate 100 random seeds.) Sometimes the AI vs. AI won’t end (they just end up moving the same pieces back and forth), so I end the game after 200 turns and call it a tie if no one is in checkmate yet.
Here’s a summary of some trials I did (X = no trial):
|Win – Loss – Tie||AI Opponent (black)|
|AI Player (white)||Random AI||Defense AI||Offense AI|
|Random AI||X||0 – 78 – 22||0 – 81 – 19|
|Defense AI||X||32 – 44 – 24||21 – 47 – 32|
|Offense AI||X||32 – 44 – 24||21 – 47 – 32|
The new AIs are clearly better than just randomly moving about, so that’s good. Interesting that numbers for Defense vs. Defense are the same as for Offense vs. Defense….
This version of Python Chess also has better command line parsing. I found out about Python’s OptionParser class and used it. Both PythonChessMain.py and PythonChessAIStats.py can be run from the command line with a number of options — the -h option should display all of the other available options and usage.
I tried to make a standalone windows executable using py2exe (there are a couple of scripts in the .zip file that try to do that), which is supposed to wrap up the code along with all of the required libraries and the python interpreter into an executable, so it can be run on a Windows machine without the user having to have everything pre-installed. Would be useful for producing a game for the masses, but it wouldn’t work for me. The script runs and builds an executable, but it crashes when I try to run it. I’ll have to investigate further.