game.cpp 23.7 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
    Copyright 2008 Sascha Peilicke <sasch.pe@gmx.de>

    This program is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public License as
    published by the Free Software Foundation; either version 2 of
    the License or (at your option) version 3 or any later version
    accepted by the membership of KDE e.V. (or its successor approved
    by the membership of KDE e.V.), which shall act as a proxy
    defined in Section 14 of version 3 of the license.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

21
#include "game.h"
22
23
#include "score.h"

Laurent Montel's avatar
Laurent Montel committed
24
#include <KLocalizedString>
25
26

#include <QApplication>
Laurent Montel's avatar
Laurent Montel committed
27
#include "kigo_debug.h"
28
29
30
31
#include <QFile>

namespace Kigo {

Chusslove Illich's avatar
i18n:    
Chusslove Illich committed
32
33
34
35
class UndoCommand : public QUndoCommand
{
    public:

Frederik Schwarzer's avatar
Frederik Schwarzer committed
36
    enum class MoveType { Stone, Passed, Resigned };
Chusslove Illich's avatar
i18n:    
Chusslove Illich committed
37
38
39
40
41
42
43
44
45
46
47
48
49
50

    UndoCommand(Player *player, MoveType moveType, const QString &undoStr)
    : QUndoCommand(undoStr), m_player(player), m_moveType(moveType)
    {}

    Player *player () const { return m_player; }
    MoveType moveType () const { return m_moveType; }

    private:

