New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Not using set_free_handicap ? #65
Comments
I think it had to do with Chinese vs Japanese when I was first working on gtp2ogs and bots weren't Chinese-only back then. In Chinese, handicaps are freely placed but in Japanese I believe they are supposed to go to specific locations. Had something to do with that, but it's been a really long time. I'm also unsure if set_free_handicap can be used to place stones one at a time. It takes a list of vertices to put stones on. |
Some bots are Chinese-ruleset-only (like AlphaGo despite its Korean Pro status) and some are Japanese-ruleset-only (like AQ, though it can accept Chinese), yet many experimental bots are Tromp-Taylor-ruleset-only (https://www.cs.cmu.edu/~wjh/go/tmp/rules/TrompTaylor.html) because that is the ruleset used by the Computer Go Server. To me, using play command is about the only viable solution. I once invited Pasky (Pachi dev) to run Pachi on OGS years ago, but that issue might have dissuaded him. |
Interesting, until now the sequences i was familiar with used either fixed_handicap or set_free_handicap for japanese rules:
and set_free_handicap for chinese rules (no choice here):
Yes, set_free_handicap must send all stones at once afaik. No big deal for sure, and looks like other engines are happy with play commands too. Initially i put workarounds in pachi, but i think i'll try fixing gtp2ogs instead: there's a potential for issues with this. For example an sgf file with initial b&w setup stones converted to gtp also results in multiple play b commands, so it's going to think it's handicap game ... |
Ahh, now I clearly see what you meant. Thanks to Nick Wedd, a large part of computer Go has been KGS-driven for a long time. |
This probably is being done incorrectly then. It's also maybe why the adaptive resign code in LZ doesn't do anything for LZ bots on OGS. I'm not sure of a simple solution though. |
Trying this in my set_free_handicap branch. This seems to work ok (starting and resuming live games at least) but there are some code paths i'm unclear about:
|
btw what's a forked game ? Can bots end up in one ? |
Yes, you load up a game and move to the move you want to start a new game from, hit Fork, and it'll make a brand new game starting from that position. Bots will accept it like any other match. |
Nice =) |
What should probably be happening is chinese handicap moves are stored in gtp2ogs and then when the last one is made and we need to ask the bot for a move, we issue the set_free_handicap before asking for the genmove. That's basically what happens when a game is resumed if the bot is restarted (or if --persist isn't being used I assume). You're the first person to notice this behavior isn't strictly right but it also means Leela Zero isn't picking up the proper handicap info either since it has a dynamic resign threshold for handicap games which is currently being ignored. I never noticed it myself these past 1-2 years since I mostly only cared about my bots ranked non-handicap matches. :) Oops! |
Yes, checkout my branch, this is pretty much what it does. I was thinking along the same line: storing moves until ready, but it turned out even simpler: bot doesn't exist until genmove, at which point loadState() is called and sends all the play commands. So gtp2ogs already does the buffering for us, just need to shape the play commands into set_free_handicap there: // Use set_free_handicap for handicap stones, play otherwise.
if (doing_handicap && handicap_moves.length < state.handicap) {
handicap_moves.push(move);
if (handicap_moves.length == state.handicap)
this.sendHandicapMoves(handicap_moves, state.width);
else continue; // don't switch color.
} else
this.command("play " + c + ' ' + move2gtpvertex(move, state.width)) |
Will do! :) |
Why remove the support for set_free_handicap in state.initial_state? |
I won't experiment tonight but here's what I'm thinking. As you point out, the bot isn't created until the game needs a move from the bot. So the initial loadState() is where the handicap magic needs to happen. When state comes from server, if there are already handicap stones placed they are in state.initial_state.black. (If someone is doing something odd with white getting the stones we can ignore that and just play the stones.) Other moves (including handicap stones?) are in state.moves. This is already supported and working with set_free_handicap. If we connect when handicap is partially done, I'm unsure if state.initial_state.black has the partial list or not. I'm going to assume it does. As more handicap placements arrive, they are added to state.moves. I assume a partial list gets sent with set_free_handicap. We'd need to set up a few test handicap games and dump gamedata packets to see exactly what the server sends in this case. If we connect at game start then state.initial_state.black is empty, and all moves that arrive go into state.moves within gtp2ogs. So it seems to me, on the gtp2ogs end, if we are in a handicap game and not all handicap moves have been played yet, do we want to add the play to state.initial_state.black instead of state.moves? (Or do they go on both? Without dumping a gamedata I'm unsure if state.initial_state.black moves are duplicated in state.moves or not.) The result here is that whether a game was connected to mid-handicap or at start of match, one the bot does start, state.initial_state.black correctly has all handicap stones inside it. And thus set_free_handicap will be used properly. Because Line 572 in f6aadc9
As such... would we change Line 795 in f6aadc9
All of the tests like Anything using state.moves.length() would have to be glanced up to see if we'd break it by not putting the handicap stones in state.moves any more, and instead putting them into state.initial_state.black. There might be better alternatives as well. For example, we leave all of the move processing logic the same but in loadState we could see if state.initial_state.black.length is less than state.handicap, and if so we know some handicap moves are inside state.moves and thus add those to our set_free_handicap command and skip the move commands at Line 572 in f6aadc9
|
The thing is we don't want to break forked games. ;) We should try and solve this in a way that works for every scenario. (Also remember someone might give handicap to the bot, which will be calling genmove on each handicap placement needed. Should be tested with any changes.) Off the cuff we could do something like
Because we know when we loadState opponent has no more handicap stones to place. And then we do this at line 572 if black had handicap:
(black.length doesn't exist in this scope but I'm just giving example what I'm thinking, could calculate those arrays in the outer scope to access here). I think the merit of this idea is nothing outside of loadState changes, we simply add more black moves to set_free_handicap if needed? |
Had to rewrite it because there are issues with it: if (state.initial_state) {
let black = decodeMoves(state.initial_state.black, state.width);
let white = decodeMoves(state.initial_state.white, state.width);
if (black.length) {
let s = "";
for (let i=0; i < black.length; ++i) {
s += " " + move2gtpvertex(black[i], state.width);
}
this.command("set_free_handicap " + s);
}
... So only in forked games we have So for now i just send play commands which is more sane. You're right, in the end we'll need a proper set_free_handicap there as well, but i wanted to get forked games out of the way to get a fix for regular games first (put another way, it's been like this a long time, it can wait a little =) |
Yes, definitely. Try to break my branch, i've been running pachi without workarounds with it for a day or two trying different things and couldn't find flaws so far. Handicap looks good except in forked games, but that's easy to fix now. |
The other way (handicap with bot as black) is more tricky, maybe we can have another issue / PR for it ? |
One thing i haven't tested is 'edited' moves: if (move.edited) {
... They could get in the way if there are some in/before the handicap stones. |
Oh that's a mess. @anoek what were you thinking? ;) I assumed from reverse engineering that state.initial_state was only the handicap stone placements. But I guess that's not true at all. Oops. |
|
I don't even know what edited moves are lol. This code predates my involvement. So initial state is the state when game was created, like forked. Easy enough. |
I will give your code a try on my bots tonight. So if there is an initial state we count off some handicap stones from there and the moves list and then do normal moves for the rest of the state. If state is shorter than handicap we append some moves to it. I agree existing code probably just gets scrapped. I'm glad you discovered this. Handicap stones played by white isn't a thing right? |
Sounds good to me.
Hell, no ! That would be sooo wrong... |
I tried it out some with RoyalLeela and it seemed to work fine. So right now on a fork of a handicap game, we'll play all stones with normal move commands and still not use set_free_handicap. But non-forked handicap matches will use the command. Do you want to try and get forked games working correctly too before doing a pull request? |
@anoek I don't know what an "edited" move in moves.edited is. Can you clue me in? :) |
Ok, I updated my branch to handle forked games but it doesn't work: Edit: Actually, forked game itself is setup without handicap, see #12434541's game info for example. |
An "edited move" is used in reviews to place stones on the board in non accordance to the normal flow of the game... ie, if you want to just create a wall of black, or make an impossible situation for demonstration purposes. I don't think you should ever see those in a game for bots, unless you are seeing them for forked games? (sorry it's been awhile since I've been in that code) |
I imagine it's safe to ignore. @lemonsqueeze Maybe make a weird board in the editor, then fork it, and see if the gamedata has moves.edited or not? If not then we never need to deal with edited moves. And atm since forked games lose handicap data, we can probably ignore that too. Unless @anoek wants to consider that a server bug and I could go post an issue for it. |
I'm pretty much okay with "if you are going to fork a game or edit a game then the bot might not be aware of handicap properly", considering handicap has been wrong all of this time and bots still work well enough. ;) |
I do consider the forked games losing handicap data a server side bug, kinda forgotten there was still some issues with that |
Just tried a few forked edited games: no edited moves so far, all goes into initial_state. I used to think the other case was the tricky one, maybe it's this one actually =) |
btw, i'm running gtp2ogs with extra logging, sometimes i see |
That means they used "auto" handicap. I tried at one point to figure out the correct handicap to process it with max/min handicap checking but decided to just give up and set RoyalLeela to do no ranked games with handicap at all. ;) If we need to check and not worry about set_free_handicap in the -1 case I can live with that. The API to my knowledge doesn't tell the client the # of handicap stones assigned is the hassle. |
I'd prefer the server internally figure out the auto handicap, then tell client the real handicap being offered/assigned. |
Ok, it's a feature then =) |
@lemonsqueeze Since #71 was merged is this issue ready to close out? |
Hi roy, yes this one can be closed, thanks. |
One thing i'm confused about: human plays black with handicap, why does gtp2ogs use play commands to send handicap stones to the engine ? Shouldn't it be using set_free_handicap instead ?
I see it's there in the code, but only on loadstate(), so it's practically never used.
I had to put some workarounds in Pachi because of this, otherwise handicap wouldn't get detected correctly. Not sure how other engines cope with this...
The text was updated successfully, but these errors were encountered: