ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/kgsueme/bin/gtp-controller
Revision: 1.4
Committed: Sun May 23 12:36:14 2004 UTC (20 years ago) by pcg
Branch: MAIN
Changes since 1.3: +1 -1 lines
Log Message:
*** empty log message ***

File Contents

# Content
1 #!/opt/bin/perl
2
3 # lots of things have been hardcoded. search for #d# to find the places
4
5 require 5.008;
6
7 use KGS::Protocol;
8 use KGS::Listener::Debug;
9
10 use IO::Socket::INET;
11 use Event;
12
13 use Getopt::Long;
14 use Carp;
15
16 our $VERSION = '0.0'; # be more confident....
17
18 $SIG{QUIT} = sub { Carp::confess "SIGQUIT" };
19
20 my $conn = new KGS::Protocol;
21 my $kgs;
22 my $gtp;
23
24 my $verbose = 1;
25 my $user = "gtpguest";
26 my $pass = undef;
27
28 $Event::DIED = sub {
29 Event::verbose_exception_handler (@_);
30 Event::unloop_all;
31 };
32
33 sub format_user($) {
34 my $format =
35 sprintf "%s|%s|%s",
36 $_[0]{name},
37 $_[0]->flags_string,
38 $_[0]->rank_string;
39
40 $format =~ y/ //d;
41 $format;
42 }
43
44 sub coord($$) {
45 qw(A B C D E F G H J K L M N O P Q R S T U V W X Y Z)[$_[0]] . $_[1];
46 }
47
48 #############################################################################
49
50 package kgs;
51
52 use base KGS::Listener;
53
54 sub new {
55 my $class = shift;
56 my $self = bless { @_ }, $class;
57
58 print STDERR "$0 version $VERSION connecting...\n" if $verbose;
59
60 my $sock = new IO::Socket::INET PeerHost => KGS::Protocol::KGSHOST, PeerPort => KGS::Protocol::KGSPORT
61 or die "connect: $!";
62
63 $sock->blocking (1);
64 $conn->handshake ($sock);
65
66 $self->listen ($conn, "any");
67
68 # Listener for kgs data
69 $self->{w} = Event->io (fd => $sock, poll => 'r', cb => sub {
70 my $len = sysread $sock, my $buf, 16384;
71 if ($len) {
72 $conn->feed_data ($buf);
73 } elsif (defined $len || (!$!{EINTR} and !$!{EAGAIN})) {
74 print STDERR "disconnected\n" if $verbose;
75 Event::unloop;
76 }
77 });
78
79 $conn->login ("gtp-controller $VERSION", $self->{user}, delete $self->{password});
80
81 $self;
82 }
83
84 sub inject_login {
85 my ($self, $msg) = @_;
86
87 print STDERR "login: $msg->{message}\n" if $verbose >= 2;
88
89 $gtp->send ("kgs-login $msg->{message}");
90 }
91
92 sub inject_msg_room {
93 my ($self, $msg) = @_;
94
95 $gtp->send ("kgs-room-chat $msg->{channel} $msg->{message}");
96 }
97
98 sub inject_any {
99 my ($self, $msg) = @_;
100 if ($verbose >= 2) {
101 print STDERR "DEBUG: $msg->{type}#$msg->{channel}";
102 for (sort keys %$msg) {
103 print STDERR" $_<$msg->{$_}>";
104 }
105 print STDERR "\n";
106 }
107 }
108
109 sub inject_upd_rooms {
110 my ($self, $msg) = @_;
111
112 for (@{$msg->{rooms}}) {
113 $gtp->send ("kgs-room-update $_->{channel} $_->{name}");
114 }
115 }
116
117 sub inject_msg_chat {
118 my ($self, $msg) = @_;
119
120 return unless (lc $self->{conn}{name}) eq (lc $msg->{name2});
121
122 $gtp->send ("kgs-user-chat $msg->{name} $msg->{message}");
123 }
124
125 sub inject_new_game {
126 my ($self, $msg) = @_;
127
128 $::lastnew = $msg->{channel};#d#
129 $gtp->send ("kgs-game-new $msg->{id} $msg->{channel}");
130 }
131
132 #############################################################################
133
134 package room;
135
136 use base KGS::Listener::Room;
137
138 sub new {
139 my $class = shift;
140 my $self = $class->SUPER::new (@_);
141
142 $self->listen ($self->{conn});
143 $self->join;
144
145 $self;
146 }
147
148 sub event_join {
149 my $self = shift;
150
151 $self->SUPER::join (@_);
152
153 $self->{timer} = Event->timer (after => 0, interval => 60, cb => sub {
154 $self->req_games;
155 });
156 }
157
158 sub event_update_games {
159 my ($self, $add, $upd, $del) = @_;
160
161 for (@$add, @$upd) {
162 $gtp->send (sprintf "kgs-game-update %d %d %s %s %s %s %d %d %f %d %d %d %d %s",
163 $self->{channel}, $_->{channel},
164 $_->type_char,
165 ::format_user $_->{user1},
166 ::format_user $_->{user2},
167 ::format_user $_->{user3},
168 $_->size,
169 $_->{handicap},
170 $_->{komi},
171 $_->moves,
172 $_->{flags},
173 $_->{observers},
174 $_->{saved},
175 $_->{notes},
176 );
177 }
178
179 for (@$del) {
180 $gtp->send ("kgs-game-delete $self->{channel} $_->{channel}");
181 }
182 }
183
184 sub event_update_users {
185 my ($self, $add, $upd, $del) = @_;
186
187 for (@$add, @$upd) {
188 $gtp->send (sprintf "kgs-user-update %s", ::format_user $_);
189 }
190
191 for (@$del) {
192 $gtp->send (sprintf "kgs-user-remove %s", ::format_user $_);
193 }
194 }
195
196 sub DESTROY {
197 my $self = shift;
198
199 $self->{timer}->cancel if $self->{timer};
200
201 $self->SUPER::DESTROY;
202 }
203
204 #############################################################################
205
206 package game;
207
208 use Gtk2::GoBoard::Constants;
209
210 use base KGS::Listener::Game;
211
212 sub new {
213 my $class = shift;
214 my $self = $class->SUPER::new (@_);
215
216 $self->listen ($self->{conn});
217 $self->join;
218
219 $self;
220 }
221
222 sub event_update_users {
223 return;
224
225 my ($self, $add, $upd, $del) = @_;
226
227 for (@$add, @$upd) {
228 $gtp->send (sprintf "kgs-user-update %s", ::format_user $_);
229 }
230
231 for (@$del) {
232 $gtp->send (sprintf "kgs-user-remove %s", ::format_user $_);
233 }
234 }
235
236 sub inject_resign_game {
237 my ($self, $msg) = @_;
238
239 $gtp->set_gid ($self->{channel});
240 $gtp->send ("play " . (qw(b w))[$msg->{player}] . " resign");
241 }
242
243 sub inject_final_result {
244 my ($self, $msg) = @_;
245
246 $gtp->send (sprintf "kgs-game-score %f %f %f %f %f %f",
247 $_->{whitescore}{territory}, $_->{whitescore}{captures}, $_->{whitescore}{komi},
248 $_->{blackscore}{territory}, $_->{blackscore}{captures}, $_->{blackscore}{komi});
249 }
250
251 sub inject_set_teacher {
252 my ($self, $msg) = @_;
253 }
254
255 sub event_update_game {
256 my ($self) = @_;
257
258 $gtp->set_gid ($self->{channel});
259
260 # timesettings etc.
261 }
262
263 sub event_update_tree {
264 my ($self) = @_;
265
266 $gtp->set_gid ($self->{channel});
267
268 my $path = $self->get_path;
269 my $prev = $self->{prevpath};
270
271 $self->{prevpath} = [ @$path ];
272
273 if (@$prev > 1
274 and @$path > @$prev
275 and (join ":", @$prev) eq (join ":", @$path[0 .. $#$prev])) {
276
277 splice @$path, @prev, $#path, ();
278
279 } else {
280 $gtp->send ("boardsize $path->[0]{rules}{size}");
281 $gtp->send ("komi $path->[0]{rules}{komi}");
282 $gtp->send ("clear_board");
283
284 my $setup = shift @$path;
285 my $handi;
286
287 while (my ($k, $v) = each %$setup) {
288 if ($k =~ /^(\d+),(\d+)$/) {
289 $handi .= " " . ::coord $1, $2;
290 }
291 }
292
293 $gtp->send ("set_free_handicap$handi");
294 }
295
296 for (@$path) {
297 while (my ($k, $v) = each %$_) {
298 if ($k =~ /^(\d+),(\d+)$/) {
299 if ($v->[0] & MARK_MOVE) {
300 if ($v->[0] & MARK_B) {
301 $gtp->send ("play b ". ::coord $1, $2);
302 } else {
303 $gtp->send ("play w ". ::coord $1, $2);
304 }
305 }
306 }
307 }
308 }
309 }
310
311 sub DESTROY {
312 my $self = shift;
313
314 $self->SUPER::DESTROY;
315 }
316
317 #############################################################################
318
319 package gtp;
320
321 use Gtk2::GoBoard::Constants;
322 use KGS::Constants;
323
324 use Fcntl;
325
326 sub new {
327 my $class = shift;
328 bless { @_ }, $class;
329 }
330
331 sub set_fh {
332 my ($self, $rfh, $wfh) = @_;
333
334 $self->{r} = $rfh;
335 $self->{w} = $wfh;
336
337 fcntl $rfh, F_SETFL, O_NONBLOCK;
338
339 my $buf;
340
341 Event->io (fd => $rfh, poll => 'r', cb => sub {
342 my $r = sysread $rfh, $buf, 16384, length $buf;
343
344 if (defined $r and !$r) {
345 die "gtp engine sent EOF, I'm simply dying now, sorry\n";
346 } else {
347 $buf =~ y/\010\015/ /d;
348 $buf =~ s/#[^\012](?=\012)//g; # idiotic part of gtp spec
349 while () {
350 if ($buf =~ s/^([=?])(?:(\d+)\s+)?(.*?)\012\012//s) { # response
351 print STDERR "got response ($1|$2|$3)\n" if $verbose >= 2;
352
353 if (my $cb = delete $self->{waitq}{$2}) {
354 $cb->($1, $3);
355 } else {
356 warn "WARNING: got response if '$1 $2' without outstanding request\n";
357 }
358 } elsif ($buf =~ s/^(?:(\d+)\s+)?([^=?].*?)\012//s) { # command
359 $self->parse_command ($1, $2);
360 } elsif ($buf =~ s/^\s*\012//) {
361 # ignore, idiotic part of gtp spec
362 } else {
363 last;
364 }
365 }
366 }
367 });
368
369 # generate login commands
370 $self->send ("protocol_version", sub { $self->{pversion} = $_[1] });
371 $self->send ("name", sub { $self->{name} = $_[1] });#d#
372 $self->send ("version", sub { $self->{version} = $_[1] });
373 }
374
375 sub run_engine {
376 my ($self, @argv) = @_;
377
378 require IPC::Open2;
379
380 my ($r, $w);
381
382 IPC::Open2::open2 ($r, $w, @argv)
383 or die "unable to start @argv: $!";
384
385 $self->set_fh ($r, $w);
386 }
387
388 sub send {
389 my ($self, $cmd, $cb) = @_;
390
391 # first check for known_command
392
393 my $id = ++$self->{id};
394
395 $cmd =~ y/\015//d;
396 $cmd =~ s/\012/\\n/g;
397
398 $self->{waitq}{$id} = $cb || sub { };
399 print { $self->{w} } "$id $cmd\012";
400 }
401
402 sub reply {
403 my ($self, $id, $response) = @_;
404
405 print { $self->{w} } "=$id $response\012";
406 }
407
408 sub reply_err {
409 my ($self, $id, $response) = @_;
410
411 print { $self->{w} } "?$id $response\012";
412 }
413
414 sub parse_command {
415 my ($self, $id, $cmd) = @_;
416
417 print STDERR "got command $cmd\n" if $verbose >= 2;
418
419 $cmd =~ s/\s+$//;
420
421 if ($cmd eq "kgs-room-list") {
422 # no args, just request all rooms
423 $kgs->send (list_rooms => group => $_) for 0..5;
424 $self->reply ($id, "");
425
426 } elsif ($cmd =~ /^kgs-room-join\s+(\d+)$/) {
427 $kgs->{room}{$1} = new room conn => $kgs->{conn}, channel => $1;
428 $self->reply ($id, "");
429 } elsif ($cmd =~ /^kgs-room-chat\s+(\d+)\s(.*)$/) {
430 $kgs->{room}{$1}->say ($2);
431 $self->reply ($id, "");
432 } elsif ($cmd =~ /^kgs-room-part\s+(\d+)$/) {
433 (delete $kgs->{room}{$1})->part ($2);
434 $self->reply ($id, "");
435
436 } elsif ($cmd =~ /^kgs-game-join\s+(\d+)$/) {
437 $kgs->{game}{$1} = new game conn => $kgs->{conn}, channel => $1;
438 $self->reply ($id, "");
439 } elsif ($cmd =~ /^kgs-game-chat\s+(\d+)\s(.*)$/) {
440 $kgs->{game}{$1}->say ($2);
441 $self->reply ($id, "");
442 } elsif ($cmd =~ /^kgs-game-part\s+(\d+)$/) {
443 (delete $kgs->{game}{$1})->part ($2);
444 $self->reply ($id, "");
445
446 } elsif ($cmd =~ /^kgs-user-chat\s+(\S+)\s+(.*)$/) {
447 $kgs->send (msg_chat =>
448 name => $kgs->{user},
449 name2 => $1,
450 message => $2);
451 $self->reply ($id, "");
452
453 } elsif ($cmd =~ /^kgs-game-new-demo\s+(\d+)\s+(\d+)$/) {
454 my $tid = $conn->alloc_channel;
455 $kgs->send (new_game =>
456 channel => $1,
457 id => $tid,
458 gametype => 0,
459 rules => {
460 ruleset => 0,
461 size => $2,
462 komi => 0,
463 timesys => 0,
464 time => 0,
465 interval => 0,
466 count => 0,
467 });
468 $self->reply ($id, $tid);
469 } elsif ($cmd =~ /^kgs-game-edit\s+(\d+)\s+(.*)$/) {
470 my $gid = $1 || $::lastnew;#d#
471 my $spec = $2;
472 my @tree = ();
473 while ($spec =~ s/^([a-z])(\d+)\s+\+?(\S+)\s*//) { # should use mg
474 my ($x, $y, $spec) = ($1, $2, $3);
475 my $add = $spec !~ s/^-//;
476 $x = index "abcdefghjklmnopqrstuvwxyz", lc $x;
477 $y--;
478
479 if ($spec eq "b") { push @tree, [set_stone => 0, $x, $y];
480 } elsif ($spec eq "w") { push @tree, [set_stone => 1, $x, $y];
481 } elsif ($spec eq "n") { push @tree, [set_stone => 2, $x, $y];
482 } elsif ($spec eq "sb") { push @tree, [mark => $add, MARK_SMALL_B, $x, $y];
483 } elsif ($spec eq "sw") { push @tree, [mark => $add, MARK_SMALL_W, $x, $y];
484 } elsif ($spec eq "sn") { push @tree, [mark => 0, MARK_SMALL_B, $x, $y];
485 } elsif ($spec eq "triangle") { push @tree, [mark => $add, MARK_TRIANGLE, $x, $y];
486 } elsif ($spec eq "square") { push @tree, [mark => $add, MARK_SQUARE, $x, $y];
487 } elsif ($spec eq "circle") { push @tree, [mark => $add, MARK_CIRCLE, $x, $y];
488 } elsif ($spec =~ /label=(\S+)/) { push @tree, [mark => length $1, MARK_LABEL, $x, $y, $1];
489 } elsif ($spec eq "grayed") { push @tree, [mark => $add, MARK_GRAYED, $x, $y];
490 } else {
491 $self->reply_err ($id, "illegal edit spec '$spec'");
492 return;
493 }
494 }
495 $kgs->send (upd_tree =>
496 channel => $gid,
497 tree => \@tree);
498 $self->reply ($id, "");
499
500 } else {
501 $self->reply_err ($id, "illegal command");
502 }
503 }
504
505 sub set_gid {
506 my ($self, $gid) = @_;
507
508 if ($gid != $self->{gid}) {
509 $self->send ("kgs-game-id $gid");
510 $self->{gid} = $gid;
511 }
512 }
513
514 package main;
515
516 sub usage {
517 print STDERR <<EOF;
518 Usage: $0 [options] -- engine engine-args...
519 -u username usernmae to connect
520 -p password optional password to connect (none => guest)
521 -v increase verbosity
522 -q decrease verbosity
523
524 $0 connects to the kiseido go server, starts the named engine
525 and communicates with it using GTP protocol using it's stdin and stdout.
526
527 If no engine is given, uses stdin/stdout itself for communications.
528
529 The engine can optionally act as controller, too, as long as it isn't
530 confused by responses on it's command input stream.
531
532 Command extension used by the controller:
533
534 kgs-login message
535 kgs-room-update <rid> <name> # update room info
536 kgs-room-chat <rid> <user> <message> # somebody says sth.
537 kgs-game-update <rid> <gid> <type> <user1> <user2> \
538 <user3> <size> <handicap> <komi> <moves> \
539 <flags> <observers> <saved> <notes>
540 kgs-game-delete <rid> <gid> # game removed
541 kgs-user-update <rid> <user> # user added/updated
542 kgs-user-delete <rid> <user> # user removed
543
544 kgs-game-resign <gid> <color>
545 kgs-game-score <gid> <w-territory> <w-captures> <w-komi> <b-territory> <b-captures> <b-komi>
546 kgs-game-id <gid> # set id for following gid-less commands
547
548 kgs-user-chat <user> <message> # got private message from user
549 kgs-game-new <tid> <gid> # a new game was created with temporary id <tid>
550 ...
551
552 Commands usable by the client as commands issued to the controller:
553
554 kgs-room-list # ask for roomlist update
555 kgs-room-join <rid> # join given room
556 kgs-room-chat <rid> <message> # say sth. in room
557 kgs-room-part <rid> # leave gives room
558
559 kgs-game-join <gid> # join the given game
560 kgs-game-part <gid> # leave the given game
561 kgs-game-chat <gid> <message> # say sth.
562
563 kgs-user-chat <user> <message> # send private msg to user
564 kgs-game-new-demo <rid> <size> # create new demo game (other agruments might get added)
565 # returns a temporary game id
566 kgs-game-edit <gid> <coord> <editspec> <coord> <editspec>...
567 # editspec is one of
568 # b|w|n set black/white/no stone
569 # sb|sw|sn set/clear black/white/no small stone
570 # [+-]triangle set/clear triangle
571 # [+-]square set/clear square
572 # [+-]circle set/clear circle
573 # [+-]grayed set/clear grayed-flag
574 # label=xyz set label to xyz
575 ...
576
577 EOF
578 exit shift;
579 }
580
581 GetOptions (
582 "u=s" => \$user,
583 "v" => sub { $verbose++ },
584 "q" => sub { $verbose-- },
585 "h" => sub { usage(0) },
586 ) or die usage(1);
587
588 $gtp = new gtp;
589
590 if (@ARGV) {
591 $gtp->run_engine (@ARGV);
592 } else {
593 $gtp->set_fh (\*STDIN, \*STDOUT);
594 }
595
596 $kgs = new kgs user => $user, password => $pass;
597
598 Event::loop;
599
600 1;
601
602