    Player *m_player;
    MoveType m_moveType;
};

51
Game::Game(QObject *parent)
52
    : QObject(parent)
53
    , m_currentMove(0), m_lastUndoIndex(0), m_currentPlayer(&m_blackPlayer)
Frederik Schwarzer's avatar
Frederik Schwarzer committed
54
    , m_blackPlayer(Player::Color::Black), m_whitePlayer(Player::Color::White)
55
    , m_komi(4.5), m_boardSize(19), m_fixedHandicap(5), m_consecutivePassMoveNumber(0)
56
    , m_gameFinished(false)
57
{
Laurent Montel's avatar
Laurent Montel committed
58
59
60
61
    connect(&m_process, &QProcess::readyRead, this, &Game::readyRead);
    connect(&m_undoStack, &QUndoStack::canRedoChanged, this, &Game::canRedoChanged);
    connect(&m_undoStack, &QUndoStack::canUndoChanged, this, &Game::canUndoChanged);
    connect(&m_undoStack, &QUndoStack::indexChanged, this, &Game::undoIndexChanged);
62
63
}

64
Game::~Game()
65
66
67
68
{
    stop();
}

69
bool Game::start(const QString &command)
70
71
{
    stop();                                   // Close old session if there's one
72
    m_process.start(command, QStringList());      // Start new process with provided command
73
    if (!m_process.waitForStarted()) {        // Blocking wait for process start
74
        m_response = QLatin1String("Unable to execute command: ") + command;
Laurent Montel's avatar
Laurent Montel committed
75
        //qCDebug(KIGO_LOG) << m_response;
76
77
78
        return false;
    }
    m_engineCommand = command;
Laurent Montel's avatar
Laurent Montel committed
79
    ////qCDebug(KIGO_LOG) << "Game" << command << "started...";
80

81
    // Test if we started a GTP-compatible Go game
82
83
    m_process.write("name\n");
    m_process.waitForReadyRead();
Frederik Schwarzer's avatar
Frederik Schwarzer committed
84
    const QString response = m_process.readAllStandardOutput();
85
    if (response.isEmpty() || !response.startsWith(QLatin1Char('='))) {
Laurent Montel's avatar
Laurent Montel committed
86
        m_response = QStringLiteral("Game did not respond to GTP command \"name\"");
Laurent Montel's avatar
Laurent Montel committed
87
        //qCDebug(KIGO_LOG) << m_response;
88
89
90
91
92
        stop();
        return false;
    } else {
        m_engineName = m_response;
    }
Laurent Montel's avatar
Laurent Montel committed
93
    //qCDebug(KIGO_LOG) << "Game is a GTP-compatible Go game";
94
95

    m_process.write("version\n");
96
    if (waitResponse()) {
97
        m_engineVersion = m_response;
98
    }
99
100
101
    return true;
}

102
void Game::stop()
103
104
105
106
107
108
109
{
    if (m_process.isOpen()) {
        m_process.write("quit\n");
        m_process.close();
    }
}

110
bool Game::init()
111
{
112
    if (!isRunning()) {
113
        return false;
114
    }
115

Laurent Montel's avatar
Laurent Montel committed
116
    //qCDebug(KIGO_LOG) << "Init game!";
117

118
119
120
121
122
123
124
    m_process.write("clear_board\n");
    if (waitResponse()) {
        // The board is wiped empty, start again with black player
        setCurrentPlayer(m_blackPlayer);
        m_fixedHandicap = 0;
        m_consecutivePassMoveNumber = 0;
        m_currentMove = 0;
125
        m_gameFinished = false;
126
127
128
        m_movesList.clear();
        m_undoStack.clear();

129
        emit boardChanged();
130
        return true;
131
    } else {
132
        return false;
133
    }
134
135
}

136
bool Game::init(const QString &fileName, int moveNumber)
137
138
{
    Q_ASSERT(moveNumber >= 0);
139
    if (!isRunning() || fileName.isEmpty() || !QFile::exists(fileName)) {
140
        return false;
141
    }
142
143
144

    m_process.write("loadsgf " + fileName.toLatin1() + ' ' + QByteArray::number(moveNumber) + '\n');
    if (waitResponse()) {
Sascha Peilicke's avatar
Sascha Peilicke committed
145
        if (m_response.startsWith(QLatin1String("white"))) { // Check which player is current
146
            setCurrentPlayer(m_whitePlayer);
147
        } else {
148
            setCurrentPlayer(m_blackPlayer);
149
        }
150

151
        m_process.write("query_boardsize\n");       // Query board size from game
152
        if (waitResponse()) {
153
            m_boardSize = m_response.toInt();
154
        }
155
        m_process.write("get_komi\n");              // Query komi from game and store it
156
        if (waitResponse()) {
157
            m_komi = m_response.toFloat();
158
        }
159
        m_process.write("get_handicap\n");          // Query fixed handicap and store it
160
        if (waitResponse()) {
161
            m_fixedHandicap = m_response.toInt();
162
        }
Laurent Montel's avatar
Laurent Montel committed
163
        //qCDebug(KIGO_LOG) << "Loaded komi is" << m_komi << "and handicap is" << m_fixedHandicap;
164
165

        m_consecutivePassMoveNumber = 0;
Frederik Gladhorn's avatar
Frederik Gladhorn committed
166
        m_currentMove = moveNumber;
167
        m_gameFinished = false;
168
169
170
        m_movesList.clear();
        m_undoStack.clear();

171
        emit boardSizeChanged(m_boardSize);
172
        emit boardChanged();                             // All done, tell the world!
173
        return true;
174
    } else {
175
        return false;
176
    }
177
178
}

179
bool Game::save(const QString &fileName)
180
{
181
    if (!isRunning() || fileName.isEmpty()) {
182
        return false;
183
    }
184
185
186
187
188

    m_process.write("printsgf " + fileName.toLatin1() + '\n');
    return waitResponse();
}

189
bool Game::setBoardSize(int size)
190
191
{
    Q_ASSERT(size >= 1 && size <= 19);
192
    if (!isRunning()) {
193
        return false;
194
    }
195
196
197
198
199
200
201
202
203
204
205

    m_process.write("boardsize " + QByteArray::number(size) + '\n');
    if (waitResponse()) {
        // Changing size wipes the board, start again with black player.
        setCurrentPlayer(m_blackPlayer);
        m_boardSize = size;
        m_fixedHandicap = 0;
        m_consecutivePassMoveNumber = 0;
        m_currentMove = 0;
        m_movesList.clear();
        m_undoStack.clear();
206
        emit boardSizeChanged(size);
207
        emit boardChanged();
208
        return true;
209
    } else {
210
        return false;
211
    }
212
213
}

214
bool Game::setKomi(float komi)
215
216
{
    Q_ASSERT(komi >= 0);
217
    if (!isRunning()) {
218
        return false;
219
    }
220
221
222
223
224

    m_process.write("komi " + QByteArray::number(komi) + '\n');
    if (waitResponse()) {
        m_komi = komi;
        return true;
225
    } else {
226
        return false;
227
    }
228
229
}

230
bool Game::setFixedHandicap(int handicap)
231
232
{
    Q_ASSERT(handicap >= 2 && handicap <= 9);
233
    if (!isRunning()) {
234
        return false;
235
    }
236
237
238
239
240
241
242
243

    if (handicap <= fixedHandicapUpperBound()) {
        m_process.write("fixed_handicap " + QByteArray::number(handicap) + '\n');
        if (waitResponse()) {
            // Black starts with setting his (fixed) handicap as it's first turn
            // which means, white is next.
            setCurrentPlayer(m_whitePlayer);
            m_fixedHandicap = handicap;
244
            emit boardChanged();
245
            return true;
246
        } else {
247
            return false;
248
        }
249
    } else {
Laurent Montel's avatar
Laurent Montel committed
250
        //qCWarning(KIGO_LOG) << "Handicap" << handicap << " not set, it is too high!";
251
252
253
254
        return false;
    }
}

255
int Game::fixedHandicapUpperBound()
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
{
    switch (m_boardSize) {  // Handcrafted values reflect what GnuGo accepts
        case 7:
        case 8:
        case 10:
        case 12:
        case 14:
        case 16:
        case 18: return 4;
        case 9:
        case 11:
        case 13:
        case 15:
        case 17:
        case 19: return 9;
        default: return 0;
    }
}

275
bool Game::playMove(const Move &move, bool undoable)
276
{
277
    return playMove(*move.player(), move.stone(), undoable);
278
279
}

280
bool Game::playMove(const Player &player, const Stone &stone, bool undoable)
281
{
282
    if (!isRunning()) {
283
        return false;
284
    }
285

286
287
    const Player *tmp = &player;
    if (!tmp->isValid()) {
Laurent Montel's avatar
Laurent Montel committed
288
        //qCDebug(KIGO_LOG) << "Invalid player argument, using current player!";
289
        tmp = m_currentPlayer;
290
    }
291
292

    QByteArray msg("play ");                    // The command to be sent
293
    if (tmp->isWhite()) {
294
        msg.append("white ");
295
    } else {
296
        msg.append("black ");
297
298
    }
    if (stone.isValid()) {
299
        msg.append(stone.toLatin1() + '\n');
300
    } else {
301
        msg.append("pass\n");
302
    }
303
304
305
306
307
308
309
310

    m_process.write(msg);                       // Send command to backend
    if (waitResponse()) {
        if (stone.isValid()) {                  // Normal move handling
            m_movesList.append(Move(tmp, stone));
            m_consecutivePassMoveNumber = 0;
        } else {                                // And pass move handling
            m_movesList.append(Move(tmp, Stone::Pass));
311
            emit passMovePlayed(*m_currentPlayer);
312
            if (m_consecutivePassMoveNumber > 0) {
313
                emit consecutivePassMovesPlayed(m_consecutivePassMoveNumber);
314
            }
315
316
317
318
319
            m_consecutivePassMoveNumber++;
        }
        m_currentMove++;

        if (undoable) {                         // Do undo stuff if desired
Frederik Schwarzer's avatar
Frederik Schwarzer committed
320
            Player *playerTemp;
Chusslove Illich's avatar
i18n:    
Chusslove Illich committed
321
            UndoCommand::MoveType moveType;
322
            QString undoStr;
323
            if (tmp->isWhite()) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
324
                playerTemp = &m_whitePlayer;
Chusslove Illich's avatar
i18n:    
Chusslove Illich committed
325
                if (stone.isValid()) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
326
                    moveType = UndoCommand::MoveType::Stone;
Chusslove Illich's avatar
i18n:    
Chusslove Illich committed
327
328
                    undoStr = i18nc("%1 stone coordinate", "White %1", stone.toString());
                } else {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
329
                    moveType = UndoCommand::MoveType::Passed;
Chusslove Illich's avatar
i18n:    
Chusslove Illich committed
330
331
                    undoStr = i18n("White passed");
                }
332
            } else {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
333
                playerTemp = &m_blackPlayer;
Chusslove Illich's avatar
i18n:    
Chusslove Illich committed
334
                if (stone.isValid()) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
335
                    moveType = UndoCommand::MoveType::Stone;
Chusslove Illich's avatar
i18n:    
Chusslove Illich committed
336
337
                    undoStr = i18nc("%1 stone coordinate", "Black %1", stone.toString());
                } else {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
338
                    moveType = UndoCommand::MoveType::Passed;
Chusslove Illich's avatar
i18n:    
Chusslove Illich committed
339
340
                    undoStr = i18n("Black passed");
                }
341
            }
Laurent Montel's avatar
Laurent Montel committed
342
            //qCDebug(KIGO_LOG) << "Push new undo command" << undoStr;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
343
            m_undoStack.push(new UndoCommand(playerTemp, moveType, undoStr));
344
345
        }

