Progress on Rustic

Discussion of chess software programming and technical issues.

Moderators: hgm, Rebel, chrisw

User avatar
mvanthoor
Posts: 1784
Joined: Wed Jul 03, 2019 4:42 pm
Location: Netherlands
Full name: Marcel Vanthoor

Re: Progress on Rustic

Post by mvanthoor »

It's (almost) alive :D

Rustic now has a completely basic alpha-beta function, which doesn't even include mate and stalemate yet. However, I couldn't resist, and had it play its first game in the console in the old fashioned way... I had it search, then I put in the move in the console, and also made it on a normal wooden chess board.

Some observations: Because it doesn't know anything but material values and PSQT, the values in the PSQT have a MASSIVE influence on its playing style and strength. (I actually had to tweak the tables to make it not play super-defensively.) It has no QSearch yet, so it suffers from the horizon effect, as expected; it is also biased to "uneven" depths (the one where it just moved its own piece), also as expected.

I haven't printed out the nodes and the speed yet, but I think I can be happy with it. Even without any move ordering, transposition table or search enhancements, it already instantly hits a search depth of 7, depth 8 takes 3-15 seconds depending on the position, but in the middle game, depth 9 is a bit too far. For that, at least some move ordering must be implemented.

It's getting there :)

The end result of that game:

Code: Select all

================================================

8   . . Q . . . . .
7   . . . r . k . i
6   i . N . . . . .
5   . . . . . . . .
4   . . . . . I . .
3   I . . . I . . .
2   . I I . . . I I
1   R . . . K . . R

    A B C D E F G H

Zobrist key:        e268d551875ae943
Active Color:       White
Castling:           KQ
En Passant:         -
Half-move clock:    2
Full-move number:   37

Rustic > s
depth: 1, best move: c8d7, eval: 2755
depth: 2, best move: c8d7, eval: 2720
depth: 3, best move: c8d7, eval: 2810
depth: 4, best move: c8d7, eval: 2760
depth: 5, best move: c8d7, eval: 2825
depth: 6, best move: c8d7, eval: 2820
depth: 7, best move: c8d7, eval: 2865
c8d7
================================================
Author of Rustic, an engine written in Rust.
Releases | Code | Docs | Progress | CCRL
User avatar
mvanthoor
Posts: 1784
Joined: Wed Jul 03, 2019 4:42 pm
Location: Netherlands
Full name: Marcel Vanthoor

Re: Progress on Rustic

Post by mvanthoor »

emadsen wrote: Thu Oct 15, 2020 1:49 am To each their own. But I think you'd be better served by dogfooding your UCI implementation when testing your engine. By that I mean test your engine by directly entering UCI commands in a terminal window. This mimics what a UCI-compliant GUI will do and eliminates the possibility of bugs introduced by differences between your custom console and UCI. Besides, nothing prevents you from supporting non-standard UCI commands. That's what I do in my engine.

<snip>
Thanks for checking in Eric :)

The xboard and uci modules are going to be (basically) the same as the console mode, only with different commands. I already planned to have the console mode share the output functions with the console mode so they'd print the same commands; the console mode would then end up as a somewhat more friendly mode to use the engine on the command line.

I've already started implementing the UCI mode, and the boilerplate (thread handling and stuff) is basically the same in all three modules, so that was a quick start. I expect the engine to be able to connect to a GUI in a few days.

Maybe, in the end, I'll drop the console mode if I find the UCI / Xboard modes usable for testing as well.

If nothing else, I learned a lot about Rust's threading model while implementing this console, and implementing UCI and XBoard will be faster because of it.
Author of Rustic, an engine written in Rust.
Releases | Code | Docs | Progress | CCRL
User avatar
hgm
Posts: 27790
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Re: Progress on Rustic

Post by hgm »

Well, it is like Eric said: there is nothing against having the engine understand a few non-standard commands to do things you consider handy, that would not be needed when playing under a GUI. I usually make my engines support extra commands 'p' and 'l' for printing an ascii board or dump the piece list.
User avatar
mvanthoor
Posts: 1784
Joined: Wed Jul 03, 2019 4:42 pm
Location: Netherlands
Full name: Marcel Vanthoor

Re: Progress on Rustic

Post by mvanthoor »

I'll see what I'll put in there. It's now possible to play a game (in a laborious fashion), so the next two things will be a bit of cleanup and then implementing the UCI-interface for easier handling of this thing :)
Author of Rustic, an engine written in Rust.
Releases | Code | Docs | Progress | CCRL
Henk
Posts: 7216
Joined: Mon May 27, 2013 10:31 am

