/* * Quackle -- Crossword game artificial intelligence and analysis tool * Copyright (C) 2005-2019 Jason Katz-Brown, John O'Laughlin, and John Fultz. * * 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 3 of the License, or * (at your option) any later version. * * 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 . */ #include #include #include "computerplayer.h" #include "datamanager.h" #include "game.h" #include "gameparameters.h" #include "move.h" #include "sim.h" #include "strategyparameters.h" // define this to get lame debugging messages //#define DEBUG_SIM using namespace Quackle; std::atomic_long SimmedMove::objectIdCounter{0}; Simulator::Simulator() : m_logfileIsOpen(false), m_hasHeader(false), m_dispatch(0), m_iterations(0), m_ignoreOppos(false) { m_originalGame.addPosition(); setThreadCount(2); } Simulator::~Simulator() { setThreadCount(0); closeLogfile(); } void Simulator::setPosition(const GamePosition &position) { if (hasSimulationResults()) writeLogFooter(); m_originalGame.setCurrentPosition(position); m_consideredMoves.clear(); m_simmedMoves.clear(); for (const auto &it : m_originalGame.currentPosition().moves()) m_simmedMoves.push_back(SimmedMove(it)); resetNumbers(); } void Simulator::setLogfile(const string &logfile, bool append) { if (m_logfile == logfile && isLogging()) return; closeLogfile(); m_logfile = logfile; if (m_logfile.empty()) { closeLogfile(); return; } const ios::openmode flags = append? (ios::out | ios::app) : ios::out; m_logfileStream.open(m_logfile.c_str(), flags); m_logfileIsOpen = m_logfileStream.is_open(); if (!m_logfileIsOpen) cerr << "Could not open " << m_logfile << " to write simulation log" << endl; m_hasHeader = false; } void Simulator::logMessage(const UVString &message) { if (isLogging()) m_logfileStream << message << endl; } void Simulator::closeLogfile() { if (isLogging()) { if (m_hasHeader) writeLogFooter(); m_logfileStream.close(); m_logfileIsOpen = false; } } void Simulator::writeLogHeader() { if (isLogging()) { m_logfileStream << "" << endl; m_xmlIndent = MARK_UV("\t"); m_hasHeader = true; // TODO include position data } } void Simulator::writeLogFooter() { if (isLogging()) { m_xmlIndent = MARK_UV(""); m_logfileStream << "" << endl; m_hasHeader = false; } } void Simulator::setDispatch(ComputerDispatch *dispatch) { m_dispatch = dispatch; } void Simulator::setIncludedMoves(const MoveList &moves) { for (auto &simmedMoveIt : m_simmedMoves) simmedMoveIt.setIncludeInSimulation(false); for (auto &it : moves) { bool found = false; for (auto &simmedMoveIt : m_simmedMoves) { if (it == simmedMoveIt.move) { simmedMoveIt.setIncludeInSimulation(true); found = true; break; } } if (!found) m_simmedMoves.push_back(SimmedMove(it)); } } void Simulator::makeSureConsideredMovesAreIncluded() { MoveList movesSuperset(moves(/* prune */ true, /* sort by win */ true)); for (const auto &it : m_consideredMoves) if (!movesSuperset.contains(it)) movesSuperset.push_back(it); setIncludedMoves(movesSuperset); } void Simulator::moveConsideredMovesToBeginning(MoveList &moves) const { for (const auto &consideredIt : m_consideredMoves) { for (auto it = moves.begin(); it != moves.end(); it++) { if (consideredIt == *it) { it = moves.erase(it); moves.insert(moves.begin(), consideredIt); } } } } void Simulator::addConsideredMove(const Move &move) { m_consideredMoves.push_back(move); } bool Simulator::isConsideredMove(const Move &move) const { const bool ret = m_consideredMoves.contains(move); return ret; } void Simulator::pruneTo(double equityThreshold, int maxNumberOfMoves) { MoveList equityMoves(moves(/* prune unincluded */ true)); MoveList toSetIncluded; const double absoluteEquityThreshold = equityMoves[0].equity - equityThreshold; const MoveList::const_iterator end = equityMoves.end(); int i = 0; for (MoveList::const_iterator it = equityMoves.begin(); i < maxNumberOfMoves && it != end; ++it, ++i) { if ((*it).equity >= absoluteEquityThreshold) toSetIncluded.push_back(*it); } setIncludedMoves(toSetIncluded); } void Simulator::resetNumbers() { for (auto &moveIt : m_simmedMoves) moveIt.clear(); m_iterations = 0; } void Simulator::simThreadFunc(SimmedMoveMessageQueue& incoming, SimmedMoveMessageQueue& outgoing) { while (true) { auto result = incoming.pop_or_terminate(); if (result.second) break; Simulator::simulateOnePosition(result.first, incoming.constants()); outgoing.push(result.first); } } void Simulator::setThreadCount(size_t count) { if (count == 0 && m_threadPool.size() != 0) { m_sendQueue.send_terminate_all(); for (auto& t : m_threadPool) t.join(); m_threadPool.clear(); } while (count > m_threadPool.size()) { m_threadPool.emplace_back(Simulator::simThreadFunc, std::ref(m_sendQueue), std::ref(m_receiveQueue)); } while (count < m_threadPool.size()) { m_sendQueue.send_terminate_one(m_threadPool.back().get_id()); m_threadPool.back().join(); m_threadPool.pop_back(); } } void Simulator::simulate(int plies, int iterations) { for (int i = 0; i < iterations; ++i) { if (m_dispatch && m_dispatch->shouldAbort()) break; simulate(plies); } } void Simulator::simulate(int plies) { #ifdef DEBUG_SIM UVcout << "let's simulate for " << plies << " plies" << endl; #endif ++m_iterations; randomizeOppoRacks(); randomizeDrawingOrder(); if (plies < 0) plies = 1000; // specified plies doesn't include candidate play ++plies; SimmedMoveConstants constants; constants.game = m_originalGame; constants.startPlayerId = m_originalGame.currentPosition().currentPlayer().id(); constants.playerCount = m_originalGame.currentPosition().players().size(); // level one's first move is the zeroth ply (the candidate) constants.decimalTurns = (plies % constants.playerCount); // also one-indexed constants.levelCount = (int)((plies - constants.decimalTurns) / constants.playerCount); constants.ignoreOppos = m_ignoreOppos; constants.isLogging = isLogging(); m_sendQueue.setConstants(constants); if (isLogging()) { if (!m_hasHeader) writeLogHeader(); m_logfileStream << m_xmlIndent << "" << endl; m_xmlIndent += MARK_UV('\t'); } int messageCount = 0; for (auto &moveIt : m_simmedMoves) { if (!moveIt.includeInSimulation()) continue; #ifdef DEBUG_SIM UVcout << "simulating " << (*moveIt).move << ":" << endl; #endif moveIt.levels.setNumberLevels(constants.levelCount + 1); SimmedMoveMessage message; message.id = moveIt.id(); message.move = moveIt.move; message.levels.setNumberLevels(constants.levelCount + 1); message.levels = moveIt.levels; message.xmlIndent = m_xmlIndent; m_sendQueue.push(message); messageCount++; } while (messageCount-- > 0) { SimmedMoveMessage message(m_receiveQueue.pop()); incorporateMessage(message); } if (isLogging()) { m_xmlIndent = m_xmlIndent.substr(0, m_xmlIndent.length() - 1); m_logfileStream << m_xmlIndent << "" << endl; } } void Simulator::simulateOnePosition(SimmedMoveMessage &message, const SimmedMoveConstants &constants) { Game game = constants.game; double residual = 0; int levelNumber = 1; for (LevelList::iterator levelIt = message.levels.begin(); levelNumber <= constants.levelCount + 1 && levelIt != message.levels.end() && !game.currentPosition().gameOver(); ++levelIt, ++levelNumber) { const int decimal = levelNumber == constants.levelCount + 1? constants.decimalTurns : constants.playerCount; if (decimal == 0) continue; (*levelIt).setNumberScores(decimal); int playerNumber = 1; for (auto &scoresIt : (*levelIt).statistics) { if (game.currentPosition().gameOver()) break; ++playerNumber; const int playerId = game.currentPosition().currentPlayer().id(); if (constants.isLogging) { message.logStream << message.xmlIndent << "" << endl; message.xmlIndent += MARK_UV('\t'); } Move move = Move::createNonmove(); if (playerId == constants.startPlayerId && levelNumber == 1) move = message.move; else if (constants.ignoreOppos && playerId != constants.startPlayerId) move = Move::createPassMove(); else move = game.currentPosition().staticBestMove(); int deadwoodScore = 0; if (game.currentPosition().doesMoveEndGame(move)) { LetterString deadwood; deadwoodScore = game.currentPosition().deadwood(&deadwood); // account for deadwood in this move rather than a separate // UnusedTilesBonus move. move.score += deadwoodScore; } scoresIt.score.incorporateValue(move.score); scoresIt.bingos.incorporateValue(move.isBingo? 1.0 : 0.0); if (constants.isLogging) { message.logStream << message.xmlIndent << game.currentPosition().currentPlayer().rack().xml() << endl; message.logStream << message.xmlIndent << move.xml() << endl; } // record future-looking residuals bool isFinalTurnForPlayerOfSimulation = false; if (levelNumber == constants.levelCount) isFinalTurnForPlayerOfSimulation = playerNumber > constants.decimalTurns; else if (levelNumber == constants.levelCount + 1) isFinalTurnForPlayerOfSimulation = playerNumber <= constants.decimalTurns; const bool isVeryFinalTurnOfSimulation = (constants.decimalTurns == 0 && levelNumber == constants.levelCount && playerNumber == constants.playerCount) || (levelNumber == constants.levelCount + 1 && playerNumber == constants.decimalTurns); if (isFinalTurnForPlayerOfSimulation && !(constants.ignoreOppos && playerId != constants.startPlayerId)) { double residualAddend = game.currentPosition().calculatePlayerConsideration(move); if (constants.isLogging) message.logStream << message.xmlIndent << "" << endl; if (isVeryFinalTurnOfSimulation) { // experimental -- do shared resource considerations // matter in a plied simulation? const double sharedResidual = game.currentPosition().calculateSharedConsideration(move); residualAddend += sharedResidual; if (constants.isLogging && sharedResidual != 0) message.logStream << message.xmlIndent << "" << endl; } if (playerId == constants.startPlayerId) residual += residualAddend; else residual -= residualAddend; } // commiting the move will account for deadwood again // so avoid double counting from above. move.score -= deadwoodScore; game.setCandidate(move); game.commitCandidate(!isVeryFinalTurnOfSimulation); if (constants.isLogging) { message.xmlIndent = message.xmlIndent.substr(0, message.xmlIndent.length() - 1); message.logStream << message.xmlIndent << "" << endl; } } } message.residual = residual; int spread = game.currentPosition().spread(constants.startPlayerId); message.gameSpread = spread; if (game.currentPosition().gameOver()) { message.bogowin = false; message.wins = spread > 0? 1 : spread == 0? 0.5 : 0; } else { message.bogowin = true; if (game.currentPosition().currentPlayer().id() == constants.startPlayerId) message.wins = QUACKLE_STRATEGY_PARAMETERS->bogowin((int)(spread + residual), game.currentPosition().bag().size() + QUACKLE_PARAMETERS->rackSize(), 0); else message.wins = 1.0 - QUACKLE_STRATEGY_PARAMETERS->bogowin((int)(-spread - residual), game.currentPosition().bag().size() + QUACKLE_PARAMETERS->rackSize(), 0); } } void Simulator::incorporateMessage(const SimmedMoveMessage &message) { if (isLogging()) m_logfileStream << message.logStream.str(); for (auto& moveIt : m_simmedMoves) { if (moveIt.id() == message.id) { if (isLogging()) { m_logfileStream << m_xmlIndent << "" << endl; m_xmlIndent += MARK_UV('\t'); } moveIt.levels = message.levels; moveIt.residual.incorporateValue(message.residual); moveIt.gameSpread.incorporateValue(message.gameSpread); moveIt.wins.incorporateValue(message.wins); if (isLogging()) { if (!message.bogowin) m_logfileStream << m_xmlIndent << "" << endl; m_xmlIndent = m_xmlIndent.substr(0, m_xmlIndent.length() - 1); m_logfileStream << m_xmlIndent << "" << endl; } break; } } } void Simulator::randomizeOppoRacks() { #ifdef DEBUG_SIM UVcout << "RANDOMIZE OPPO RACKS " << endl; #endif m_originalGame.currentPosition().ensureProperBag(); Bag bag(m_originalGame.currentPosition().unseenBag()); for (const auto &it : m_originalGame.currentPosition().players()) { if ((it == m_originalGame.currentPosition().currentPlayer())) continue; // TODO -- some kind of inference engine can be inserted here Rack rack = m_partialOppoRack; // We must refill the partial rack from a bag that does not // contain the partial rack. bag.removeLetters(rack.tiles()); bag.refill(rack); m_originalGame.currentPosition().setPlayerRack(it.id(), rack, /* adjust bag */ true); } #ifdef DEBUG_SIM UVcout << "RANDOMIZE OPPO RACKS DONE" << endl; #endif m_originalGame.currentPosition().ensureProperBag(); } void Simulator::setPartialOppoRack(const Rack &rack) { m_partialOppoRack = rack; } void Simulator::randomizeDrawingOrder() { m_originalGame.currentPosition().setDrawingOrder(m_originalGame.currentPosition().bag().someShuffledTiles()); } MoveList Simulator::moves(bool prune, bool byWin) const { MoveList ret; const bool useCalculatedEquity = hasSimulationResults(); for (const auto &it : m_simmedMoves) { if (prune && !it.includeInSimulation()) continue; Move move(it.move); if (useCalculatedEquity) { move.equity = it.calculateEquity(); move.win = it.wins.averagedValue(); } ret.push_back(move); } if (byWin && useCalculatedEquity) MoveList::sort(ret, MoveList::Win); else MoveList::sort(ret, MoveList::Equity); return ret; } const SimmedMove &Simulator::simmedMoveForMove(const Move &move) const { for (const auto &it : m_simmedMoves) if (it.move == move) return it; return m_simmedMoves.back(); } int Simulator::numLevels() const { if (m_simmedMoves.empty()) return 0; return m_simmedMoves.front().levels.size(); } int Simulator::numPlayersAtLevel(int levelIndex) const { if (m_simmedMoves.empty()) return 0; return m_simmedMoves.front().levels[levelIndex].statistics.size(); } //////////// double AveragedValue::standardDeviation() const { return m_incorporatedValues <= 1 ? 0 : sqrt( (m_incorporatedValues * m_squaredValueSum - m_valueSum * m_valueSum) / (m_incorporatedValues * (m_incorporatedValues - 1)) ); } void AveragedValue::clear() { m_valueSum = 0; m_squaredValueSum = 0; m_incorporatedValues = 0; } //////////// void SimmedMoveMessageQueue::push(SimmedMoveMessage& msg) { std::lock_guard lk(m_queueMutex); m_queue.push(std::move(msg)); m_condition.notify_one(); } std::pair SimmedMoveMessageQueue::pop_or_terminate() { std::unique_lock lk(m_queueMutex); while (m_queue.empty() && !m_terminateAll && m_terminateOne != std::this_thread::get_id()) m_condition.wait(lk); std::pair result; result.second = m_terminateAll || m_terminateOne == std::this_thread::get_id(); if (result.second) m_terminateOne = std::thread::id(); else { result.first = std::move(m_queue.front()); m_queue.pop(); } return result; } SimmedMoveMessage SimmedMoveMessageQueue::pop() { std::unique_lock lk(m_queueMutex); while (m_queue.empty()) m_condition.wait(lk); SimmedMoveMessage result = std::move(m_queue.front()); m_queue.pop(); return result; } void SimmedMoveMessageQueue::send_terminate_all() { std::lock_guard lk(m_queueMutex); m_terminateAll = true; m_condition.notify_all(); } void SimmedMoveMessageQueue::send_terminate_one(const std::thread::id& id) { std::lock_guard lk(m_queueMutex); m_terminateOne = id; m_condition.notify_all(); } //////////// double SimmedMove::calculateEquity() const { if (levels.empty()) { return move.equity; } double equity = 0; for (const auto &levelIt : levels) { for (PositionStatisticsList::const_iterator scoresIt = levelIt.statistics.begin(); scoresIt != levelIt.statistics.end(); scoresIt++) { if (scoresIt == levelIt.statistics.begin()) equity += (*scoresIt).score.averagedValue(); else equity -= (*scoresIt).score.averagedValue(); } } equity += residual.averagedValue(); return equity; } double SimmedMove::calculateWinPercentage() const { return wins.hasValues()? wins.averagedValue() * 100 : move.win; } void LevelList::setNumberLevels(unsigned int number) { while (size() < number) push_back(Level()); } void SimmedMove::clear() { levels.clear(); } PositionStatistics SimmedMove::getPositionStatistics(int level, int playerIndex) const { return levels[level].statistics[playerIndex]; } AveragedValue PositionStatistics::getStatistic(StatisticType type) const { switch (type) { case StatisticScore: return score; case StatisticBingos: return bingos; } return AveragedValue(); } //////////// void Level::setNumberScores(unsigned int number) { while (statistics.size() < number) statistics.push_back(PositionStatistics()); } ////////// UVOStream& operator<<(UVOStream &o, const Quackle::AveragedValue &value) { o << "[" << value.valueSum() << "/" << value.incorporatedValues() << "=" << value.averagedValue() << " sd " << value.standardDeviation() << "]"; return o; } UVOStream& operator<<(UVOStream &o, const Quackle::PositionStatistics &value) { o << "Stats: score " << value.score << ", bingos " << value.bingos << endl; return o; } UVOStream& operator<<(UVOStream &o, const Quackle::Level &level) { for (const auto &it : level.statistics) o << it; return o; } UVOStream& operator<<(UVOStream &o, const Quackle::SimmedMove &move) { o << "Simmed move " << move.move << ":"; int levelNumber = 0; for (const auto &it : move.levels) o << endl << "level " << ++levelNumber << ": " << it; o << endl; o << "Being simmed: " << move.includeInSimulation() << endl; o << "Residual: " << move.residual << endl; o << "Spread: " << move.gameSpread << endl; o << "Wins: " << move.wins << endl; return o; } UVOStream& operator<<(UVOStream& o, const Quackle::SimmedMoveList& moves) { for (const auto &it : moves) o << it << endl; return o; }