1 /****************************************************************************
2 
3  Copyright (C) 2002-2014 Gilles Debunne. All rights reserved.
4 
5  This file is part of the QGLViewer library version 2.7.2.
6 
7  http://www.libqglviewer.com - contact@libqglviewer.com
8 
9  This file may be used under the terms of the GNU General Public License
10  versions 2.0 or 3.0 as published by the Free Software Foundation and
11  appearing in the LICENSE file included in the packaging of this file.
12  In addition, as a special exception, Gilles Debunne gives you certain
13  additional rights, described in the file GPL_EXCEPTION in this package.
14 
15  libQGLViewer uses dual licensing. Commercial/proprietary software must
16  purchase a libQGLViewer Commercial License.
17 
18  This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
19  WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
20 
21 *****************************************************************************/
22 
23 #include "agoraViewer.h"
24 #include "board.h"
25 
26 #include <fstream>
27 #include <qfiledialog.h>
28 #include <qfileinfo.h>
29 #include <qmessagebox.h>
30 #include <qstatusbar.h>
31 #include <qtextedit.h>
32 #include <qtimer.h>
33 
34 #include <QGLViewer/manipulatedCameraFrame.h>
35 
36 #include "ui_agoraWindow.h"
37 class AgoraWindow : public QMainWindow, public Ui::AgoraWindow {};
38 
39 using namespace std;
40 using namespace qglviewer;
41 
42 static QString colorString[2] = {"White", "Black"};
43 
44 class BoardConstraint : public Constraint {
45 public:
constrainRotation(Quaternion & q,Frame * const fr)46   virtual void constrainRotation(Quaternion &q, Frame *const fr) {
47     const Vec up = fr->transformOf(Vec(0.0, 0.0, 1.0));
48     Vec axis = q.axis();
49     float angle = 2.0 * acos(q[3]);
50     if (fabs(axis * up) > fabs(axis.x))
51       axis.projectOnAxis(up);
52     else {
53       angle = (axis.x > 0.0) ? angle : -angle;
54       axis.setValue(fabs(axis.x), 0.0, 0.0);
55       const float currentAngle =
56           asin(fr->inverseTransformOf(Vec(0.0, 0.0, -1.0)).z);
57       if (currentAngle + angle > -0.2)
58         angle = -0.2 - currentAngle; // Not too low
59       if (currentAngle + angle < -M_PI / 2.0)
60         angle = -M_PI / 2.0 - currentAngle; // Do not pass on the other side
61     }
62     q = Quaternion(axis, angle);
63   }
64 };
65 
66 #if QT_VERSION < 0x040000
AgoraViewer(QWidget * parent,const char * name)67 AgoraViewer::AgoraViewer(QWidget *parent, const char *name)
68     : QGLViewer(parent, name),
69 #else
70 AgoraViewer::AgoraViewer(QWidget *parent)
71     : QGLViewer(parent),
72 #endif
73       boardFileName_("standard.ago"), selectedPiece_(-1),
74       displayPossibleMoves_(true), animatePlays_(false /*true*/),
75       animationStep_(0) {
76   kfi_ = new KeyFrameInterpolator(new Frame());
77 
78   QObject::connect(kfi_, SIGNAL(interpolated()), this, SLOT(update()));
79   QObject::connect(kfi_, SIGNAL(endReached()), this, SLOT(simplePlay()));
80 
81   undoTimer_ = new QTimer();
82 #if QT_VERSION >= 0x040000
83   undoTimer_->setSingleShot(true);
84 #endif
85   QObject::connect(undoTimer_, SIGNAL(timeout()), this, SLOT(playNextMove()));
86 
87   for (int i = 0; i < 2; ++i)
88     connect(&(computerPlayer_[i]), SIGNAL(moveMade(QString, int)), this,
89             SLOT(playComputerMove(QString, int)));
90 
91   computerPlayer_[0].setIsActive(false);
92   computerPlayer_[1].setIsActive(false);
93 
94   QStringList programFileNames;
95   programFileNames << "agoraAI"
96                    << "agoraAI.exe"
97                    << "../AI/agoraAI"
98                    << "../../AI/release/agoraAI.exe"
99                    << "../../AI/debug/agoraAI.exe"
100                    << "../AI/release/agoraAI.exe"
101                    << "../AI/debug/agoraAI.exe";
102   for (int i = 0; i < programFileNames.size(); ++i) {
103     if (QFileInfo(programFileNames.at(i)).isExecutable()) {
104       for (int c = 0; c < 2; ++c)
105         computerPlayer_[c].setProgramFileName(programFileNames.at(i));
106       break;
107     }
108   }
109 }
110 
111 // I n i t i a l i z a t i o n   f u n c t i o n s
112 
init()113 void AgoraViewer::init() {
114   initViewer();
115   initSpotLight();
116   initBoard();
117   fitCameraToBoard();
118 
119   setMouseBinding(Qt::NoModifier, Qt::RightButton, CAMERA, ROTATE);
120   setMouseBinding(Qt::NoModifier, Qt::LeftButton, SELECT);
121 
122 #if QT_VERSION >= 0x040000
123   // Signals and slots connections
124   AgoraWindow *agoWin = (AgoraWindow *)(window());
125   connect(agoWin->fileExitAction, SIGNAL(activated()), agoWin, SLOT(close()));
126   connect(agoWin->fileOpenAction, SIGNAL(activated()), this, SLOT(load()));
127   connect(agoWin->fileSaveAction, SIGNAL(activated()), this, SLOT(save()));
128   connect(agoWin->fileSaveAsAction, SIGNAL(activated()), this, SLOT(saveAs()));
129   connect(agoWin->gameNewGameAction, SIGNAL(activated()), this,
130           SLOT(newGame()));
131   connect(agoWin->gameUndoAction, SIGNAL(activated()), this, SLOT(undo()));
132   connect(agoWin->gameRedoAction, SIGNAL(activated()), this, SLOT(redo()));
133 
134   connect(agoWin->gameBlackIsHumanAction, SIGNAL(toggled(bool)), this,
135           SLOT(blackPlayerIsHuman(bool)));
136   connect(agoWin->gameWhiteIsHumanAction, SIGNAL(toggled(bool)), this,
137           SLOT(whitePlayerIsHuman(bool)));
138 
139   connect(agoWin->gameBlackIsHumanAction, SIGNAL(toggled(bool)),
140           agoWin->gameConfigureBlackPlayerAction, SLOT(setDisabled(bool)));
141   connect(agoWin->gameWhiteIsHumanAction, SIGNAL(toggled(bool)),
142           agoWin->gameConfigureWhitePlayerAction, SLOT(setDisabled(bool)));
143 
144   connect(agoWin->gameConfigureBlackPlayerAction, SIGNAL(activated()), this,
145           SLOT(configureBlackPlayer()));
146   connect(agoWin->gameConfigureWhitePlayerAction, SIGNAL(activated()), this,
147           SLOT(configureWhitePlayer()));
148 
149   connect(agoWin->togglePossibleMoveAction, SIGNAL(toggled(bool)), this,
150           SLOT(toggleDisplayPossibleMoves(bool)));
151   connect(agoWin->toggleAnimationAction, SIGNAL(toggled(bool)), this,
152           SLOT(toggleAnimation(bool)));
153 
154   connect(agoWin->helpAboutAction, SIGNAL(activated()), this, SLOT(about()));
155   connect(agoWin->helpRulesAction, SIGNAL(activated()), this,
156           SLOT(displayRules()));
157 #endif
158 }
159 
initViewer()160 void AgoraViewer::initViewer() {
161   glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
162 
163   // Enable GL textures
164   glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
165   glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
166 
167   // Nice texture coordinate interpolation
168   glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
169 
170   const QString texFileName("wood.png");
171   QImage img(texFileName);
172 
173   if (img.isNull()) {
174     QMessageBox::warning(
175         NULL, QString("Texture file not found"),
176         QString("Unable to load wood texture (%1).").arg(texFileName));
177     return;
178   }
179 
180   // 1E-3 needed. Just try with width=128 and see !
181   if ((img.width() != 1 << (int)(1 + log(img.width() - 1 + 1E-3) / log(2.0))) ||
182       (img.height() !=
183        1 << (int)(1 + log(img.height() - 1 + 1E-3) / log(2.0)))) {
184     QMessageBox::warning(
185         NULL, QString("Incorrect texture"),
186         QString("Wood texture dimensions are not powers of 2."));
187     return;
188   }
189 
190   QImage glImg = QGLWidget::convertToGLFormat(img); // flipped 32bit RGBA
191 
192   // Bind the img texture...
193   glTexImage2D(GL_TEXTURE_2D, 0, 4, glImg.width(), glImg.height(), 0, GL_RGBA,
194                GL_UNSIGNED_BYTE, glImg.bits());
195   glDisable(GL_TEXTURE_2D);
196 }
197 
initBoard()198 void AgoraViewer::initBoard() {
199   board_ = new Board();
200 
201   QStringList boardDir;
202   boardFileName_ = "";
203   boardDir << "AgoraBoards"
204            << "../AgoraBoards"
205            << "../../AgoraBoards";
206   for (int i = 0; i < boardDir.size(); ++i) {
207     if (QFileInfo(boardDir.at(i)).isDir()) {
208       boardFileName_ = boardDir.at(i) + "/standard.ago";
209       break;
210     }
211   }
212 
213   if (boardFileName_.isEmpty()) {
214     QMessageBox::warning(NULL, "Unable to find agora board directory",
215                          "Unable to find agora board directory\n(tried " +
216                              boardDir.join(", ") + ")");
217     return;
218   }
219 
220   newGame();
221 }
222 
initSpotLight()223 void AgoraViewer::initSpotLight() {
224   glMatrixMode(GL_MODELVIEW);
225   glEnable(GL_LIGHT1);
226   glLoadIdentity();
227 
228   // Light default parameters
229   const GLfloat light_ambient[4] = {2.0, 2.0, 2.0, 1.0};
230   const GLfloat light_specular[4] = {1.0, 1.0, 1.0, 1.0};
231   const GLfloat light_diffuse[4] = {2.0, 2.0, 2.0, 1.0};
232 
233   glLightf(GL_LIGHT1, GL_SPOT_EXPONENT, 2.0);
234   glLightf(GL_LIGHT1, GL_SPOT_CUTOFF, 60.0);
235   glLightf(GL_LIGHT1, GL_CONSTANT_ATTENUATION, 0.1f);
236   glLightf(GL_LIGHT1, GL_LINEAR_ATTENUATION, 0.3f);
237   glLightf(GL_LIGHT1, GL_QUADRATIC_ATTENUATION, 0.3f);
238   glLightfv(GL_LIGHT1, GL_AMBIENT, light_ambient);
239   glLightfv(GL_LIGHT1, GL_SPECULAR, light_specular);
240   glLightfv(GL_LIGHT1, GL_DIFFUSE, light_diffuse);
241 }
242 
newGame()243 void AgoraViewer::newGame() {
244   if (!QFileInfo(boardFileName_).isReadable()) {
245     QMessageBox::warning(NULL, "Unreadable board file",
246                          "Unable to read board file (" + boardFileName_ +
247                              ").\nLoad another board.");
248     return;
249   }
250 
251 #if QT_VERSION < 0x040000
252   std::ifstream f(boardFileName_.latin1());
253 #else
254   std::ifstream f(boardFileName_.toLatin1().constData());
255 #endif
256   f >> *board_;
257   f.close();
258 
259   selectedPiece_ = -1;
260 
261   playNextMove();
262 }
263 
fitCameraToBoard()264 void AgoraViewer::fitCameraToBoard() {
265   static BoardConstraint *boardConstraint = new BoardConstraint();
266 
267   // Free camera rotation motion
268   camera()->frame()->setConstraint(NULL);
269 
270   setSceneCenter(
271       Vec(board_->size().width() / 2.0, board_->size().height() / 2.0, 0.0));
272   setSceneRadius(sceneCenter().norm());
273 
274   camera()->setUpVector(Vec(0.0, 0.0, 1.0));
275   camera()->setPosition(Vec(2.0f * sceneCenter().x, -1.5f * sceneCenter().y,
276                             0.9f * (sceneCenter().x + sceneCenter().y)));
277   camera()->lookAt(sceneCenter());
278   // showEntireScene();
279 
280   // Limit camera rotation motion
281   camera()->frame()->setConstraint(boardConstraint);
282 }
283 
284 // D r a w i n g   f u n c t i o n s
draw()285 void AgoraViewer::draw() {
286   glEnable(GL_LIGHTING);
287 
288   if (animationStep_ > 0) {
289     glPushMatrix();
290     glMultMatrixd(kfi_->frame()->matrix());
291     // Board::drawPiece(board_->blackPlays());
292     glPopMatrix();
293 
294     // if (animationStep_ > 1)
295     //	board_->drawFlippingPieces(currentMove_.end(), animationStep_%2);
296   }
297 
298   const GLfloat pos[4] = {board_->size().width() / 2.0,
299                           board_->size().height() / 2.0, 3.0, 1.0};
300   const GLfloat spot_dir[3] = {0.0, 0.0, -1.0};
301   glLightfv(GL_LIGHT1, GL_POSITION, pos);
302   glLightfv(GL_LIGHT1, GL_SPOT_DIRECTION, spot_dir);
303 
304   board_->draw();
305 
306   if (selectedPiece_ >= 0) {
307     board_->drawSelectedPiece(selectedPiece_);
308     if (displayPossibleMoves_)
309       board_->drawPossibleDestinations(selectedPiece_);
310   }
311 }
312 
313 // G a m e   I n t e r f a c e
play(const Move & m)314 void AgoraViewer::play(const Move &m) {
315   currentMove_ = m;
316   if (animatePlays_)
317     animatePlay();
318   else
319     simplePlay();
320 }
321 
simplePlay()322 void AgoraViewer::simplePlay() {
323   board_->play(currentMove_);
324   update();
325   playNextMove();
326 }
327 
playNextMove()328 void AgoraViewer::playNextMove() {
329   static const QString stateFileName = "board.ago";
330 
331 // Updates statusBar
332 #if QT_VERSION < 0x040000
333   ((QMainWindow *)(((QWidget *)parent())->parent()))
334       ->statusBar()
335       ->message(board_->statusMessage());
336 #else
337   ((QMainWindow *)(((QWidget *)parent())->parent()))
338       ->statusBar()
339       ->showMessage(board_->statusMessage());
340 #endif
341 
342   if (board_->gameIsOver())
343     QMessageBox::information(this, "Game over", board_->statusMessage());
344   else {
345 // Save board state
346 #if QT_VERSION < 0x040000
347     std::ofstream f(stateFileName.latin1());
348 #else
349     std::ofstream f(stateFileName.toLatin1().constData());
350 #endif
351     f << *board_;
352     f.close();
353 
354     computerPlayer_[board_->blackPlays()].play(
355         board_->blackPlays(), stateFileName, board_->nbMovesLeft());
356   }
357 }
358 
playComputerMove(QString move,int duration)359 void AgoraViewer::playComputerMove(QString move, int duration) {
360   QString message = colorString[board_->blackPlays()] +
361                     " program played %1 in %2 out of %3 seconds.";
362   message =
363       message.arg(move)
364           .arg(duration / 1000.0, 0, 'f', 1)
365           .arg(computerPlayer_[board_->blackPlays()].allowedTime() / 1000.0, 0,
366                'f', 1);
367 #if QT_VERSION < 0x040000
368   qWarning(message.latin1());
369 #else
370   qWarning("%s", message.toLatin1().constData());
371 #endif
372   Move m(move);
373   if (m.isValid(board_))
374     play(m);
375   else
376     QMessageBox::warning(this, "Unvalid move",
377                          "The move " + move + " is not valid for " +
378                              colorString[board_->blackPlays()]);
379 }
380 
animatePlay()381 void AgoraViewer::animatePlay() {
382   kfi_->deletePath();
383   // Cut/pasted from board posOf(). Not shared to prevent QGLViewer dependency
384   // in board.h
385 
386   //	kfi_->addKeyFrame(Frame(Vec(currentMove_.start().x()+0.5,currentMove_.start().y()+0.5,0.0),
387   //Quaternion()));
388   //	kfi_->addKeyFrame(Frame(Vec((currentMove_.start().x()+currentMove_.end().x()+1.0)/2.0,
389   //		(currentMove_.start().y()+currentMove_.end().y()+1.0)/2.0, 1.0),
390   //Quaternion()), 0.6f);
391   //  kfi_->addKeyFrame(Frame(Vec(currentMove_.end().x()+0.5,currentMove_.end().y()+0.5,0.0),
392   //  Quaternion()), 1.2f);
393   kfi_->addKeyFrame(Frame(
394       Vec(currentMove_.end().x() + 0.5, currentMove_.end().y() + 0.5, 0.0),
395       Quaternion()));
396 
397   animationStep_ = 1;
398   kfi_->startInterpolation();
399 }
400 
401 // M o v e   S e l e c t i o n
drawWithNames()402 void AgoraViewer::drawWithNames() {
403   // Selection enabled only when the computer program is not active
404   if ((!computerPlayer_[board_->blackPlays()].isActive()) &&
405       (animationStep_ == 0)) {
406     if (selectedPiece_ >= 0)
407       board_->drawPossibleDestinations(selectedPiece_, true);
408 
409     // Always drawned, so that a new selection can occur
410     board_->drawSelectablePieces();
411   }
412 }
413 
postSelection(const QPoint &)414 void AgoraViewer::postSelection(const QPoint &) {
415   if (selectedPiece_ >= 0) {
416     if (selectedName() >= 0)
417       if (selectedName() == selectedPiece_)
418         selectedPiece_ = -1;
419       else {
420         Move m(board_, selectedPiece_, selectedName(),
421                qApp->keyboardModifiers() != Qt::NoModifier);
422         if (m.isValid(board_)) {
423           selectedPiece_ = -1;
424           play(m);
425         } else if (board_->canBeSelected(selectedName()))
426           selectedPiece_ = selectedName();
427         else
428           selectedPiece_ = -1;
429       }
430     else
431       selectedPiece_ = -1;
432   } else if (selectedName() >= 0)
433     selectedPiece_ = selectedName();
434 }
435 
436 // F i l e   m e n u   f u n c t i o n s
selectBoardFileName()437 void AgoraViewer::selectBoardFileName() {
438 #if QT_VERSION < 0x040000
439   boardFileName_ = QFileDialog::getOpenFileName(
440       "Boards", "Agora files (*.ago);;All files (*)", this);
441 #else
442   boardFileName_ =
443       QFileDialog::getOpenFileName(this, "Select a saved game", boardFileName_,
444                                    "Agora files (*.ago);;All files (*)");
445 #endif
446 }
447 
load()448 void AgoraViewer::load() {
449   selectBoardFileName();
450 
451   // In case of Cancel
452   if (boardFileName_.isEmpty())
453     return;
454 
455   newGame();
456   fitCameraToBoard();
457 }
458 
save()459 void AgoraViewer::save() {
460 #if QT_VERSION < 0x040000
461   std::ofstream f(boardFileName_.latin1());
462 #else
463   std::ofstream f(boardFileName_.toLatin1().constData());
464 #endif
465   f << *board_;
466   f.close();
467 }
468 
saveAs()469 void AgoraViewer::saveAs() {
470 #if QT_VERSION < 0x040000
471   boardFileName_ = QFileDialog::getSaveFileName(
472       "Boards", "Agora files (*.ago);;All files (*)", this,
473       boardFileName_.latin1());
474 #else
475   boardFileName_ = QFileDialog::getSaveFileName(
476       this, "Save game", boardFileName_, "Agora files (*.ago);;All files (*)");
477 #endif
478 
479   QFileInfo fi(boardFileName_);
480 
481 #if QT_VERSION < 0x040000
482   if (fi.extension().isEmpty())
483 #else
484   if (fi.suffix().isEmpty())
485 #endif
486   {
487     boardFileName_ += ".ago";
488     fi.setFile(boardFileName_);
489   }
490 
491 #if QT_VERSION < 0x040000
492   if (fi.exists()) {
493     if (QMessageBox::warning(
494             this, "Existing file",
495             "File " + fi.fileName() + " already exists.\nSave anyway ?",
496             QMessageBox::Yes, QMessageBox::Cancel) == QMessageBox::Cancel)
497       return;
498 
499     if (!fi.isWritable()) {
500       QMessageBox::warning(this, "Cannot save",
501                            "File " + fi.fileName() + " is not writeable.");
502       return;
503     }
504   }
505 #endif
506 
507   save();
508 }
509 
510 // G a m e   m e n u   m e t h o d s
511 
blackPlayerIsHuman(bool on)512 void AgoraViewer::blackPlayerIsHuman(bool on) { setPlayerIsHuman(on, true); }
513 
whitePlayerIsHuman(bool on)514 void AgoraViewer::whitePlayerIsHuman(bool on) { setPlayerIsHuman(on, false); }
515 
configureBlackPlayer()516 void AgoraViewer::configureBlackPlayer() { configurePlayer(true); }
517 
configureWhitePlayer()518 void AgoraViewer::configureWhitePlayer() { configurePlayer(false); }
519 
setPlayerIsHuman(bool on,bool black)520 void AgoraViewer::setPlayerIsHuman(bool on, bool black) {
521   computerPlayer_[black].setIsActive(!on);
522   playNextMove();
523 }
524 
configurePlayer(bool black)525 void AgoraViewer::configurePlayer(bool black) {
526   computerPlayer_[black].configure();
527 }
528 
529 // U n d o   a n d   R e do
undo()530 void AgoraViewer::undo() {
531   if ((animationStep_ == 0) && (board_->undo()))
532     finalizeUndoRedo();
533 }
534 
redo()535 void AgoraViewer::redo() {
536   if ((animationStep_ == 0) && (board_->redo()))
537     finalizeUndoRedo();
538 }
539 
finalizeUndoRedo()540 void AgoraViewer::finalizeUndoRedo() {
541   selectedPiece_ = -1;
542   update();
543 #if QT_VERSION < 0x040000
544   undoTimer_->start(1000, true);
545 #else
546   undoTimer_->start(1000);
547 #endif
548 }
549 
about()550 void AgoraViewer::about() {
551   QMessageBox::about(
552       this, "A g o r a",
553       "A g o r a\nCreated by Gilles Debunne\nVersion 2.0 - January 2008");
554 }
555 
displayRules()556 void AgoraViewer::displayRules() {
557   static QTextEdit *te;
558 
559   static const QString rules =
560       "<b>A g o r a</b><br><br>"
561       "Agora is a strategy game for two players.<br><br>"
562       "The goal is to have the maximum number of pieces of your color when the "
563       "number of allowed moves is reached "
564       "(or to convert all the pieces into your color before that).<br><br>"
565       "A piece can be moved on a nearby square... "
566       "details to come.<br><br>"
567       "Enjoy !";
568 
569   if (!te) {
570     te = new QTextEdit(rules);
571     te->resize(500, 300);
572   }
573 
574   te->show();
575 }
576