lithander wrote: ↑Sat Jan 30, 2021 2:51 am
I have replaced my piece counting evaluation with the tuned PSTs from Sunfish because I was curious and it made the engine play much stronger. But it's a bit like using neural networks - sure they work but you won't have an intuition why. I'll stick with counting pieces for now, that I understand just fine!
Very good idea. It's good to know what a PST actually does. When your program gets stronger, change the PST's here and there, and see what effect they have on the playing style. A PST basically isn't more than you telling the program: "This is a good square for that piece." If the PST's are tuned, you often use the evaluation of a stronger engine as a reference; at least in the beginning. The PST's then start to include compensation for the lack of knowledge in the evaluation function. This is the reason why you need to retune the PST's every time you change the evaluation.
My PST's are my own; I'm not going to copy/paste PST's from other engines. I'll probably go and tune them with my own engine as a reference to see if there are versions that can make the engine stronger than it is now. That is what I meant with maxing out each feature before adding a new one.
Many thanks for taking the time! Really appreciate it.
The performance doesn't surprise me. First of all I wrote my move generation code from scratch and so it doesn't use bitboards. My board representation is just an 8x8 array of pieces and then you loop over that and whenever you find a piece there's a switch statement that calls piece-type-specific routines to add the moves of this piece. It's very straight forward. Here's the
Youtube video with timecode where I explain that part to my (immagined
) audience. But of course conjuring black magic on 64bit integers would be much faster. I just didn't feel like I could do that without looking at existing implementations which is against my rules. My move generator's 'perft 6' takes 15 seconds on the start position. That's about 8 Million nodes generated per second. For a short while I was very proud about myself, then I discovered that 'qperft' finds the same result in 0.47 seconds and I cried a little!
Well... don't use QPerft as a reference. it is one of the fastest perft _calculators_ in the world. It's a fine program, but it includes many optimizations that are specific to perft, and have no bearing on actually playing chess. There are some optimizations in there however, that _are_ suited to make a move generator faster in a 'real' chess program, without any side effects. Those optimizations are for a later date, at least for my engine.
Rustic's perft speed, on an i7-6700K:
Code: Select all
Perft 1: 20 (0 ms, inf leaves/sec)
Perft 2: 400 (0 ms, inf leaves/sec)
Perft 3: 8902 (0 ms, inf leaves/sec)
Perft 4: 197281 (5 ms, 39456200 leaves/sec)
Perft 5: 4865609 (140 ms, 34754350 leaves/sec)
Perft 6: 119060324 (3286 ms, 36232600 leaves/sec)
Total time spent: 3431 ms
Execution speed: 36179695 leaves/second
As said, it's about 5 times faster than your engine, but it can't (yet) hold a candle to something like QPerft. It will become much faster when a transposition table is implemented; Perft is good for testing this, because if your transposition table doesn't work correctly (or your Zobrist hashing is off, which also makes your TT not work as it should), your perft numbers won't match.
It is also good to keep in mind that, when your engine progresses, it will become _slower_ if you don't actively do something to counter that. Previously, my Perft speeds were higher (around 41 million lps), but then I made the calculation of the PST's incremental.
What this means is that the engine doesn't calculate PST's from scratch anymore. Previously, the evaluation would run through the pieces, and add all the PST values for one side. Now, I add them all together when I set up the board. Then, when I move a piece, I substract the PST value from that square, and then add the value of the destination square.
This makes the evaluation MUCH faster. However, because the engine now does more work when making a move, I lost about 10% perft speed.
The evaluation is one of, if not the, heaviest/most called function in the engine, so a faster evaluation is better than a fast perft. Perft has no bearing on how strong/fast your total engine is; it shows how fast it can generate and execute moves.
Also, keep in mind that the numbers will fluctuate. When you add/remove code to your program, the compiler will change the layout and how the program sits in memory. Sometimes even some stuff like adding a single function of 3 lines that calculates one value, can change the perft speed of your program... either negatively, or positively.
With regard to your audience: I think there is one. Seeing how busy this forum is since the beginning of the pandemic in 2020, chess programming seems to have taken a real flight. If you are looking for video's similar to yours, with lots of good information, look at these series:
Chess Programming channel by Maksim Korz (Code Monkey King)
Writing a chess engine in C, by Richard Allbert (Bluefever Software)
In time, as I fill this out, written information about chess programming will also be available on Rustic's website:
https://rustic-chess.org/
The search in the version you tried was just minmax, so no pruning at all. It reaches a fixed depth of 4 plys and stops. But quiescence is on my list for version 0.2 because the kind of blunders the lack of it causes is really hard to endure. Even for a bad chess player like myself!
Oh, OK. That explains it. Even with basic alpha-beta, qsearch, and simple MVV-LVA sorting, you should be able to reach depth 7-8 in the middle game on something like an i7-6700K, in a few seconds.
I only implement a tiny subset of UCI at the moment. In version 0.2 I plan to at least support time controls and I also want to send an 'info' string back to the GUI. But I don't think I will add all UCI commands. Is there anything you found particularily irritating or consider important for even the most barebone engine?
"Irritating" maybe is the wrong word. But, as Ras says, you should not rely on a particular order of commands or parameters within those commands. Some things I did in my engine:
- If the engine receives a command it doesn't know, it just ignores it without any reply. This is what the protocol states it should do.
- If the engine receives "position startpos moves ..." and it encounters an illegal move, it stops parsing there, and sends an "info string <error>" to the GUI.
- If it receives an illegal FEN, it sends an "info string <error>", and does not change the board as it is.
- As soon as "go" is parsed, the engine assumes "infinite", and changes this if it encounters something else. If it encounters stuff it doesn't know, or it encounters nothing else, "go" will be "go infinite".
- The protocol states that "an engine should start up as fast as possible", and thus it should initialize only after "ucinewgame". Maybe this was a point 20 years ago, but nowadays, I disagree. When I start my engine, I expect it to be ready immediately. This means the following...
- It does not _need_ "ucinewgame" to start playing its first game; if it does receive it, it just resets the board. Then it will have done two initializations. Who cares...
- It doesn't _need_ "uci" to start playing or analyzing. The GUI could conceivably ignore everything, and just immediately send "go <...>", and the engine will work.
- I have implemented an -f (--fen) parameter, so I can set up a fen string on the command line, or in a script, without having to execute "position fen <fen>"
- I have implemented a -q (--quiet) parameter when the engine starts up. In the latest version, it is -q <integer>. If I start the engine with -q0 (or no parameter at all), it outputs full info and stats. -q1 surpresses the sending of stats, and only outputs the info information with regard to depth. I use this when I want to quickly analyze a position. -q2 surpresses everything. Some GUI's are very slow/bad at parsing the incoming information (Arena can't handle great blasts of information when running a tournament, for example). If you want to run a tournament and are not going to watch it, "-q 2" can be used to send no info/stats to the GUI or tournament manager.
All of this means that I can do this:
./rustic -q1 -f "fen_string"
(engine starts)
go
And it will analyze the position, only outputting depth/speed information, and no intermediate stats.
All UCI comamnds are implemented in Rustic, except for the following:
- register/copyprotection: no use for this in an open source engine.
- debug: Not needed it. Maybe, someday, I'll implement it.
- go mate: never used this. Maybe someday I'll implement it.
- go searchmoves: didn't even know it existed. Many GUI's can't even do it. I'll probably not implement this.
I noticed your engine before and liked the patience and care that oozes from it.
Thanks. I try. Sometimes I feel as if my development is too slow, because I take so much time for implementing and optimizing each function to the point where I feel that I have to never touch it again. On the other hand, it doesn't matter... the only person for which I'm writing this engine, is me, myself and I. In the end, I intend to start using this engine to analyze games. The target to reach is (someday) at least 2851 ELO on CCRL. That is the rating of Fritz 11, which has been my main engine for a long time. The next target will be 2895, which is the rating of Glaurung 2.2., the immediate predecessor of Stockfish. I have not set any goals with regard to the time it may take.
Rust is also an interesting choice. It's been on my radar for a while but I never gave it a try, yet. But it sounds perfect for chess engines.
Rust compiles down to LLVM intermediate language (same as used by the Clang and Clang++ compilers), so in the end, Rust is basically C/C++ with regard to speed. The one thing I love & hate at the same time is crates.io. (Rust's version of NuGet, pip, or NPM.) It makes it very easy and convenient to add a dependency, but one small dependency often is small by itself because it leans on 20 other dependencies. Rustic only has these:
rand: Random number generator
if_chain: a way to prevent massive if x && y && z && a && ... {} statements.
clap: command line parser library
crossbeam-channel: much faster multi-threading channel implementation than what is in the standard library right now.
Even so, a compile pulls in 34 crates in total.
However, there is also a huge amount of junk there. IMHO, crates.io should have been a curated repository, which only contains crates that are sanctioned by the Rust Core Team; i.e. crates that are so massive and extensively used, that they can be considered as essential. A crate could start out on "public-crates.io", and if it becomes big and important enough, it could transition to "crates.io". Many such idea's have been floated around, but the Rust Core team doesn't want to implement any of it. They are convinced that "the people" will make the important crates visible/at the top in crates.io, and the rest will become unused automatically.
When I feel like I have a good understanding of what components go into a strong engine (and why) I can totally see myself trying something more ambitious. I even have a name already for my potential long-term project! But one step at a time... it's been only a few months ago that I watched Queens Gambit and got interested in chess, after all, haha.
Good luck. It's rare to see someone writing a chess engine after just getting into chess. I've been playing (but not studying) chess for 30 years. I'm a decent but not super strong player myself (1850-2000 ELO, if I put the effort in).