346
347
348
349
350
351
        if (tmp->isWhite()) {                   // Determine the next current player
            setCurrentPlayer(m_blackPlayer);
        } else {
            setCurrentPlayer(m_whitePlayer);
        }

352
        emit boardChanged();
353
        return true;
354
    } else {
355
        return false;
356
    }
357
358
}

359
bool Game::generateMove(const Player &player, bool undoable)
360
{
361
    if (!isRunning()) {
362
        return false;
363
    }
364
365
    const Player *tmp = &player;
    if (!tmp->isValid()) {
Laurent Montel's avatar
Laurent Montel committed
366
        //qCDebug(KIGO_LOG) << "Invalid player argument, using current player!";
367
        tmp = m_currentPlayer;
368
    }
369

370
    if (tmp->isWhite()) {
371
372
373
374
375
376
377
378
379
        m_process.write("level " + QByteArray::number(m_whitePlayer.strength()) + '\n');
        waitResponse(); // Setting level is not mission-critical, no error checking
        m_process.write("genmove white\n");
    } else {
        m_process.write("level " + QByteArray::number(m_blackPlayer.strength()) + '\n');
        waitResponse(); // Setting level is not mission-critical, no error checking
        m_process.write("genmove black\n");
    }
    if (waitResponse(true)) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
380
        bool boardChange = false;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
381
        Player *playerTemp;
Chusslove Illich's avatar
i18n:    
Chusslove Illich committed
382
        UndoCommand::MoveType moveType;
383
        QString undoStr;
Chusslove Illich's avatar
i18n:    
Chusslove Illich committed
384

385
        if (tmp->isWhite()) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
386
            playerTemp = &m_whitePlayer;
387
        } else {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
388
            playerTemp = &m_blackPlayer;
389
        }
390
        if (m_response == QLatin1String("PASS")) {
391
            m_currentMove++;
392
            emit passMovePlayed(*m_currentPlayer);
393
            if (m_consecutivePassMoveNumber > 0) {
394
                emit consecutivePassMovesPlayed(m_consecutivePassMoveNumber);
395
                m_gameFinished = true;
396
            }
397
            m_consecutivePassMoveNumber++;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
398
            moveType = UndoCommand::MoveType::Passed;
Chusslove Illich's avatar
i18n:    
Chusslove Illich committed
399
400
401
402
403
            if (tmp->isWhite()) {
                undoStr = i18n("White passed");
            } else {
                undoStr = i18n("Black passed");
            }
404
        } else if (m_response == QLatin1String("resign")) {
405
            emit resigned(*m_currentPlayer);
406
            m_gameFinished = true;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
407
            moveType = UndoCommand::MoveType::Resigned;
Chusslove Illich's avatar
i18n:    
Chusslove Illich committed
408
409
410
411
412
            if (tmp->isWhite()) {
                undoStr = i18n("White resigned");
            } else {
                undoStr = i18n("Black resigned");
            }
413
414
415
416
        } else {
            m_currentMove++;
            m_movesList.append(Move(tmp, Stone(m_response)));
            m_consecutivePassMoveNumber = 0;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
417
            moveType = UndoCommand::MoveType::Stone;
Chusslove Illich's avatar
i18n:    
Chusslove Illich committed
418
419
420
421
422
            if (tmp->isWhite()) {
                undoStr = i18nc("%1 response from Go engine", "White %1", m_response);
            } else {
                undoStr = i18nc("%1 response from Go engine", "Black %1", m_response);
            }
423
            boardChange = true;
424
425
426
        }

        if (undoable) {
Laurent Montel's avatar
Laurent Montel committed
427
            //qCDebug(KIGO_LOG) << "Push new undo command" << undoStr;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
428
            m_undoStack.push(new UndoCommand(playerTemp, moveType, undoStr));
429
        }
430
431
432
433
434
        if (tmp->isWhite()) {
            setCurrentPlayer(m_blackPlayer);
        } else {
            setCurrentPlayer(m_whitePlayer);
        }
435
436
437
        if (boardChange) {
            emit boardChanged();
        }
438
        return true;
439
    } else {
440
        return false;
441
    }
442
443
}

444
bool Game::undoMove()
445
{
446
    if (!isRunning()) {
447
        return false;
448
    }
449
450
451

    m_process.write("undo\n");
    if (waitResponse()) {
452
        Move lastMove = m_movesList.takeLast();
453
        m_currentMove--;
454
455
456
457
458
459
460
461
462
        if (lastMove.player()->isComputer()) {
            // Do one more undo
            m_process.write("undo\n");
            if (waitResponse()) {
                lastMove = m_movesList.takeLast();
                m_currentMove--;
            }
            m_undoStack.undo();
        }
463
464
465
466
467
468
        if (lastMove.player()->isWhite()) {
            setCurrentPlayer(m_whitePlayer);
        } else {
            setCurrentPlayer(m_blackPlayer);
        }
        if (m_consecutivePassMoveNumber > 0) {
469
            m_consecutivePassMoveNumber--;
470
471
472
        }
        //TODO: What happens with pass moves deeper in the history?
        m_undoStack.undo();
473

474
        emit boardChanged();
475
        return true;
476
    } else {
477
        return false;
478
    }
479
480
}

481
bool Game::redoMove()
482
{
483
    if (!isRunning()) {
484
        return false;
485
    }
486

Chusslove Illich's avatar
i18n:    
Chusslove Illich committed
487
    const UndoCommand *undoCmd = static_cast<const UndoCommand*>(m_undoStack.command(m_undoStack.index()));
488

Frederik Schwarzer's avatar
Frederik Schwarzer committed
489
    const Player *player = undoCmd->player();
490

Frederik Schwarzer's avatar
Frederik Schwarzer committed
491
    if (undoCmd->moveType() == UndoCommand::MoveType::Passed) {
Laurent Montel's avatar
Laurent Montel committed
492
        //qCDebug(KIGO_LOG) << "Redo a pass move for" << player << undoCmd->text();
493
        playMove(*player, Stone(), false);         // E.g. pass move
Frederik Schwarzer's avatar
Frederik Schwarzer committed
494
    } else if (undoCmd->moveType() == UndoCommand::MoveType::Resigned) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
495
        // Note: Although it is possible to undo after a resign and redo it,
496
        //       it is a bit questionable whether this makes sense logically.
Laurent Montel's avatar
Laurent Montel committed
497
        //qCDebug(KIGO_LOG) << "Redo a resign for" << player << undoCmd->text();
498
        emit resigned(*player);
499
        m_gameFinished = true;
500
        //emit resigned(*m_currentPlayer);
501
    } else {
Laurent Montel's avatar
Laurent Montel committed
502
        //qCDebug(KIGO_LOG) << "Redo a normal move for" << player << undoCmd->text();
Chusslove Illich's avatar
i18n:    
Chusslove Illich committed
503
        playMove(*player, Stone(undoCmd->text()), false);
504
    }
505
506
507
508
    m_undoStack.redo();
    return false;
}

509
Move Game::lastMove() const
510
511
512
513
514
{
    Q_ASSERT(!m_movesList.isEmpty());
    return m_movesList.last();
}

Frederik Gladhorn's avatar
Frederik Gladhorn committed
515
516
517
518
519
520
521
522
int Game::moveCount()
{
    if (!isRunning()) {
        return 0;
    }

    m_process.write("move_history\n");          // Query fixed handicap and store it
    if (waitResponse()) {
Laurent Montel's avatar
Laurent Montel committed
523
        return m_response.count(QLatin1Char('\n')) + 1;
Frederik Gladhorn's avatar
Frederik Gladhorn committed
524
525
526
527
    }
    return 0;
}

528
QList<Stone> Game::stones(const Player &player)
529
530
{
    QList<Stone> list;
531
    if (!isRunning()) {
532
        return list;
533
    }
534
535
536
537
538

    // Invalid player means all stones
    if (!player.isWhite()) {
        m_process.write("list_stones black\n");
        if (waitResponse() && !m_response.isEmpty()) {
Laurent Montel's avatar
Laurent Montel committed
539
            foreach (const QString &pos, m_response.split(QLatin1Char(' '))) {
540
                list.append(Stone(pos));
541
            }
542
543
544
545
546
        }
    }
    if (!player.isBlack()) {
        m_process.write("list_stones white\n");
        if (waitResponse() && !m_response.isEmpty()) {
Laurent Montel's avatar
Laurent Montel committed
547
            foreach (const QString &pos, m_response.split(QLatin1Char(' '))) {
548
                list.append(Stone(pos));
549
            }
550
551
552
553
554
        }
    }
    return list;
}

555
QList<Move> Game::moves(const Player &player)
556
557
{
    QList<Move> list;
558
    if (!isRunning()) {
559
        return list;
560
    }
561
562
563
564
565

    if (!player.isValid()) {
        list = m_movesList;
    } else {
        foreach (const Move &move, m_movesList) {
566
            if (move.player()->color() == player.color()) {
567
                list.append(move);
568
            }
569
570
571
572
573
        }
    }
    return list;
}

574
QList<Stone> Game::liberties(const Stone &stone)
575
576
{
    QList<Stone> list;
577
    if (!isRunning() || !stone.isValid()) {
578
        return list;
579
    }
580
581
582

    m_process.write("findlib " + stone.toLatin1() + '\n');
    if (waitResponse() && !m_response.isEmpty()) {
Laurent Montel's avatar
Laurent Montel committed
583
        foreach (const QString &entry, m_response.split(QLatin1Char(' '))) {
584
            list.append(Stone(entry));
585
        }
586
587
588
589
    }
    return list;
}

590
QList<Stone> Game::bestMoves(const Player &player)
591
592
{
    QList<Stone> list;
593
    if (!isRunning() || !player.isValid()) {
594
        return list;
595
    }
596

597
    if (player.isWhite()) {
598
        m_process.write("top_moves_white\n");
599
    } else {
600
        m_process.write("top_moves_black\n");
601
    }
602
    if (waitResponse(true) && !m_response.isEmpty()) {
Laurent Montel's avatar
Laurent Montel committed
603
        const QStringList parts = m_response.split(QLatin1Char(' '));
604
605
        if (parts.size() % 2 == 0) {
            for (int i = 0; i < parts.size(); i += 2) {
606
                list.append(Stone(parts[i], QString(parts[i + 1]).toFloat()));
607
608
            }
        }
609
610
611
612
    }
    return list;
}

613
QList<Stone> Game::legalMoves(const Player &player)
614
615
{
    QList<Stone> list;
616
    if (!isRunning() || !player.isValid()) {
617
        return list;
618
    }
619

620
    if (player.isWhite()) {
621
        m_process.write("all_legal white\n");
622
    } else {
623
        m_process.write("all_legal black\n");
624
    }
625
    if (waitResponse() && !m_response.isEmpty()) {
Laurent Montel's avatar
Laurent Montel committed
626
        foreach (const QString &entry, m_response.split(QLatin1Char(' '))) {
627
            list.append(Stone(entry));
628
        }
629
630
631
632
    }
    return list;
}

633
int Game::captures(const Player &player)
634
{
635
    if (!isRunning() || !player.isValid()) {
636
        return 0;
637
    }
638

639
    if (player.isWhite()) {
640
        m_process.write("captures white\n");
641
    } else {
642
        m_process.write("captures black\n");
643
    }
644
645
646
    return waitResponse() ? m_response.toInt() : 0;
}

647
Game::FinalState Game::finalState(const Stone &stone)
648
{
649
    if (!isRunning() || !stone.isValid()) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
650
        return FinalState::FinalStateInvalid;
651
    }
652
653
654

    m_process.write("final_status " + stone.toLatin1() + '\n');
    if (waitResponse()) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
655
656
657
658
659
660
661
662
        if (m_response == QLatin1String("alive")) { return FinalState::FinalAlive; }
        else if (m_response == QLatin1String("dead")) { return FinalState::FinalDead; }
        else if (m_response == QLatin1String("seki")) { return FinalState::FinalSeki; }
        else if (m_response == QLatin1String("white_territory")) { return FinalState::FinalWhiteTerritory; }
        else if (m_response == QLatin1String("blacK_territory")) { return FinalState::FinalBlackTerritory; }
        else if (m_response == QLatin1String("dame")) { return FinalState::FinalDame; }
        else { return FinalState::FinalStateInvalid; }
    } else {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
663
        return FinalState::FinalStateInvalid;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
664
    }
665
666
}

667
QList<Stone> Game::finalStates(FinalState state)
668
669
{
    QList<Stone> list;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
670
    if (!isRunning() || state == FinalState::FinalStateInvalid) {
671
        return list;
672
    }
673
674
675

    QByteArray msg("final_status_list ");
    switch (state) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
676
677
678
679
680
681
682
        case FinalState::FinalAlive: msg.append("alive"); break;
        case FinalState::FinalDead: msg.append("dead"); break;
        case FinalState::FinalSeki: msg.append("seki"); break;
        case FinalState::FinalWhiteTerritory: msg.append("white_territory"); break;
        case FinalState::FinalBlackTerritory: msg.append("black_territory"); break;
        case FinalState::FinalDame: msg.append("dame"); break;
        case FinalState::FinalStateInvalid: /* Will never happen */ break;
683
684
685
686
    }
    msg.append('\n');
    m_process.write(msg);
    if (waitResponse() && !m_response.isEmpty()) {
Laurent Montel's avatar
Laurent Montel committed
687
        foreach (const QString &entry, m_response.split(QLatin1Char(' '))) {
688
            list.append(Stone(entry));
689
        }
690
691
692
693
    }
    return list;
}

