I am a Go player of no particular ability, but the game has been long part of my life. My brother taught me to play when I was in elementary school. I probably haven’t improved much since then. Nevertheless it has influenced my imagination, and found its way into my art.
Do I want to draw out a 19x19 board by hand? I do not. That’s what computers are for.
Wikipedia has records of many famous games in a text format peculiar to the site, which the wiki engine converts into illustrations. It would be nice to feed one of those text strings into a program, and tell the program to turn it into a graphic that I can use in my work.
If you go to an article about Go on Wikipedia and click the Edit link, you will see examples of such strings. This:
{{Go board 5x5
| ul| u| u| u| ur
| l| b| w| | r
| b| c| b1| w| r
| l| b| w| | r
| dl| d| d| d| dr|32}}
Renders as this:
If you control-click on any point in the rendering (not this one, it’s a screenshot) and open it in a new tab, you’ll see that it’s a 32x32 pixel PNG file. The b1
file looks like this:
The wiki engine substitutes pipe-delimited markup for various PNG files and out comes a Go board. ul
is “up left,” u
is “up,” ur
is up right, and so on around the edge of the board. b
is a black stone, w
a white stone, c
an indicator circle, b1
a black stone with a “1” on it. A blank is an open intersection. 32 tells the engine to use the 32-pixel PNGs. There are also 30- and 22-pixel images for smaller boards.
Looking at another example, this:
{{Goban 9x9
| ul| u| u| u| u| u| u| u| u
| l| | w| | | | | |
| l| w| b| w| | | | |
| l| w| b1| b3| w4| | | |
| l| | w2| b5| b7| w8| | |
| l| | | w6| b9| | | |
| l| | | | 10| | | |
| l| | | | | | | |
| l| | | | | | | | |22}}
Renders as this:
How did the engine know that “10” was a white stone? Fortunately for the engine, black always plays first, so any even-numbered play is white. It also seems to understand that Go board
and Goban
both indicate “use Go board rendering mode.” How deep into that string is it looking? Does Go eat beans
work?
This, from a subset of moves in Game #1 of AlphaGo versus Lee Sedol:
{{Goban
<!--A B C D E F G H J K L M N O P Q R S T-->
| | | | | | | | | | w| | | | | | | | | <!--19-->
| | | | | | | | | w| b| w| | b| b| b|67|68| w| <!--18-->
| | |16| | | b| | | w| b| | b|79| w|78| w| w| b| <!--17-->
| | | | w| | | w| | w| b| | b| |01| w|36| b|70| <!--16-->
| | | | | | | | | w| b| w| b| | b| w| |14| b| <!--15-->
| | | w| | | | w| b| b| w| w| b| b| | w|13|10|12|69<!--14-->
| | | | | | | w|52| | w| b| w| | | w| b| |21| <!--13-->
|72|74| | | |86|85|51| w| w| b| w|00| | w| b|15|08| <!--12-->
|71|66|50| | |64|75| w| b| | b| b| w| | | b| |11| <!--11-->
|73|65|82|54| |76| b| b| b| b| | | w| | |03|02| | <!--10-->
| | | | |63|77| | | | | b| w| | |07|06|04| | <!--9-->
|83|19|45| b| | | | | | w| w| b| w| |49|05| b|09| <!--8-->
| |18| w| b| | | | | b| | | b| w|80|48|81| | | <!--7-->
|46|17| b| w| b| | | | | | | b| w| |29|26|43|41| <!--6-->
| |20| w| w| | | | | | | | b| w| |27|23| w|42|44<!--5-->
|62|58|61| w| b| w| b| | | x| | b| w| |31|30|28| | <!--4-->
|84|55|56|59| | b| w| | | |25| b| w| |35| b|32| | <!--3-->
| |60|57|53| | b| | | |24| b| w|22| |33|34|37|38| <!--2-->
| | | | | | | | | | |47| | | | |39| |40| <!--1-->
<!--A B C D E F G H J K L M N O P Q R S T-->|20}}
Renders as this:
The engine can infer which spaces are at the edge of the board and use the appropriate image without an explicit ul
or similar token. Does it include them unless they’re explicitly declared, as in the previous example? If so how do you depict the middle of the board? Also HTML-style comments are allowed. Column “I” has been eliminated for some reason, which maybe doesn’t matter since the engine isn’t using it anyway, but then why bother dropping it? This is getting complicated. Is the format documented somewhere?
Yes. Sort of. A note from 2005 says that a manual is in progress. There is a one-letter invocation spec and a two-letter invocation spec. The example for the latter is broken. If I want to plug any old Wikipedia {{Goban}}
tag into my renderer, it will have to be quite robust. Or I will have to clean the tag manually first. No thank you.1
Most digitally recorded Go games are stored in a file format called SGF, for “Smart Game Format.” The SGF file for Hane Yasumasa versus Yamashiro Hiroshi for Game #19 of the Okan Title in 1978 looks like this:
(;
EV[19th Okan Title]
PB[Yamashiro Hiroshi]
BR[Okan]
PW[Hane Yasumasa]
WR[8p]
KM[5.5]
RE[W+R]
DT[1978-10-26]
;B[qd];W[qp];B[dq];W[cc];B[co];W[oc];B[oq];W[jq];B[md];W[oe]
;B[pf];W[pi];B[of];W[ne];B[pc];W[ob];B[kd];W[le];B[ke];W[kf]
;B[jf];W[ld];B[lf];W[kg];B[me];W[lc];B[mc];W[kc];B[jc];W[mf]
;B[lg];W[mg];B[lh];W[mb];B[kh];W[pq];B[op];W[po];B[lq];W[eq]
;B[dp];W[eo];B[jg];W[ep];B[dn];W[fm];B[qk];W[ok];B[qi];W[ko]
;B[mo];W[qh];B[ph];W[qj];B[ri];W[pe];B[qe];W[pg];B[oh];W[oi]
;B[rh];W[qg];B[rg];W[og];B[nh];W[qf];B[rf];W[nf];B[jb];W[lb]
;B[pp];W[qq];B[rn];W[qo];B[pk];W[om];B[mm];W[mk];B[oo];W[qm]
;B[rm];W[rk];B[qn];W[ql];B[pn];W[ro];B[pj];W[oj];B[rj];W[pl]
;B[qj];W[ln];B[nl];W[ol];B[mn];W[mh];B[ec];W[ce];B[ee];W[df]
;B[ef];W[db];B[eb];W[rl];B[on];W[or];B[nr];W[pr];B[jr];W[ir]
;B[kr];W[iq];B[dh];W[dg];B[eh];W[qb];B[bh];W[rc];B[rd];W[bg]
;B[jn];W[el];B[lj];W[mj];B[jo];W[kp];B[gl];W[gk];B[fk];W[hl]
;B[hk];W[gj];B[fl];W[gm];B[il];W[hm];B[hj];W[lm];B[kq];W[im]
;B[bb];W[ea];B[bd];W[cd];B[cb];W[dc];B[bc];W[ca];B[ba];W[be]
;B[da];W[mr];B[ns];W[ca];B[ad];W[ab];B[da];W[nq];B[mq];W[ca]
;B[ll];W[da];B[jp];W[jl];B[kl];W[jm];B[km];W[gi];B[ek];W[ii]
;B[em];W[dr];B[cr];W[do];B[dl];W[er];B[ip];W[br];B[bq];W[cs]
;B[cq];W[cn];B[bo];W[en];B[dm];W[bi];B[ah];W[ch];B[ci];W[cg]
;B[bj];W[di];B[cj];W[gb];B[gq];W[gr];B[hp];W[hr])
What it lacks in readability it makes up for in regularity. Opening and closing parentheses delimit the file. Semicolons are beginning-of-line markers. The first line is metadata. Columns and rows, in that order, are letter-denominated. i
exists as a valid denomination. Whitespace, including spaces, is insignificant, though preserved between brackets in metadata. There is a specification.
Unfortunately this is a list of plays. The Wikipedia Go format is a hairball but it describes a board state, which is what I want for my drawing. My renderer will have to execute life-and-death analysis so that captured stones are removed from the board. Checking for death on the board may be challenging. Analyzing the Go renderer in the guts of the PHP that runs the Wiki engine may hasten my literal death. The former it is.
First I’ll have Rust consume an SGF file. By way of preliminary cleanup I’ll normalize out the spaces and EOLs, as they’re not semantic and I’ll throw out the metadata anyway.
use std::fs;
use stringr;
fn main() {
let sgf = load_sgf("Okan-1978.sgf");
println!("{}", sgf);
}
fn load_sgf(f: &str) -> String {
let sgf = fs::read_to_string(f).expect("file failed to load");
stringr::remove_whitespace(&sgf)
}
Next I’ll isolate the plays and convert them to a vector of strings.
fn sgf_to_play_strings(sgf: &str) -> Vec<String> {
// slice off parentheses and split on semicolons
let mut str_plays = sgf[1..sgf.len() - 1].split(';');
// semicolons are beginning of line delimiters, first is blank, second is metadata
str_plays.next();
str_plays.next();
str_plays.map(str::to_string).collect()
}
That works:
["B[qd]", "W[qp]", "B[dq]", "W[cc]", "B[co]", "W[oc]", "B[oq]", "W[jq]", "B[md]", "W[oe]", "B[pf]", "W[pi]", "B[of]", "W[ne]", "B[pc]", "W[ob]", "B[kd]", "W[le]", "B[ke]", "W[kf]", "B[jf]", "W[ld]", "B[lf]", "W[kg]", "B[me]", "W[lc]", "B[mc]", "W[kc]", "B[jc]", "W[mf]", "B[lg]", "W[mg]", "B[lh]", "W[mb]", "B[kh]", "W[pq]", "B[op]", "W[po]", "B[lq]", "W[eq]", "B[dp]", "W[eo]", "B[jg]", "W[ep]", "B[dn]", "W[fm]", "B[qk]", "W[ok]", "B[qi]", "W[ko]", "B[mo]", "W[qh]", "B[ph]", "W[qj]", "B[ri]", "W[pe]", "B[qe]", "W[pg]", "B[oh]", "W[oi]", "B[rh]", "W[qg]", "B[rg]", "W[og]", "B[nh]", "W[qf]", "B[rf]", "W[nf]", "B[jb]", "W[lb]", "B[pp]", "W[qq]", "B[rn]", "W[qo]", "B[pk]", "W[om]", "B[mm]", "W[mk]", "B[oo]", "W[qm]", "B[rm]", "W[rk]", "B[qn]", "W[ql]", "B[pn]", "W[ro]", "B[pj]", "W[oj]", "B[rj]", "W[pl]", "B[qj]", "W[ln]", "B[nl]", "W[ol]", "B[mn]", "W[mh]", "B[ec]", "W[ce]", "B[ee]", "W[df]", "B[ef]", "W[db]", "B[eb]", "W[rl]", "B[on]", "W[or]", "B[nr]", "W[pr]", "B[jr]", "W[ir]", "B[kr]", "W[iq]", "B[dh]", "W[dg]", "B[eh]", "W[qb]", "B[bh]", "W[rc]", "B[rd]", "W[bg]", "B[jn]", "W[el]", "B[lj]", "W[mj]", "B[jo]", "W[kp]", "B[gl]", "W[gk]", "B[fk]", "W[hl]", "B[hk]", "W[gj]", "B[fl]", "W[gm]", "B[il]", "W[hm]", "B[hj]", "W[lm]", "B[kq]", "W[im]", "B[bb]", "W[ea]", "B[bd]", "W[cd]", "B[cb]", "W[dc]", "B[bc]", "W[ca]", "B[ba]", "W[be]", "B[da]", "W[mr]", "B[ns]", "W[ca]", "B[ad]", "W[ab]", "B[da]", "W[nq]", "B[mq]", "W[ca]", "B[ll]", "W[da]", "B[jp]", "W[jl]", "B[kl]", "W[jm]", "B[km]", "W[gi]", "B[ek]", "W[ii]", "B[em]", "W[dr]", "B[cr]", "W[do]", "B[dl]", "W[er]", "B[ip]", "W[br]", "B[bq]", "W[cs]", "B[cq]", "W[cn]", "B[bo]", "W[en]", "B[dm]", "W[bi]", "B[ah]", "W[ch]", "B[ci]", "W[cg]", "B[bj]", "W[di]", "B[cj]", "W[gb]", "B[gq]", "W[gr]", "B[hp]", "W[hr]"]
The next steps are to convert those strings into something meaningful in Rust. I envision a vector of Play
s. I’ll start here:
#[derive(Debug)]
struct Play {
sgf_token: String,
}
And with that:
fn play_strings_to_data(play_strings: Vec<String>) -> Vec<Play> {
let mut plays: Vec<Play> = vec![];
for ps in play_strings.iter() {
plays.push(Play { sgf_token: ps.to_string() });
}
plays
}
Sure enough:
[Play { sgf_token: "B[qd]" }, Play { sgf_token: "W[qp]" }, Play { sgf_token: "B[dq]" }, Play { sgf_token: "W[cc]" }, Play { sgf_token: "B[co]" }, Play { sgf_token: "W[oc]" }, Play { sgf_token: "B[oq]" }, Play { sgf_token: "W[jq]" }, Play { sgf_token: "B[md]" }, Play { sgf_token: "W[oe]" }, Play { sgf_token: "B[pf]" }, Play { sgf_token: "W[pi]" }, Play { sgf_token: "B[of]" }, Play { sgf_token: "W[ne]" }, Play { sgf_token: "B[pc]" }, Play { sgf_token: "W[ob]" }, Play { sgf_token: "B[kd]" }, Play { sgf_token: "W[le]" }, Play { sgf_token: "B[ke]" }, // etc.
Modeling the player shouldn't be too hard. Rust can do this with an algebraic type.
#[derive(Debug)]
struct Play {
sgf_token: String,
player: Player,
}
#[derive(Debug)]
enum Player {
White,
Black,
}
Determining the player is as easy as picking off the first character of each sgf_token
and mapping it accordingly. That could be a one-liner with slice notation, but it’s past time to start introducing some error checking.
fn determine_player(sgf_token: String) -> Player {
let player_char = sgf_token.chars().nth(1);
match player_char {
Some('B') => return Player::Black,
Some('W') => return Player::White,
_ => panic!("player_char `{:?}` is neither B nor W", player_char),
}
}
If for no other reason to save me from myself.
thread 'main' panicked at 'player_char `Some('[')` is neither B nor W', src/main.rs:54:10
Ahem.
let player_char = sgf_token.chars().nth(0);
And,
// from play_strings_to_data
for ps in play_strings.iter() {
plays.push(Play {
sgf_token: ps.to_string(),
player: determine_player(ps.to_string()),
});
}
That's better.
[Play { sgf_token: "B[qd]", player: Black }, Play { sgf_token: "W[qp]", player: White }, Play { sgf_token: "B[dq]", player: Black }, Play { sgf_token: "W[cc]", player: White }, Play { sgf_token: "B[co]", player: Black }, Play { sgf_token: "W[oc]", player: White }, Play { sgf_token: "B[oq]", player: Black }, Play { sgf_token: "W[jq]", player: White }, Play { sgf_token: "B[md]", player: Black }, Play { sgf_token: "W[oe]", player: White }, // etc.
Translating the position of each play into data is going to be trickier. While the above result is regular enough to pick apart slice by slice, passing is a legal move in Go,2 and SGF represents a pass as either []
or [tt]
, the latter which is a play at column 20, row 20, hence not on a 19x19 board. If the value comes back as an empty string or tt
the move is a pass. Otherwise I can slice the column and row and represent it numerically.
The interesting problem is how to represent a pass in Rust. SGF does it with an out-of-range value, tt
, or the equivalent of a null []
, which is illegal in Rust, because nulls literally caused billions of dollars of damage. Rust, however, has option
s. If the move is a play, Rust can return a Position
that looks like this:
struct Position {
column: u8,
row: u8,
}
I’ll add this to the struct
for Play
:
position: Option<Position>,
Then the declaration for determine_position()
will look like this:
fn determine_position(sgf_token: String) -> Option<Position> {
The value for position
will either be Some(Position)
for a play or None
for a pass. This is cool. I think it’s cool.
fn determine_position(sgf_token: String) -> Option<Position> {
let play = &sgf_token[2..sgf_token.len() - 1];
match play {
"" => {
None
}
"tt" => {
None
}
_ => {
Some(Position { column: 0, row: 0 })
}
}
}
That play at 0,0 is just to get the ball rolling.
[Play { sgf_token: "B[qd]", player: Black, position: Some(Position { column: 0, row: 0 }) }, Play { sgf_token: "W[qp]", player: White, position: Some(Position { column: 0, row: 0 }) }, // etc.
Ball is rolling. Time to extract numerical values from the column-row representation. Since the ASCII value of “a” is 97, I can cast the character-ordinal as an integer and subtract 97 for a numerical result from 0 to 18. Yes, I’m going to zero-index the goban.
fn determine_position(sgf_token: String) -> Option<Position> {
let play = &sgf_token[2..sgf_token.len() - 1];
match play {
"" => {
None
}
"tt" => {
None
}
_ => {
lazy_static! {
static ref RE: Regex = Regex::new(r"^[a-t][a-t]$").unwrap();
}
assert!(RE.is_match(play), "play `{}` is messed up", play);
let column = play.chars().nth(0).unwrap() as i32 - 97;
assert!( (column >= 0) && (column <= 18), "column value {} for token {} is out of range 0-18", column, play);
let row = play.chars().nth(1).unwrap() as i32 - 97;
assert!( (row >= 0) && (row <= 18), "row value {} for token {} is out of range 0-18", row, play);
Some(Position { column: column as usize, row: row as usize })
}
}
}
That hellscape inside of lazy_static!
is a regular expression that matches to a two-character string in which each character is in the range of a
to t
. The following assert!
should prevent the error I ran into above, trying to assign [
to a player. lazy_static!
prevents the Regex
from compiling over and over again.
Boy howdy:
[Play { sgf_token: "B[qd]", player: Black, position: Some(Position { column: 16, row: 3 }) }, Play { sgf_token: "W[qp]", player: White, position: Some(Position { column: 16, row: 15 }) }, Play { sgf_token: "B[dq]", player: Black, position: Some(Position { column: 3, row: 16 }) }, Play { sgf_token: "W[cc]", player: White, position: Some(Position { column: 2, row: 2 }) }, // etc.
Position cc
is indeed Column 2, Row 2 on a zero index. This checks out. Nevertheless I want to make sure that those passes are working. I wrote an SGF file with a terrible game of Go:
(;
whole lotta passes
;B[tt];W[];B[tt];W[];B[aa];W[bc];B[tt];W[])
My program loaded it and behaved correctly:
[Play { sgf_token: "B[tt]", player: Black, position: None }, Play { sgf_token: "W[]", player: White, position: None }, Play { sgf_token: "B[tt]", player: Black, position: None }, Play { sgf_token: "W[]", player: White, position: None }, Play { sgf_token: "B[aa]", player: Black, position: Some(Position { column: 0, row: 0 }) }, Play { sgf_token: "W[bc]", player: White, position: Some(Position { column: 1, row: 2 }) }, Play { sgf_token: "B[tt]", player: Black, position: None }, Play { sgf_token: "W[]", player: White, position: None }]
Time to make a board. I’m going to pick the obvious solution and make it a vector of vectors. Rust vectors must contain items of identical types so I’ll make a another struct to contain the data.
struct Point {
column: u8,
row: u8,
is_star: bool,
stone: Option<Stone>,
}
#[derive(PartialEq)]
enum Stone {
Black,
White,
}
Loading the column and row number into the struct
may seem redundant because the vectors are indexed, but in fact it can be handy because the contents of vectors aren’t able to describe where they’re located relative to the rest of the vector. is_star
tracks whether the position is a star point. Similar to the way I handled passes above, the stone
will either be Some(Black)
, Some(White)
, or None
.
#[derive(PartialEq)]
allows me to make an equality comparison that enables this:
impl Stone {
fn other(self) -> Stone {
if self == Stone::Black {
Stone::White
} else {
Stone::Black
}
}
}
Which means a stone
of type Stone
with a value of Black
can call stone.other()
and return White
and vice-versa. Mostly I just wanted to see if I could write that.
Rust can build a 19x19 vector of vectors full of default points in one line if you tell Point
to #[derive(Clone)]
:
vec![vec![Point { column: 0, row: 0, is_star: false, stone: None }; 19]; 19]
Is this a row of columns or a column of rows? Rust doesn’t care, I just have to pick one and use my brain consistently. But if I’m going to access points with something like board_state[4][12]
as if the first number was x and the second y, columns are increasing along x, which means that I’ll be accessing by [column][row]
and therefore board_state
is 19 columns of 19 rows, which a row of columns.3
fn initialize_board() -> Vec<Vec<Point>> {
let mut board = vec![vec![Point { column: 0, row: 0, is_star: false, stone: None }; 19]; 19];
let stars = [3, 9, 15];
for x in 0..19 {
for y in 0..19 {
board[x][y].column = x;
board[x][y].row = y;
if stars.contains(&x) && stars.contains(&y) { board[x][y].is_star = true; }
}
}
board
}
So far, so good:
[[Point { column: 0, row: 0, is_star: false, stone: None }, Point { column: 0, row: 1, is_star: false, stone: None }, Point { column: 0, row: 2, is_star: false, stone: None }, Point { column: 0, row: 3, is_star: false, stone: None }, // etc. for 361 points
Next I’ll write a naive mapping of plays to the board. This won’t check for death and later writes of stone
may squish earlier ones. But I want to get to the fun part, output, which fun aside will make it easier to write the non-naive mapping.
First I’ll write an implementation for Player
that maps player color to stone color:
impl Player {
fn stone(self) -> Stone {
if self == Player::White {
Stone::White
} else {
Stone::Black
}
}
}
With that, the naive transfer from plays to board straight is straightforward:
fn map_plays_to_board(plays: Vec<Play>, mut board: Vec<Vec<Point>>) -> Vec<Vec<Point>> {
for play in plays {
match play.position {
Some(_) => {
let this_position = play.position.unwrap();
board[this_position.column][this_position.row].stone = Some(play.player.stone());
}
None => (),
}
}
board
}
Naive board state achieved:
[[Point { column: 0, row: 0, is_star: false, stone: None }, Point { column: 0, row: 1, is_star: false, stone: Some(White) }, Point { column: 0, row: 2, is_star: false, stone: None }, Point { column: 0, row: 3, is_star: false, stone: Some(Black) }, Point { column: 0, row: 4, is_star: false, stone: None }, Point { column: 0, row: 5, is_star: false, stone: None }, // etc. for 361 points
Time for output, starting with text representation. If I had chosen a column of rows it would be easy, just concatenate each row and print it as a line. But no, I have to loop across consecutive columns instead (note the ordering of [j][i]
):
fn render_board_as_text(board: Vec<Vec<Point>>) {
for i in 0..19 {
for j in 0..19 {
let mut p = '.';
if board[j][i].is_star { p = 'x'; }
match board[j][i].stone {
Some(Stone::Black) => p = 'B',
Some(Stone::White) => p = 'W',
None => (),
}
print!("{}", p);
}
print!("{}", "\n");
}
}
Could be prettier, but I’ll take it:
.BWWW..............
WBBWB.W..B.WW.W.W..
.BWWB....BWWB.WB.W.
BBWx.....xBWB..xBB.
.WW.B.....BWBWWWB..
...WB....BWBWWBBWB.
.WWW.....BWBW.WWWB.
BBWBB.....BBWBBBWB.
.WBW..W.W.....WWBB.
.BBx..WB.x.BW.WBBB.
....BBWB....W.WBBW.
...BWBBWBWBB.BWWWW.
...BBWWWWWBWB.W.WB.
..WBW....B.WB.BBBB.
.BBWW....BW.B.BWWW.
...BW..BBBW...BBW..
.BBBW.B.WWBBBWBWW..
.WBWW.WWWBB.WBWW...
..W..........B.....
The same game loaded into qGo proves that the above is naively correct:4
You can see the naivete in the difference between the open points at P14 and Q14 in qGo and the black stones at [14,5] and [15,5] in the text rendering. qGo knows how to kill those stones. I’ll implement that later. Time to draw. The choice of a row of columns might have been tough for text rendering but it’s perfect for graphics. Note the [x][y]
order.
fn initialize_board() -> Vec<Vec<Point>> {
let mut board = vec![vec![Point { column: 0, row: 0, is_star: false, stone: None }; 19]; 19];
let stars = [3, 9, 15];
for x in 0..19 {
for y in 0..19 {
board[x][y].column = x;
board[x][y].row = y;
if stars.contains(&x) && stars.contains(&y) { board[x][y].is_star = true; }
}
}
board
}
And then the drawing itself.
fn render_board(board: Vec<Vec<Point>>) {
let stone_size = 30;
let inset = 40;
let board_length = (stone_size * 18) + (inset * 2);
let mut canvas = Canvas::new(board_length, board_length);
let wood = Drawing::new()
.with_shape(Shape::Rectangle { width: board_length, height: board_length })
.with_style(Style::filled(rgb::RGB { r: 230, g: 200, b: 100 }));
canvas.display_list.add(wood);
for n in 0..19 {
let v_line = Drawing::new()
.with_shape(Shape::Rectangle { width: 2, height: (stone_size * 18) + 2 })
.with_xy((inset + (stone_size * n)) as f32, inset as f32)
.with_style(Style::filled(Color::black()));
canvas.display_list.add(v_line);
}
for n in 0..19 {
let h_line = Drawing::new()
.with_shape(Shape::Rectangle { width: (stone_size * 18) + 2, height: 2 })
.with_xy(inset as f32, (inset + (stone_size * n)) as f32)
.with_style(Style::filled(Color::black()));
canvas.display_list.add(h_line);
}
for i in 0..18 {
for j in 0..18 {
let point = &board[i][j];
if point.is_star {
let star_point = Drawing::new()
.with_shape(Shape::Circle { radius: 4 })
.with_xy( (inset + 1 + (stone_size * point.column as u32)) as f32, (inset + 1 + (stone_size * point.row as u32)) as f32)
.with_style(Style::filled(Color::black()));
canvas.display_list.add(star_point);
}
match point.stone {
Some(Stone::Black) => {
let stone = Drawing::new().with_shape(Shape::Circle {radius: stone_size / 2})
.with_style(Style::filled(Color::black()))
.with_xy((inset + (stone_size * point.column as u32)) as f32, (inset + (stone_size * point.row as u32)) as f32);
canvas.display_list.add(stone);
}
Some(Stone::White) => {
let stone = Drawing::new().with_shape(Shape::Circle {radius: stone_size / 2})
.with_style(Style::filled(rgb::RGB { r: 255, g: 255, b: 255 }))
.with_xy((inset + (stone_size * point.column as u32)) as f32, (inset + (stone_size * point.row as u32)) as f32);
canvas.display_list.add(stone);
}
_ => (),
}
}
}
render::save(&canvas, "/path/to/board.svg", SvgRenderer::new(),).expect("Failed to save");
}
And there you have it (it’s not actually an SVG, I converted it for Substack):
Rust’s draw
crate doesn’t have great documentation so those lines are actually very thin rectangles, and the correct arguments for the Style::new
function that would let me both fill and stroke the stones elude me. Graphics instructions are verbose in most cases but the above is downright prolix. Unfortunately the lack of implementations in the crate have made any component of Drawing
go out of scope if it so much as leaves its block, so a lot of code has to get duplicated. I regard the code in its current state as an acceptable second draft. Kill checks and cleanup are next, for Part 2.
Why go into this, knowing that my readers come here for art commentary, and the few reading this paragraph hit the End
key to get to the point already? It’s to demonstrate why my Humbug Detection Meter so often slams rightward until it emits smoke in regards to the art world. It probably looks like arrogance, but it’s nothing more than a true-ish observation of what the Visual Art Smart Set is typically trying to pass off as courage, insight, clarity, erudition, and candor. And not cynically - they’re honestly maxed out. I see through the broadcloth and gingham whether or no, sang Whitman. But I myself am a mere flicker of light.
Another possibility would be to create a draft Wikipedia page, load a {{Goban}}
tag with the board I want to render into the Edit field, tweak it as needed, preview it, let the engine do the rendering for me, and screenshot the result. An ugly and limited solution, and you should never disdain ugly and limited solutions out-of-hand. But I want it to turn out in the style of the new paintings that I’m working on so I can incorporate them more easily.
Officially, the game ends with two consecutive passes, but those moves are not made part of the record.
You’re unusual if your instincts about that sort of thing are correct. Mine often aren’t. A lot of people think that Delicious Line was an elaborate WordPress installation but I actually coded the entire thing from scratch from Python, XSLT, and PostGIS. The last item taught me that you can’t rely on users to do their own geographic input. Without looking it up: you know what an X-Y coordinate graph looks like, and you know what latitude and longitude are. Is longitude X or Y? I’ll entertain answers in the comments.
Aren’t we all, on a good day.
I Made Rust Draw a Goban, Part One
Some of the would-be visual art smart set may be honestly deluded, but never underestimate the incidence of opportunism, not to mention fashion victimhood and the fear of being "out of it."