1 //-----------------------------------------------------------------------------
2 /** @file libpentobi_gtp/GtpEngine.cpp
3     @author Markus Enzenberger
4     @copyright GNU General Public License version 3 or later */
5 //-----------------------------------------------------------------------------
6 
7 #include "GtpEngine.h"
8 
9 #include <fstream>
10 #include <memory>
11 #include "libboardgame_base/CpuTime.h"
12 #include "libboardgame_base/Log.h"
13 #include "libboardgame_base/RandomGenerator.h"
14 #include "libboardgame_base/SgfUtil.h"
15 #include "libboardgame_base/TreeReader.h"
16 #include "libpentobi_base/MoveMarker.h"
17 #include "libpentobi_base/PentobiTreeWriter.h"
18 
19 namespace libpentobi_gtp {
20 
21 using namespace std;
22 using libboardgame_base::RandomGenerator;
23 using libboardgame_base::TreeReader;
24 using libboardgame_base::get_last_node;
25 using libboardgame_gtp::Failure;
26 using libpentobi_base::Grid;
27 using libpentobi_base::Move;
28 using libpentobi_base::MoveList;
29 using libpentobi_base::MoveMarker;
30 using libpentobi_base::PentobiTreeWriter;
31 using libpentobi_base::Point;
32 using libpentobi_base::SgfNode;
33 
34 //-----------------------------------------------------------------------------
35 
GtpEngine(Variant variant)36 GtpEngine::GtpEngine(Variant variant)
37     : m_game(variant)
38 {
39     add("all_legal", &GtpEngine::cmd_all_legal);
40     add("clear_board", &GtpEngine::cmd_clear_board);
41     add("cputime", &GtpEngine::cmd_cputime);
42     add("final_score", &GtpEngine::cmd_final_score);
43     add("loadsgf", &GtpEngine::cmd_loadsgf);
44     add("point_integers", &GtpEngine::cmd_point_integers);
45     add("move_info", &GtpEngine::cmd_move_info);
46     add("p", &GtpEngine::cmd_p);
47     add("param_base", &GtpEngine::cmd_param_base);
48     add("play", &GtpEngine::cmd_play);
49     add("savesgf", &GtpEngine::cmd_savesgf);
50     add("set_game", &GtpEngine::cmd_set_game);
51     add("set_random_seed", &GtpEngine::cmd_set_random_seed);
52     add("showboard", &GtpEngine::cmd_showboard);
53     add("undo", &GtpEngine::cmd_undo);
54 }
55 
board_changed()56 void GtpEngine::board_changed()
57 {
58     if (m_show_board)
59         LIBBOARDGAME_LOG(get_board());
60 }
61 
cmd_all_legal(Arguments args,Response & response)62 void GtpEngine::cmd_all_legal(Arguments args, Response& response)
63 {
64     auto& bd = get_board();
65     auto moves = make_unique<MoveList>();
66     auto marker = make_unique<MoveMarker>();
67     bd.gen_moves(get_color_arg(args), *marker, *moves);
68     for (auto mv : *moves)
69         response << bd.to_string(mv, false) << '\n';
70 }
71 
cmd_clear_board()72 void GtpEngine::cmd_clear_board()
73 {
74     m_game.init();
75     board_changed();
76 }
77 
cmd_cputime(Response & response)78 void GtpEngine::cmd_cputime(Response& response)
79 {
80     auto time = libboardgame_base::cpu_time();
81     if (time < 0)
82         throw Failure("cannot determine cpu time");
83     response << time;
84 }
85 
cmd_final_score(Response & response)86 void GtpEngine::cmd_final_score(Response& response)
87 {
88     auto& bd = get_board();
89     if (get_nu_players(bd.get_variant()) > 2)
90     {
91         for (Color c : bd.get_colors())
92             response << bd.get_points(c) << ' ';
93     }
94     else
95     {
96         auto score = bd.get_score_twoplayer(Color(0));
97         if (score > 0)
98             response << "B+" << score;
99         else if (score < 0)
100             response << "W+" << (-score);
101         else
102             response << "0";
103     }
104 }
105 
cmd_g(Response & response)106 void GtpEngine::cmd_g(Response& response)
107 {
108     genmove(get_board().get_effective_to_play(), response);
109 }
110 
cmd_genmove(Arguments args,Response & response)111 void GtpEngine::cmd_genmove(Arguments args, Response& response)
112 {
113     genmove(get_color_arg(args), response);
114 }
115 
cmd_loadsgf(Arguments args)116 void GtpEngine::cmd_loadsgf(Arguments args)
117 {
118     args.check_size_less_equal(2);
119     auto file = args.get<string>(0);
120     unsigned move_number = 0;
121     if (args.get_size() == 2)
122         move_number = args.get_min<unsigned>(1, 1);
123     try
124     {
125         TreeReader reader;
126         reader.read(file);
127         auto tree = reader.get_tree_transfer_ownership();
128         m_game.init(tree);
129         const SgfNode* node = nullptr;
130         if (move_number > 0)
131             node = m_game.get_tree().get_node_before_move_number(move_number - 1);
132         if (node == nullptr)
133             node = &get_last_node(m_game.get_root());
134         m_game.goto_node(*node);
135         board_changed();
136     }
137     catch (const runtime_error& e)
138     {
139         throw Failure(e.what());
140     }
141 }
142 
143 /** Return move info of a move given by its integer or string representation. */
cmd_move_info(Arguments args,Response & response)144 void GtpEngine::cmd_move_info(Arguments args, Response& response)
145 {
146     auto& bd = get_board();
147     Move mv;
148     try
149     {
150         mv = Move(args.get<Move::IntType>());
151     }
152     catch (const Failure&)
153     {
154         if (! bd.from_string(mv, args.get<string>()))
155         {
156             ostringstream msg;
157             msg << "invalid argument '" << args.get()
158                 << "' (expected move or move ID)";
159             throw Failure(msg.str());
160         }
161     }
162     auto& geo = bd.get_geometry();
163     auto piece = bd.get_move_piece(mv);
164     auto& info_ext_2 = bd.get_move_info_ext_2(mv);
165     response
166         << "\n"
167         << "ID:     " << mv.to_int() << "\n"
168         << "Piece:  " << static_cast<int>(piece.to_int())
169         << " (" << bd.get_piece_info(piece).get_name() << ")\n"
170         << "Points:";
171     for (auto p : bd.get_move_points(mv))
172         response << ' ' << geo.to_string(p);
173     response
174         << "\n"
175         << "BrkSym: " << info_ext_2.breaks_symmetry << "\n"
176         << "SymMv:  " << bd.to_string(info_ext_2.symmetric_move);
177 }
178 
cmd_p(Arguments args)179 void GtpEngine::cmd_p(Arguments args)
180 {
181     play(get_board().get_to_play(), args, 0);
182 }
183 
cmd_param_base(Arguments args,Response & response)184 void GtpEngine::cmd_param_base(Arguments args, Response& response)
185 {
186     if (args.get_size() == 0)
187         response
188             << "accept_illegal " << m_accept_illegal << '\n'
189             << "resign " << m_resign << '\n';
190     else
191     {
192         args.check_size(2);
193         auto name = args.get(0);
194         if (name == "accept_illegal")
195             m_accept_illegal = args.get<bool>(1);
196         else if (name == "resign")
197             m_resign = args.get<bool>(1);
198         else
199         {
200             ostringstream msg;
201             msg << "unknown parameter '" << name << "'";
202             throw Failure(msg.str());
203         }
204     }
205 }
206 
cmd_play(Arguments args)207 void GtpEngine::cmd_play(Arguments args)
208 {
209     play(get_color_arg(args, 0), args, 1);
210 }
211 
cmd_point_integers(Response & response)212 void GtpEngine::cmd_point_integers(Response& response)
213 {
214     auto& geo = get_board().get_geometry();
215     Grid<Point::IntType> grid;
216     for (Point p : geo)
217         grid[p] = p.to_int();
218     response << '\n' << grid.to_string(geo);
219 }
220 
cmd_reg_genmove(Arguments args,Response & response)221 void GtpEngine::cmd_reg_genmove(Arguments args, Response& response)
222 {
223     RandomGenerator::set_global_seed_last();
224     Move move = get_player().genmove(get_board(), get_color_arg(args));
225     if (move.is_null())
226         throw Failure("player failed to generate a move");
227     response << get_board().to_string(move, false);
228 }
229 
cmd_savesgf(Arguments args)230 void GtpEngine::cmd_savesgf(Arguments args)
231 {
232     ofstream out(args.get<string>());
233     PentobiTreeWriter writer(out, m_game.get_tree());
234     writer.set_indent(1);
235     writer.write();
236     if (! out)
237         throw Failure(strerror(errno));
238 }
239 
240 /** Set the game variant.
241     Argument: game variant as in GM property of Pentobi SGF files
242     <br>
243     This command is similar to the command that is used by Quarry
244     (http://home.gna.org/quarry/) to set a game at GTP engines that support
245     multiple games. */
cmd_set_game(Arguments args)246 void GtpEngine::cmd_set_game(Arguments args)
247 {
248     Variant variant;
249     string line(&*args.get_line().begin(), args.get_line().size());
250     if (! parse_variant(line, variant))
251         throw Failure("invalid argument");
252     m_game.init(variant);
253     board_changed();
254 }
255 
256 /** Set global random seed.
257     Arguments: random seed */
cmd_set_random_seed(Arguments args)258 void GtpEngine::cmd_set_random_seed(Arguments args)
259 {
260     RandomGenerator::set_global_seed(args.get<RandomGenerator::ResultType>());
261 }
262 
cmd_showboard(Response & response)263 void GtpEngine::cmd_showboard(Response& response)
264 {
265     response << '\n' << get_board();
266 }
267 
cmd_undo()268 void GtpEngine::cmd_undo()
269 {
270     auto& bd = get_board();
271     if (bd.get_nu_moves() == 0)
272         throw Failure("cannot undo");
273     m_game.undo();
274     board_changed();
275 }
276 
genmove(Color c,Response & response)277 void GtpEngine::genmove(Color c, Response& response)
278 {
279     auto& bd = get_board();
280     auto& player = get_player();
281     auto mv = player.genmove(bd, c);
282     if (mv.is_null())
283     {
284         response << "pass";
285         return;
286     }
287     if (! bd.is_legal(c, mv))
288     {
289         ostringstream msg;
290         msg << "player generated illegal move: " << bd.to_string(mv);
291         throw Failure(msg.str());
292     }
293     if (m_resign && player.resign())
294     {
295         response << "resign";
296         return;
297     }
298     m_game.play(c, mv, true);
299     response << bd.to_string(mv, false);
300     board_changed();
301 }
302 
get_color_arg(Arguments args) const303 Color GtpEngine::get_color_arg(Arguments args) const
304 {
305     if (args.get_size() > 1)
306         throw Failure("too many arguments");
307     return get_color_arg(args, 0);
308 }
309 
get_color_arg(Arguments args,unsigned i) const310 Color GtpEngine::get_color_arg(Arguments args, unsigned i) const
311 {
312     string s = args.get_tolower(i);
313     auto& bd = get_board();
314     auto variant = bd.get_variant();
315     if (get_nu_colors(variant) == 2)
316     {
317         if (s == "blue" || s == "black" || s == "b")
318             return Color(0);
319         if (s == "green" || s == "white" || s == "w")
320             return Color(1);
321     }
322     else
323     {
324         if (s == "1" || s == "blue")
325             return Color(0);
326         if (s == "2" || s == "yellow")
327             return Color(1);
328         if (s == "3" || s == "red")
329             return Color(2);
330         if (s == "4" || s == "green")
331             return Color(3);
332     }
333     throw Failure("invalid color argument '" + s + "'");
334 }
335 
get_player() const336 PlayerBase& GtpEngine::get_player() const
337 {
338     if (m_player == nullptr)
339         throw Failure("no player set");
340     return *m_player;
341 }
342 
on_handle_cmd_begin()343 void GtpEngine::on_handle_cmd_begin()
344 {
345     libboardgame_base::flush_log();
346 }
347 
play(Color c,Arguments args,unsigned arg_move_begin)348 void GtpEngine::play(Color c, Arguments args, unsigned arg_move_begin)
349 {
350     auto& bd = get_board();
351     if (bd.get_nu_moves() >= Board::max_moves)
352         throw Failure("too many moves");
353     Move mv;
354     if (arg_move_begin == 0)
355     {
356         auto line = args.get_line();
357         if (! bd.from_string(mv, string(&*line.begin(), line.size())))
358             throw Failure("invalid move ");
359     }
360     else
361     {
362         auto line = args.get_remaining_line(arg_move_begin - 1);
363         if (! bd.from_string(mv, string(&*line.begin(), line.size())))
364             throw Failure("invalid move ");
365     }
366     if (mv.is_null())
367         throw Failure("play pass not supported (anymore)");
368     // Board::play() can handle illegal moves at arbitrary positions, even
369     // overlapping, but it does not check (for performance reasons) if the
370     // piece-left count is already zero.
371     if (! bd.is_piece_left(c, bd.get_move_piece(mv)))
372         throw Failure("piece already played");
373     if (! m_accept_illegal && ! bd.is_legal(c, mv))
374         throw Failure("illegal move");
375     m_game.play(c, mv, true);
376     board_changed();
377 }
378 
set_player(PlayerBase & player)379 void GtpEngine::set_player(PlayerBase& player)
380 {
381     m_player = &player;
382     add("genmove", &GtpEngine::cmd_genmove);
383     add("g", &GtpEngine::cmd_g);
384     add("reg_genmove", &GtpEngine::cmd_reg_genmove);
385 }
386 
set_show_board(bool enable)387 void GtpEngine::set_show_board(bool enable)
388 {
389     if (enable && ! m_show_board)
390         LIBBOARDGAME_LOG(get_board());
391     m_show_board = enable;
392 }
393 
394 //-----------------------------------------------------------------------------
395 
396 } // namespace libpentobi_gtp
397