694
Score Game::finalScore()
695
{
696
    if (!isRunning()) {
697
        return Score();
698
    }
699
700
701
702
703

    m_process.write("final_score\n");
    return waitResponse() ? Score(m_response) : Score();
}

704
Score Game::estimatedScore()
705
{
706
    if (!isRunning()) {
707
        return Score();
708
    }
709
710
711
712
713

    m_process.write("estimate_score\n");
    return waitResponse() ? Score(m_response) : Score();
}

714
bool Game::waitResponse(bool nonBlocking)
715
716
717
{
    if (m_process.state() != QProcess::Running) {   // No GTP connection means no computing fun!
        switch (m_process.error()) {
Laurent Montel's avatar
Laurent Montel committed
718
719
720
            case QProcess::FailedToStart: m_response = QStringLiteral("No Go game is running!"); break;
            case QProcess::Crashed: m_response = QStringLiteral("The Go game crashed!"); break;
            case QProcess::Timedout: m_response = QStringLiteral("The Go game timed out!"); break;
721
722
            case QProcess::WriteError: m_response = m_process.readAllStandardError(); break;
            case QProcess::ReadError: m_response = m_process.readAllStandardError(); break;
Laurent Montel's avatar
Laurent Montel committed
723
            case QProcess::UnknownError: m_response = QStringLiteral("Unknown error!"); break;
724
        }
Laurent Montel's avatar
Laurent Montel committed
725
        qCWarning(KIGO_LOG) << "Command failed:" << m_response;
726
727
728
        return false;
    }

729
    if (nonBlocking) {
730
        emit waiting(true);
731
    }
732

733
734
735
736
737
    // Wait for finished command execution. We have to do this untill '\n\n' (or '\r\n\r\n' or
    // '\r\r' in case of MS Windows and Mac, respectively) arrives in our input buffer to show
    // that the Go game is done processing our request. The 'nonBlocking' parameter decides
    // whether we block and wait (suitable for short commands) or if we continue processing
    // events in between to stop the UI from blocking (suitable for longer commands).
738
739
740
741
742
743
744
745
746
747
748
749
    // The latter may introduce flickering.
    m_response.clear();
    do {
        if (nonBlocking) {
            while (m_waitAndProcessEvents) {    // The flag is modified by another slot
                qApp->processEvents();          // called when QProcess signals readyRead()
            }
            m_waitAndProcessEvents = true;
        } else {
            m_process.waitForReadyRead();       // Blocking wait
        }
        m_response += m_process.readAllStandardOutput();
750
751
752
    } while (!m_response.endsWith(QLatin1String("\r\r")) &&
             !m_response.endsWith(QLatin1String("\n\n")) &&
             !m_response.endsWith(QLatin1String("\r\n\r\n")));
753

754
    if (nonBlocking) {
755
        emit waiting(false);
756
    }
757

758
    if (m_response.size() < 1) {
759
        return false;
760
    }
761
762
763
    QChar tmp = m_response[0];                  // First message character indicates success or error
    m_response.remove(0, 2);                    // Remove the first two chars (e.g. "? " or "= ")
    m_response = m_response.trimmed();          // Remove further whitespaces, newlines, ...
Laurent Montel's avatar
Laurent Montel committed
764
    return tmp != QLatin1Char('?');                          // '?' Means the game didn't understand the query
765
766
}

767
768
769
770
void Game::gameSetup()
{
    emit boardInitialized();
}
Frederik Schwarzer's avatar
Frederik Schwarzer committed
771

772
void Game::readyRead()
773
774
775
776
{
    m_waitAndProcessEvents = false;
}

777
778
779
780
781
void Game::undoIndexChanged(int index)
{
    m_lastUndoIndex = index;
}

782
void Game::setCurrentPlayer(Player &player)
783
{
784
785
    m_currentPlayer = &player;
    emit currentPlayerChanged(*m_currentPlayer);
786
787
788
}

} // End of namespace Kigo