Re: Progress on Rustic

Post by Henk »

I think best is not to focus on results. Most important is maintainable code.
One spends most of the time in refactoring and debugging code. So best is to make that easy.
Results. Who cares about results? We are not Stockfish or LCzero testers.
Nobody cares about our stupid engines.
User avatar
mvanthoor
Posts: 1784
Joined: Wed Jul 03, 2019 4:42 pm
Location: Netherlands
Full name: Marcel Vanthoor

Re: Progress on Rustic

Post by mvanthoor »

Henk wrote: Thu Oct 15, 2020 1:49 pm I think best is not to focus on results. Most important is maintainable code.
One spends most of the time in refactoring and debugging code. So best is to make that easy.
In my opinion, my engine is written to be extremely maintainable. It is modular enough that it actually can have features disabled or enabled. Until now, it's only one feature, called "extra", which compiles code into the engine to run a 172 test perft suite (and in the future, WAC.epd), and generate new magic numbers. (Mostly useful for studying how magic numbers ARE actually generated; many engines take these from other engines.) Even though it's only one feature, it shows that I can add capabilities to the engine in such a way that they can be compiled into it on request. Also, every module does only one thing:

"Board" handles the board and makes/unmakes moves onto it.
"movegen" generates moves.
"evaluate" evaluates.
"engine" is the actual engine
"search" handles searching
"comm" receives and sends messages (through uci, xboard, or console)
"extra" contains the extra stuff mentioned above
"misc" contains useful functions that are used throughout the engine and don't belong to a single module.

The engine is completely mutli-threaded: nothing is blocking, and there's no peeking from one module into another module's data or memory space. If stuff needs to be sent from one module to another, then "Engine" will handle it. "Engine" is the spider-module in the middle of the web where everything is routed through, so Engine always knows about the state of the chess-board, and what other modules should be doing at any point in time. So, an incoming message will be intercepted by Comm; it will package this into a CommReport, which it sends to the engine. The engine will then react accordingly by sending messages to running threads where necessary.

Each module has the same setup. It has a "defs.rs" file with definitions, and the entry file that has the same name as the module. Only the entry file and the definitions are public. Also, every function in the engine does one thing only: the thing it describes. And there are no global variables. None. If you want a method to have data, you will have to pass it by argument, or send data into it using a sender if the method happens to be running in a thread.

