LCOV - code coverage report
Current view: top level - Source - searcher.cpp (source / functions) Coverage Total Hit
Test: coverage Lines: 48.8 % 246 120
Test Date: 2026-03-02 16:42:41 Functions: 75.0 % 32 24

            Line data    Source code
       1              : #include "searcher.hpp"
       2              : 
       3              : #include "com.hpp"
       4              : #include "dynamicConfig.hpp"
       5              : #include "logging.hpp"
       6              : #include "moveApply.hpp"
       7              : #include "xboard.hpp"
       8              : 
       9         5075 : TimeType Searcher::getCurrentMoveMs()const{
      10         5075 :    if (ThreadPool::instance().main().getData().isPondering || ThreadPool::instance().main().getData().isAnalysis) { return INFINITETIME; }
      11              : 
      12         5073 :    TimeType ret = currentMoveMs;
      13         5073 :    if (TimeMan::msecUntilNextTC > 0) {
      14              :       bool extented = false;
      15            2 :       switch (moveDifficulty) {
      16            0 :          case MoveDifficultyUtil::MD_forced: 
      17              :             // only one move in movelist !
      18            0 :             ret = (ret >> 4); 
      19              :             break; 
      20            0 :          case MoveDifficultyUtil::MD_easy: 
      21              :             ///@todo this is not used anymore
      22            0 :             ret = (ret >> 3); 
      23              :             break;   
      24              :          case MoveDifficultyUtil::MD_std: 
      25              :             // nothing special
      26              :             break;
      27            0 :          case MoveDifficultyUtil::MD_moobAttackIID:
      28              :             // score is decreasing during IID but still quite high (IID moob, sharp position)
      29            0 :             ret = static_cast<TimeType>(std::min(TimeMan::msecUntilNextTC / MoveDifficultyUtil::maxStealDivisor, ret * MoveDifficultyUtil::emergencyFactorIIDGood));
      30              :             extented = true;
      31              :             break; 
      32            0 :          case MoveDifficultyUtil::MD_moobDefenceIID:
      33              :             // score is decreasing during IID and it's not smelling good (sharp position)
      34              :             extented = true;
      35            0 :             ret = static_cast<TimeType>(std::min(TimeMan::msecUntilNextTC / MoveDifficultyUtil::maxStealDivisor, ret * MoveDifficultyUtil::emergencyFactorIID));
      36              :             break; 
      37              :       }
      38              :       if (!extented) {
      39            2 :          switch (positionEvolution) {
      40              :             case MoveDifficultyUtil::PE_none:
      41              :             case MoveDifficultyUtil::PE_std: 
      42              :                // nothing special
      43              :                break;
      44            0 :             case MoveDifficultyUtil::PE_boomingAttack:
      45              :                // let's validate this a little more
      46            0 :                ret = static_cast<TimeType>(std::min(TimeMan::msecUntilNextTC / MoveDifficultyUtil::maxStealDivisor, ret * MoveDifficultyUtil::emergencyFactorBoomHistory));
      47            0 :                break; 
      48            0 :             case MoveDifficultyUtil::PE_boomingDefence:
      49              :                // let's validate this a little more
      50            0 :                ret = static_cast<TimeType>(std::min(TimeMan::msecUntilNextTC / MoveDifficultyUtil::maxStealDivisor, ret * MoveDifficultyUtil::emergencyFactorBoomHistory));
      51            0 :                break; 
      52            0 :             case MoveDifficultyUtil::PE_moobingAttack:
      53              :                // let's try to understand better
      54            0 :                ret = static_cast<TimeType>(std::min(TimeMan::msecUntilNextTC / MoveDifficultyUtil::maxStealDivisor, ret * MoveDifficultyUtil::emergencyFactorMoobHistory));
      55            0 :                break; 
      56            0 :             case MoveDifficultyUtil::PE_moobingDefence:
      57              :                // let's try to defend
      58            0 :                ret = static_cast<TimeType>(std::min(TimeMan::msecUntilNextTC / MoveDifficultyUtil::maxStealDivisor, ret * MoveDifficultyUtil::emergencyFactorMoobHistory));
      59            0 :                break; 
      60              :          }
      61              :       }
      62              :    }
      63              :    // take variability into account
      64         5073 :    ret = std::min(TimeMan::maxTime, static_cast<TimeType>(static_cast<float>(ret) * MoveDifficultyUtil::variabilityFactor())); // inside [0.5 .. 2]
      65         5073 :    return std::max(ret, static_cast<TimeType>(20)); // if not much time left, let's try something ...;
      66              : }
      67              : 
      68      4400925 : void Searcher::getCMHPtr(const unsigned int ply, CMHPtrArray& cmhPtr) {
      69              :    cmhPtr.fill(nullptr);
      70      8801850 :    for (unsigned int k = 0; k < MAX_CMH_PLY; ++k) {
      71      4400925 :       assert(static_cast<int>(ply) - static_cast<int>(2*k) < MAX_PLY && static_cast<int>(ply) - static_cast<int>(2*k) >= 0);
      72      4400925 :       if (ply > 2*k && isValidMove(stack[ply - 2*k].p.lastMove)) {
      73              :          const Position & pRef = stack[ply - 2*k].p;
      74      4152185 :          const Square to = correctedMove2ToKingDest(pRef.lastMove);
      75      4152185 :          const int ptIdx = isEnPassant(pRef.lastMove) ? PieceIdx(P_wp) : PieceIdx(pRef.board_const(to));
      76      4152185 :          cmhPtr[k] = &historyT.counter_history[ptIdx][to];
      77              :       }
      78              :    }
      79      4400925 : }
      80              : 
      81          208 : bool Searcher::isBooming(uint16_t halfmove){
      82              :    assert(halfmove >= 0);
      83          208 :    if (halfmove < 4) { return false; }  // no booming at the beginning of the game of course
      84          192 :    if (stack[halfmove-2].h == nullHash) { return false; } // no record in previous state
      85          191 :    if (stack[halfmove-4].h == nullHash) { return false; } // no record in former state
      86              :    constexpr ScoreType boomMargin = 80;
      87          190 :    return stack[halfmove-4].eval <= stack[halfmove-2].eval + boomMargin;
      88              : }
      89              : 
      90           35 : bool Searcher::isMoobing(uint16_t halfmove){
      91              :    assert(halfmove >= 0);
      92           35 :    if (halfmove < 4) { return false; }  // no moobing at the beginning of the game of course
      93           19 :    if (stack[halfmove-2].h == nullHash) { return false; } // no record in previous state
      94           18 :    if (stack[halfmove-4].h == nullHash) { return false; } // no record in former state
      95              :    constexpr ScoreType moobMargin = 80;
      96           17 :    return stack[halfmove-4].eval >= stack[halfmove-2].eval + moobMargin;
      97              : }
      98              : 
      99     22856171 : ScoreType Searcher::getCMHScore(const Position& p, const Square from, const Square to, const CMHPtrArray& cmhPtr) const {
     100              :    ScoreType ret = 0;
     101     68568513 :    for (int i = 0; i < MAX_CMH_PLY; i++) {
     102     22856171 :       if (cmhPtr[i]) { ret += (*cmhPtr[i])[PieceIdx(p.board_const(from)) * NbSquare + to]; }
     103              :    }
     104     22856171 :    return ret/MAX_CMH_PLY;
     105              : }
     106              : 
     107            0 : bool Searcher::isCMHGood(const Position& p, const Square from, const Square to, const CMHPtrArray& cmhPtr, const ScoreType threshold) const {
     108            0 :    for (int i = 0; i < MAX_CMH_PLY; i++) {
     109            0 :       if (cmhPtr[i]) {
     110            0 :          const auto cmhScore = (*cmhPtr[i])[PieceIdx(p.board_const(from)) * NbSquare + to];
     111            0 :          if (cmhScore >= threshold){
     112              :                /*
     113              :                std::cout << ToString(p) << std::endl;
     114              :                std::cout << SquareNames[from] << std::endl;
     115              :                std::cout << SquareNames[to] << std::endl;
     116              :                std::cout << cmhScore << std::endl;
     117              :                */
     118              :                return true;
     119              :          }
     120              :       }
     121              :    }
     122              :    return false;
     123              : }
     124              : 
     125       526842 : bool Searcher::isCMHBad(const Position& p, const Square from, const Square to, const CMHPtrArray& cmhPtr, const ScoreType threshold) const {
     126              :    int nbBad = 0;
     127      1580526 :    for (int i = 0; i < MAX_CMH_PLY; i++) {
     128       526842 :       if (cmhPtr[i]) {
     129       460444 :          if ((*cmhPtr[i])[PieceIdx(p.board_const(from)) * NbSquare + to] < threshold) ++nbBad;
     130              :       }
     131              :    }
     132       526842 :    return nbBad == MAX_CMH_PLY;
     133              : }
     134              : 
     135         6655 : ScoreType Searcher::drawScore(const Position& p, DepthType height) const {
     136              :    // handles chess variants
     137              :    ///@todo other chess variants
     138         6655 :    if (DynamicConfig::armageddon) {
     139            0 :       if (p.c == Co_White) return matedScore(height);
     140            0 :       else return matingScore(height-1);
     141              :    }
     142         6655 :    return static_cast<ScoreType>(-1 + 2 * ((stats.counters[Stats::sid_nodes] + stats.counters[Stats::sid_qnodes]) % 2));
     143              : }
     144              : 
     145           26 : void Searcher::idleLoop() {
     146           26 :    _searching = false;
     147              :    while (true) {
     148          249 :       std::unique_lock lock(_mutex);
     149          249 :       Logging::LogIt(Logging::logInfo) << "begin of idleloop " << id();
     150          249 :       _cv.notify_one(); // Wake up anyone waiting for search finished
     151          498 :       _cv.wait(lock, [&] { return _searching; });
     152          249 :       if (_exit) {
     153           26 :          Logging::LogIt(Logging::logInfo) << "Exiting thread loop " << id();
     154           26 :          return;
     155              :       }
     156          223 :       lock.unlock();
     157          223 :       searchLauncher(); // blocking
     158          223 :       Logging::LogIt(Logging::logInfo) << "end of idleloop " << id();
     159              :    }
     160              : }
     161              : 
     162          249 : void Searcher::startThread() {
     163          249 :    std::lock_guard lock(_mutex);
     164          249 :    Logging::LogIt(Logging::logInfo) << "Starting worker " << id();
     165          249 :    _searching = true;
     166          249 :    Logging::LogIt(Logging::logInfo) << "Setting stopflag to false on worker " << id();
     167          249 :    stopFlag   = false;
     168          249 :    _cv.notify_one(); // Wake up the thread in idleLoop()
     169          249 : }
     170              : 
     171          273 : void Searcher::wait() {
     172          273 :    std::unique_lock lock(_mutex);
     173          273 :    Logging::LogIt(Logging::logInfo) << "Thread waiting " << id();
     174          305 :    _cv.wait(lock, [&] { return !_searching; });
     175          273 : }
     176              : 
     177              : // multi-threaded search (blocking call)
     178          223 : void Searcher::searchLauncher() {
     179          223 :    Logging::LogIt(Logging::logInfo) << "Search launched for thread " << id();
     180              :    // starts other threads first but they are locked for now ...
     181          208 :    if (isMainThread()) { ThreadPool::instance().startOthers(); }
     182              :    // so here searchDriver() will update the thread _data structure
     183          223 :    searchDriver();
     184          223 : }
     185              : 
     186         1715 : size_t Searcher::id() const { return _index; }
     187              : 
     188     10660799 : bool Searcher::isMainThread() const { return id() == 0; }
     189              : 
     190           26 : Searcher::Searcher(size_t n): _index(n), _exit(false), _searching(true), _stdThread(&Searcher::idleLoop, this) {
     191           26 :    startTime = Clock::now();
     192           26 :    wait(); // wait for idleLoop to start in the _stdThread object
     193           26 : }
     194              : 
     195           26 : Searcher::~Searcher() {
     196           26 :    _exit = true;
     197           26 :    startThread();
     198           26 :    Logging::LogIt(Logging::logInfo) << "Waiting for thread worker to join...";
     199           26 :    _stdThread.join();
     200           52 : }
     201              : 
     202          223 : void Searcher::setData(const ThreadData& d) {
     203          223 :    _data = d; // this is a copy
     204            0 : }
     205              : 
     206         1256 : ThreadData& Searcher::getData() { return _data; }
     207              : 
     208            0 : const ThreadData& Searcher::getData() const { return _data; }
     209              : 
     210         4683 : SearchData& Searcher::getSearchData() { return _data.datas; }
     211              : 
     212            0 : const SearchData& Searcher::getSearchData() const { return _data.datas; }
     213              : 
     214         4068 : bool Searcher::searching() const { return _searching; }
     215              : 
     216           52 : void Searcher::clearGame() {
     217              :    clearPawnTT();
     218           52 :    stats.init();
     219           52 :    killerT.initKillers();
     220           52 :    historyT.initHistory();
     221           52 :    counterT.initCounter();
     222           52 :    previousBest = INVALIDMOVE;
     223              : 
     224              :    // clear stack data
     225       106548 :    for (auto & d : stack){
     226       106496 :       d = {Position(), nullHash, 0, INVALIDMINIMOVE};
     227              :    }
     228       106496 : }
     229              : 
     230          223 : void Searcher::clearSearch(bool forceHistoryClear) {
     231              : #ifdef REPRODUCTIBLE_RESULTS
     232              :    clearPawnTT();
     233              :    forceHistoryClear = true;
     234              : #endif
     235          223 :    stats.init();
     236          223 :    killerT.initKillers();
     237          223 :    if (forceHistoryClear) historyT.initHistory();
     238          223 :    counterT.initCounter();
     239          223 :    previousBest = INVALIDMOVE;
     240          223 : }
     241              : 
     242           26 : void Searcher::initPawnTable() {
     243           26 :    Logging::LogIt(Logging::logInfo) << "Init Pawn TT (one per thread)";
     244           26 :    Logging::LogIt(Logging::logInfo) << "PawnEntry size " << sizeof(PawnEntry);
     245           52 :    ttSizePawn = powerFloor((SIZE_MULTIPLIER * DynamicConfig::ttPawnSizeMb / DynamicConfig::threads) / sizeof(PawnEntry));
     246           26 :    assert(BB::countBit(ttSizePawn) == 1); // a power of 2 and not 0 ...
     247      3014682 :    tablePawn.reset(new PawnEntry[ttSizePawn]);
     248           52 :    Logging::LogIt(Logging::logInfo) << "Size of Pawn TT " << ttSizePawn * sizeof(PawnEntry) / 1024 << "Kb (" << ttSizePawn << " entries)";
     249           26 : }
     250              : 
     251            0 : void Searcher::clearPawnTT() {
     252      6422580 :    for (unsigned int k = 0; k < ttSizePawn; ++k) tablePawn[k].h = nullHash;
     253            0 : }
     254              : 
     255       796968 : bool Searcher::getPawnEntry(Hash h, PawnEntry*& pe) {
     256       796968 :    assert(h != nullHash);
     257       796968 :    PawnEntry& _e = tablePawn[h & (ttSizePawn - 1)];
     258       796968 :    pe = &_e;
     259       796968 :    if (_e.h != Hash64to32(h)) return false;
     260              :    ///@todo check for hash collision ?
     261              :    stats.incr(Stats::sid_ttPawnhits);
     262       648214 :    return !DynamicConfig::disableTT;
     263              : }
     264              : 
     265       796968 : void Searcher::prefetchPawn(Hash h) {
     266       796968 :    void* addr = (&tablePawn[h & (ttSizePawn - 1)]);
     267              : #if defined(__INTEL_COMPILER)
     268              :    __asm__("");
     269              : #elif defined(_MSC_VER)
     270              :    _mm_prefetch((char*)addr, _MM_HINT_T0);
     271              : #else
     272       796968 :    __builtin_prefetch(addr);
     273              : #endif
     274       796968 : }
     275              : 
     276              : std::atomic<bool> Searcher::startLock;
     277              : 
     278            0 : Searcher& Searcher::getCoSearcher(size_t id) {
     279            0 :    static std::map<size_t, std::unique_ptr<Searcher>> coSearchers;
     280              :    // init new co-searcher if not already present
     281            0 :    if (!coSearchers.contains(id)) {
     282            0 :       coSearchers[id] = std::unique_ptr<Searcher>(new Searcher(id + MAX_THREADS));
     283            0 :       coSearchers[id]->initPawnTable();
     284              :    }
     285            0 :    return *coSearchers[id];
     286              : }
     287              : 
     288            0 : Position Searcher::getQuiet(const Position& p, Searcher* searcher, ScoreType* qScore) {
     289              :    // fixed co-searcher if not given
     290            0 :    Searcher& cos = getCoSearcher(searcher ? searcher->id() : 2 * MAX_THREADS);
     291            0 :    cos.clearSearch(true);
     292              : 
     293            0 :    PVList    pv;
     294              :    DepthType height   = 1;
     295            0 :    DepthType seldepth = 0;
     296              :    ScoreType s        = 0;
     297              : 
     298            0 :    Position pQuiet = p; // because p is given const
     299              : #ifdef WITH_NNUE
     300            0 :    NNUEEvaluator evaluator;
     301              :    pQuiet.associateEvaluator(evaluator);
     302            0 :    pQuiet.resetNNUEEvaluator(pQuiet.evaluator());
     303              : #endif
     304              : 
     305              :    // go for a qsearch (no pruning, open bounds)
     306            0 :    cos.stopFlag = false;
     307            0 :    cos.currentMoveMs = INFINITETIME;
     308            0 :    cos.isStoppableCoSearcher = true;
     309            0 :    cos.subSearch = true;
     310              : 
     311            0 :    s = cos.qsearchNoPruning(-10000, 10000, pQuiet, height, seldepth, &pv);
     312              : 
     313            0 :    cos.subSearch = false;
     314              : 
     315            0 :    if (qScore) *qScore = s;
     316              : 
     317              :    //std::cout << "pv : " << ToString(pv) << std::endl;
     318              : 
     319              :    // goto qsearch leaf
     320            0 :    for (const auto& m : pv) {
     321            0 :       Position p2 = pQuiet;
     322              :       //std::cout << "Applying move " << ToString(m) << std::endl;
     323            0 :       if (const MoveInfo moveInfo(p2,m); !applyMove(p2, moveInfo)) break;
     324            0 :       pQuiet = p2;
     325            0 :    }
     326              : 
     327              : #ifdef WITH_NNUE
     328              :    pQuiet.dissociateEvaluator();
     329              : #endif
     330            0 :    return pQuiet;
     331            0 : }
     332              : 
     333              : #ifdef WITH_GENFILE
     334              : 
     335            0 : struct GenFENEntry{
     336              :    std::string fen;
     337              :    Move m;
     338              :    ScoreType s;
     339              :    uint16_t ply;
     340              :    Color stm;
     341              :    bool operator<(const GenFENEntry& other) const {
     342            0 :       return fen < other.fen;
     343              :    }
     344            0 :    void write(std::ofstream & stream, int result) const {
     345              :       stream << "fen " << fen << "\n"
     346            0 :              << "move " << ToString(m) << "\n"
     347            0 :              << "score " << s << "\n"
     348              :              //<< "eval "   << e << "\n"
     349            0 :              << "ply " << ply << "\n"
     350            0 :              << "result " << (stm == Co_White ? result : -result) << "\n"
     351            0 :              << "e" << "\n";
     352            0 :    }
     353              :    ///@todo writeBinary
     354              : };
     355              : 
     356            0 : void Searcher::writeToGenFile(const Position& p, bool getQuietPos, const ThreadData & d, const std::optional<int> result) {
     357              :    static uint64_t sfensWritten = 0;
     358              : 
     359            0 :    static std::set<GenFENEntry> buffer;
     360              : 
     361            0 :    ThreadData data = d; // copy data from PV
     362            0 :    Position pLeaf = p; // copy current pos
     363              : 
     364            0 :    if (getQuietPos){
     365              : 
     366            0 :       Searcher& cos = getCoSearcher(id());
     367            0 :       Logging::LogIt(Logging::logDebug) << "Looking for quiet position";
     368              : 
     369            0 :       const int          oldMinOutLvl  = DynamicConfig::minOutputLevel;
     370            0 :       const bool         oldDisableTT  = DynamicConfig::disableTT;
     371            0 :       const unsigned int oldLevel      = DynamicConfig::level;
     372            0 :       const unsigned int oldRandomOpen = DynamicConfig::randomOpen;
     373            0 :       const unsigned int oldRandomPly  = DynamicConfig::randomPly;
     374              : 
     375              :       // init sub search
     376              :       //DynamicConfig::minOutputLevel = Logging::logMax;
     377            0 :       DynamicConfig::disableTT      = true; // do not use TT in order to get qsearch leaf node
     378            0 :       DynamicConfig::level          = 100;
     379            0 :       DynamicConfig::randomOpen     = 0;
     380            0 :       DynamicConfig::randomPly      = 0;
     381              : 
     382              :       ///@todo in the following code the evaluator will be reset 3 times ! (getQuiet, before eval, at the beginning of searchDriver) ...
     383              : 
     384              :       // look for a quiet position using qsearch
     385            0 :       ScoreType qScore = 0;
     386            0 :       pLeaf = getQuiet(p, this, &qScore);
     387              : 
     388            0 :       Logging::LogIt(Logging::logDebug) << "quiet position is " << GetFEN(pLeaf); 
     389              : 
     390            0 :       ScoreType  e = 0;
     391            0 :       if (Abs(qScore) < 1000) {
     392              : #ifdef WITH_NNUE
     393            0 :          NNUEEvaluator evaluator;
     394              :          pLeaf.associateEvaluator(evaluator);
     395            0 :          pLeaf.resetNNUEEvaluator(pLeaf.evaluator());
     396              : #endif
     397              :          // evaluate quiet leaf position
     398            0 :          EvalData eData;
     399            0 :          e = eval(pLeaf, eData, cos, true, false);
     400              : 
     401            0 :          DynamicConfig::disableTT = false; // use TT here
     402            0 :          if (Abs(e) < 1000) {
     403            0 :             const Hash matHash = MaterialHash::getMaterialHash(p.mat);
     404              :             float gp = 1;
     405            0 :             if (matHash != nullHash) {
     406              :                const MaterialHash::MaterialHashEntry& MEntry = MaterialHash::materialHashTable[matHash];
     407              :                gp                                            = MEntry.gamePhase();
     408              :             }
     409            0 :             const DepthType depth = static_cast<DepthType>(clampDepth(DynamicConfig::genFenDepth) * gp + clampDepth(DynamicConfig::genFenDepthEG) * (1.f - gp));
     410              : 
     411            0 :             data.p     = pLeaf;
     412            0 :             data.depth = depth;
     413              :             cos.setData(data);
     414              : 
     415            0 :             cos.stopFlag = false;
     416            0 :             cos.subSearch = true;
     417            0 :             cos.currentMoveMs = INFINITETIME;
     418            0 :             cos.isStoppableCoSearcher = true;
     419            0 :             cos.clearSearch(true); // reset node count
     420            0 :             cos.subSearch = true;
     421              : 
     422            0 :             cos.searchDriver(false);
     423              : 
     424            0 :             cos.subSearch = false;
     425              : 
     426            0 :             data = cos.getData();
     427              :             // std::cout << data << std::endl; // debug
     428              :          }
     429              :       }
     430              : 
     431            0 :       DynamicConfig::minOutputLevel = oldMinOutLvl;
     432            0 :       DynamicConfig::disableTT      = oldDisableTT;
     433            0 :       DynamicConfig::level          = oldLevel;
     434            0 :       DynamicConfig::randomOpen     = oldRandomOpen;
     435            0 :       DynamicConfig::randomPly      = oldRandomPly;
     436              : 
     437              :       // end of sub search
     438              : 
     439              :    }
     440              :    // skip when bestmove is capture or when you have not reached randomPly limit yet
     441              :    else{
     442            0 :       if(isCapture(data.best) || pLeaf.halfmoves <= DynamicConfig::randomPly || pLeaf.halfmoves <= 10){
     443            0 :          data.best = INVALIDMOVE;
     444              :       }
     445              :    }
     446              : 
     447            0 :    if (data.best != INVALIDMOVE && Abs(data.score) < 1500) {
     448            0 :       buffer.emplace(GetFEN(pLeaf), data.best, data.score, pLeaf.halfmoves, pLeaf.c);
     449            0 :       ++sfensWritten;
     450            0 :       if (sfensWritten % 10'000 == 0) Logging::LogIt(Logging::logInfoPrio) << "Sfens written " << sfensWritten;
     451              :    }
     452              : 
     453            0 :    if (result.has_value()){
     454            0 :       Logging::LogIt(Logging::logInfo) << "Game ended, result " << result.value();
     455            0 :       for (const auto & entry : buffer){
     456            0 :          entry.write(genStream,result.value());
     457              :       }
     458              :       buffer.clear();
     459              :    }
     460              : 
     461            0 : }
     462              : #endif
        

Generated by: LCOV version 2.0-1