If this turns out to be unmaintainable, I don't know what will ever be. I'll probably delete my engine, quit software engineering, burn my computer, and go and be depressed for the rest of my life.
Results. Who cares about results? We are not Stockfish or LCzero testers.
Nobody cares about our stupid engines.
I don't agree. If there are engines in which my interest is waning, it's the top ones. I stick with Stockfish 10, because version 11 added uci_elo and recalibrated the strength to CCRL; uci_elo 1350 is MUCH stronger than FIDE 1350 Elo. (It's more like 1700-1750 or thereabout.) I haven't even tried 12 yet. My next favorite engine is Texel 1.07. It has a wonderful levels system. It can be weakened to the point that even a beginning player is able to defeat it.

I'm also following engines that are at, or just beyond the development stage of where Rustic now is, and I enjoy seeing them get better, especially if they are developed completely in the open, almost live, like Maksim's (CMK's) BBC and Wukong. When Vivien was flailing around with Weini and later started Minic, I followed that; and Terje's development of Weiss. I'm also following Stash and Madchess, but development has slowed down a bit. They are all engine's that are starting out (Wukong, BBC), are in the middle of the pack (Stash, Madchess), or medium high up there (Minic, Weiss). There's no high-end 3200+ engine among them.

The lower-to-middle grade engines are the most enjoyable to follow because their code is still understandable, and they still play chess I can actually understand.
Author of Rustic, an engine written in Rust.
Releases | Code | Docs | Progress | CCRL
JohnWoe
Posts: 491
Joined: Sat Mar 02, 2013 11:31 pm

Re: Progress on Rustic

Post by JohnWoe »

Henk wrote: Thu Oct 15, 2020 1:49 pm I think best is not to focus on results. Most important is maintainable code.
One spends most of the time in refactoring and debugging code. So best is to make that easy.
Results. Who cares about results? We are not Stockfish or LCzero testers.
Nobody cares about our stupid engines.
I focus on sloc. Currently at 1700 sloc. And going down! I don't want to maintain some 5000 lines beast. No matter how strong.

Big professional engines like Stockfish are best left for big teams with time and computing power.
User avatar
mvanthoor
Posts: 1784
Joined: Wed Jul 03, 2019 4:42 pm
Location: Netherlands
Full name: Marcel Vanthoor

Re: Progress on Rustic

Post by mvanthoor »

JohnWoe wrote: Fri Oct 16, 2020 3:28 pm
Henk wrote: Thu Oct 15, 2020 1:49 pm I think best is not to focus on results. Most important is maintainable code.
One spends most of the time in refactoring and debugging code. So best is to make that easy.
Results. Who cares about results? We are not Stockfish or LCzero testers.
Nobody cares about our stupid engines.
I focus on sloc. Currently at 1700 sloc. And going down! I don't want to maintain some 5000 lines beast. No matter how strong.

Big professional engines like Stockfish are best left for big teams with time and computing power.
If there's one thing I stopped focusing on, it's total lines of code... especially because Rust's formatter already splits chained function calls over more than one line automatically. Defining a PSQT could take 1 line, but in my case it takes 10, because I put them in an 8x8 grid. Also, not using strings or numbers anywhere (if at all possible) takes a lot of liens for constants.

If anything, I try to keep my functions and files short. I can fit 40 lines of code in one window (because of my poor vision it can't be smaller), so I try to keep functions under 40 lines, and files under 200 lines (5 screens of scrolling). I really dislike very long files because of the scrolling it takes, especially in my case.

I just managed a massive refactor, so I can now start implementing UCI seriously :)
Author of Rustic, an engine written in Rust.
Releases | Code | Docs | Progress | CCRL
User avatar
mvanthoor
Posts: 1784
Joined: Wed Jul 03, 2019 4:42 pm
Location: Netherlands
Full name: Marcel Vanthoor

Re: Progress on Rustic

Post by mvanthoor »

Today I've routed search output through the engine thread to the Comm module, so search doesn't write to STDOUT directly anymore. This allows me to convert the search information into UCI, XBoard, or whatever protocol I need to support in the future without actually having to change either the search or the engine. (Assuming I don't need additional information.)

I've also implemented basic move sorting using MVV_LVA using this table:

Code: Select all

pub const MVV_LVA: [[u8; NrOf::PIECE_TYPES + 1]; NrOf::PIECE_TYPES + 1] = [
    [0, 0, 0, 0, 0, 0, 0],       // victim K, attacker K, Q, R, B, N, P, None
    [50, 51, 52, 53, 54, 55, 0], // victim Q, attacker K, Q, R, B, N, P, None
    [40, 41, 42, 43, 44, 45, 0], // victim R, attacker K, Q, R, B, N, P, None
    [30, 31, 32, 33, 34, 35, 0], // victim B, attacker K, Q, R, B, N, P, None
    [20, 21, 22, 23, 24, 25, 0], // victim K, attacker K, Q, R, B, N, P, None
    [10, 11, 12, 13, 14, 15, 0], // victim P, attacker K, Q, R, B, N, P, None
    [0, 0, 0, 0, 0, 0, 0],       // victim None, attacker K, Q, R, B, N, P, None
];
The King (impossible to capture) and None (no piece captured) are just in there so i can index any move into this table without checking anything. So Queen captured by Pawn yields 55, Rook captured by King yields 40, and so on. Using this I implemented move_scoring() and then tried sort_moves() and then pick_move() (sort_moves sorts all the moves, pick_move runs through the list and swaps the best scored move into the current index in the move list):

Code: Select all

$ ./target/release/rustic.exe -f "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1"

// No sorting.
depth: 1, bestmove: d2d4, eval: 400, time: 0s, nodes: 1, knps: 0
depth: 2, bestmove: d2d4, eval: -855, time: 0s, nodes: 8, knps: 0
depth: 3, bestmove: d2d4, eval: 35, time: 0s, nodes: 242, knps: 0
depth: 4, bestmove: g1h1, eval: -560, time: 0.002s, nodes: 2541, knps: 1271
depth: 5, bestmove: c4c5, eval: -105, time: 0.044s, nodes: 90990, knps: 2068
depth: 6, bestmove: g1h1, eval: -550, time: 0.53s, nodes: 704017, knps: 1328
depth: 7, bestmove: c4c5, eval: -140, time: 7.655s, nodes: 19164279, knps: 2503
depth: 8, bestmove: c4c5, eval: -525, time: 101.286s, nodes: 149397666, knps: 1475

// Sort Moves
depth: 1, bestmove: d2d4, eval: 400, time: 0s, nodes: 1, knps: 0
depth: 2, bestmove: d2d4, eval: -855, time: 0s, nodes: 8, knps: 0
depth: 3, bestmove: d2d4, eval: 35, time: 0s, nodes: 107, knps: 0
depth: 4, bestmove: g1h1, eval: -560, time: 0s, nodes: 625, knps: 0
depth: 5, bestmove: c4c5, eval: -105, time: 0.005s, nodes: 8324, knps: 1665
depth: 6, bestmove: g1h1, eval: -550, time: 0.022s, nodes: 26090, knps: 1186
depth: 7, bestmove: c4c5, eval: -140, time: 0.142s, nodes: 222147, knps: 1564
depth: 8, bestmove: c4c5, eval: -525, time: 0.692s, nodes: 839942, knps: 1214
depth: 9, bestmove: c4c5, eval: -190, time: 4.11s, nodes: 6640093, knps: 1616
depth: 10, bestmove: c4c5, eval: -490, time: 17.971s, nodes: 23531619, knps: 1309

// Pick Move
depth: 1, bestmove: d2d4, eval: 400, time: 0s, nodes: 1, knps: 0
depth: 2, bestmove: d2d4, eval: -855, time: 0s, nodes: 8, knps: 0
depth: 3, bestmove: d2d4, eval: 35, time: 0s, nodes: 107, knps: 0
depth: 4, bestmove: g1h1, eval: -560, time: 0s, nodes: 625, knps: 0
depth: 5, bestmove: c4c5, eval: -105, time: 0.003s, nodes: 8324, knps: 2775
depth: 6, bestmove: g1h1, eval: -550, time: 0.012s, nodes: 26090, knps: 2174
depth: 7, bestmove: c4c5, eval: -140, time: 0.07s, nodes: 222147, knps: 3174
depth: 8, bestmove: c4c5, eval: -525, time: 0.499s, nodes: 839942, knps: 1683
depth: 9, bestmove: c4c5, eval: -190, time: 1.881s, nodes: 6640093, knps: 3530
depth: 10, bestmove: c4c5, eval: -490, time: 13.823s, nodes: 23531619, knps: 1702
depth: 11, bestmove: c4c5, eval: -265, time: 64.836s, nodes: 228325596, knps: 3522
Rustic can now basically search up to depth 8-9 instantaneously in many positions. Please note that killers, history heuristic and a hash table are not yet implemented. (And quiescence search isn't either, which will probably cause iterative deepening to not go as deep, as it extends each iteration. I also assume that QSearch will solve the evaluation jumping around, and I'll have to look up if I need to print the eval from the side to move, or use the side of the color the engine is playing all the time.)
Author of Rustic, an engine written in Rust.
Releases | Code | Docs | Progress | CCRL
Terje
Posts: 347
Joined: Tue Nov 19, 2019 4:34 am
Location: https://github.com/TerjeKir/weiss
Full name: Terje Kirstihagen

Re: Progress on Rustic

Post by Terje »

mvanthoor wrote: Tue Oct 20, 2020 1:08 am Today I've routed search output through the engine thread to the Comm module, so search doesn't write to STDOUT directly anymore. This allows me to convert the search information into UCI, XBoard, or whatever protocol I need to support in the future without actually having to change either the search or the engine. (Assuming I don't need additional information.)

I've also implemented basic move sorting using MVV_LVA using this table:

Code: Select all

pub const MVV_LVA: [[u8; NrOf::PIECE_TYPES + 1]; NrOf::PIECE_TYPES + 1] = [
    [0, 0, 0, 0, 0, 0, 0],       // victim K, attacker K, Q, R, B, N, P, None
    [50, 51, 52, 53, 54, 55, 0], // victim Q, attacker K, Q, R, B, N, P, None
    [40, 41, 42, 43, 44, 45, 0], // victim R, attacker K, Q, R, B, N, P, None
    [30, 31, 32, 33, 34, 35, 0], // victim B, attacker K, Q, R, B, N, P, None
    [20, 21, 22, 23, 24, 25, 0], // victim K, attacker K, Q, R, B, N, P, None
    [10, 11, 12, 13, 14, 15, 0], // victim P, attacker K, Q, R, B, N, P, None
    [0, 0, 0, 0, 0, 0, 0],       // victim None, attacker K, Q, R, B, N, P, None
];
The King (impossible to capture) and None (no piece captured) are just in there so i can index any move into this table without checking anything. So Queen captured by Pawn yields 55, Rook captured by King yields 40, and so on. Using this I implemented move_scoring() and then tried sort_moves() and then pick_move() (sort_moves sorts all the moves, pick_move runs through the list and swaps the best scored move into the current index in the move list):

Code: Select all

$ ./target/release/rustic.exe -f "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1"

// No sorting.
depth: 1, bestmove: d2d4, eval: 400, time: 0s, nodes: 1, knps: 0
depth: 2, bestmove: d2d4, eval: -855, time: 0s, nodes: 8, knps: 0
depth: 3, bestmove: d2d4, eval: 35, time: 0s, nodes: 242, knps: 0
depth: 4, bestmove: g1h1, eval: -560, time: 0.002s, nodes: 2541, knps: 1271
depth: 5, bestmove: c4c5, eval: -105, time: 0.044s, nodes: 90990, knps: 2068
depth: 6, bestmove: g1h1, eval: -550, time: 0.53s, nodes: 704017, knps: 1328
depth: 7, bestmove: c4c5, eval: -140, time: 7.655s, nodes: 19164279, knps: 2503
depth: 8, bestmove: c4c5, eval: -525, time: 101.286s, nodes: 149397666, knps: 1475

// Sort Moves
depth: 1, bestmove: d2d4, eval: 400, time: 0s, nodes: 1, knps: 0
depth: 2, bestmove: d2d4, eval: -855, time: 0s, nodes: 8, knps: 0
depth: 3, bestmove: d2d4, eval: 35, time: 0s, nodes: 107, knps: 0
depth: 4, bestmove: g1h1, eval: -560, time: 0s, nodes: 625, knps: 0
depth: 5, bestmove: c4c5, eval: -105, time: 0.005s, nodes: 8324, knps: 1665
depth: 6, bestmove: g1h1, eval: -550, time: 0.022s, nodes: 26090, knps: 1186
depth: 7, bestmove: c4c5, eval: -140, time: 0.142s, nodes: 222147, knps: 1564
depth: 8, bestmove: c4c5, eval: -525, time: 0.692s, nodes: 839942, knps: 1214
depth: 9, bestmove: c4c5, eval: -190, time: 4.11s, nodes: 6640093, knps: 1616
depth: 10, bestmove: c4c5, eval: -490, time: 17.971s, nodes: 23531619, knps: 1309

// Pick Move
depth: 1, bestmove: d2d4, eval: 400, time: 0s, nodes: 1, knps: 0
depth: 2, bestmove: d2d4, eval: -855, time: 0s, nodes: 8, knps: 0
depth: 3, bestmove: d2d4, eval: 35, time: 0s, nodes: 107, knps: 0
depth: 4, bestmove: g1h1, eval: -560, time: 0s, nodes: 625, knps: 0
depth: 5, bestmove: c4c5, eval: -105, time: 0.003s, nodes: 8324, knps: 2775
depth: 6, bestmove: g1h1, eval: -550, time: 0.012s, nodes: 26090, knps: 2174
depth: 7, bestmove: c4c5, eval: -140, time: 0.07s, nodes: 222147, knps: 3174
depth: 8, bestmove: c4c5, eval: -525, time: 0.499s, nodes: 839942, knps: 1683
depth: 9, bestmove: c4c5, eval: -190, time: 1.881s, nodes: 6640093, knps: 3530
depth: 10, bestmove: c4c5, eval: -490, time: 13.823s, nodes: 23531619, knps: 1702
depth: 11, bestmove: c4c5, eval: -265, time: 64.836s, nodes: 228325596, knps: 3522
Rustic can now basically search up to depth 8-9 instantaneously in many positions. Please note that killers, history heuristic and a hash table are not yet implemented. (And quiescence search isn't either, which will probably cause iterative deepening to not go as deep, as it extends each iteration. I also assume that QSearch will solve the evaluation jumping around, and I'll have to look up if I need to print the eval from the side to move, or use the side of the color the engine is playing all the time.)
Weiss scores the position -550 or so at those depths with c5 as best move so the output looks reasonable. QSearch will probably fix the odd/even jumps.

For UCI you print eval relative to the side you search for.

Nice progress :D