Prechádzať zdrojové kódy

Merge branch 'master' into conan_build

david 3 rokov pred
rodič
commit
72a501c8b0
13 zmenil súbory, kde vykonal 1343 pridanie a 222 odobranie
  1. 5 3
      TradeWars Proxy Notes.md
  2. 1 1
      buysell.h
  3. 52 19
      director.cpp
  4. 540 19
      dispatchers.cpp
  5. 59 3
      dispatchers.h
  6. 315 142
      galaxy.cpp
  7. 46 5
      galaxy.h
  8. 219 17
      scripts.cpp
  9. 33 2
      scripts.h
  10. 24 6
      test-director.cpp
  11. 22 1
      test-galaxy.cpp
  12. 18 4
      utils.cpp
  13. 9 0
      utils.h

+ 5 - 3
TradeWars Proxy Notes.md

@@ -2,11 +2,15 @@
 
 ## TODO
 
+* I have the ANSI music from Yankee Trader.
+* 
+* TradeDispatch -- return some value that says we've Traded/or there's nothing here to trade with.  (Keep calling TradeDispatch until nothing -- then move on!)
+* MoveDispatch -- return success that we moved to a sector.  (Should we give it two sectors that we want to trade with -- and have it move to the nearest/closest one?)  That way we don't waste a move (going to the furthest one, only to move back to the nearer)?
+* 
 * make a program that reads the logs and feeds it into the director.  
 * any problems -- exit and save your logs!  we can simulate/reproduce the problem.  :D
 * This can be the beginnings of tests.  Navigate 4-5 sectors, test that the galaxy is set correct.  Are the warps right, are the ports right.  
 * If I go into a sector with a port, can I do computer/ask about that port?  That might be a nicer way of exploring the galaxy -- no need to update ports, we'd already have the port's information! Yes, this works.  We also get what we have in our holds.  This is great for trading!
-
 * I'm able to tell which port I can start trading with, should I check that before moving?  Or is it not worth it to waste X turns?
 * FAILED:  I have fuel in my holds, ports are only selling fuel. 
 2021-11-03 00:26:03.586801    fatal (   director.cpp:252 ) Sector: 9994
@@ -20,9 +24,7 @@
 2021-11-03 00:26:03.401860    fatal (   director.cpp:488 ) 9994,987 : 3
 2021-11-03 00:26:03.401899    fatal (    scripts.cpp:14  ) ScriptTrader::activate 9994 & 987
 2021-11-03 00:26:03.401939    fatal (    scripts.cpp:21  ) 4 and 3
-
 * was able to trade ok with 987 & 15611....
-
 * I think there's a bug in the detect "burnt" ports.  I don't think it takes into consideration ports that I've seen but not docked with (all percents 0).  Whoops!
 * If there's a port that's BBB, and there's a SBS or BSS, it should use E.
 * We're not interested.  If we're doing single trade (not trade pairs), I probably need to redock with the port.

+ 1 - 1
buysell.h

@@ -12,7 +12,7 @@ enum PRODUCT { FUEL = 0, ORG = 1, EQU = 2 };
 
 // typedef std::array<bool, 3> buysell;
 struct buysell {
-  bool foe[3];
+  bool foe[3]; // TRUE = BUY
   bool operator==(const buysell& rhs) const;
   friend std::ostream& operator<<(std::ostream& os, const buysell& bs);
 };

+ 52 - 19
director.cpp

@@ -215,7 +215,10 @@ void Director::server_line(const std::string &line,
     if (startswith(line, " Items     Status  Trading % of max OnBoard"))
       SL_parser = SF_portline;
     */
-    if (endswith(line, "Relative Density Scan")) SL_parser = SF_densityline;
+    if (endswith(line, "Relative Density Scan")) { 
+      galaxy.dscan.reset(current_sector);
+      SL_parser = SF_densityline;
+    }
     if (startswith(line, "Sector  : ")) SL_parser = SF_sectorline;
     if (line == ": ") SL_parser = SF_cimline;
     if (line == "<Info>") SL_parser = SF_infoline;
@@ -478,6 +481,7 @@ MenuDispatch *Director::init_scripts_menu(void) {
     md->menu = {{"!", "Terror"},
                 {"T", "Trade"},
                 {"S", "Safe Move"},
+                {"C", "Closest Trade"},
                 {"U", "Upgrade Planet Pants"}};
     md->setNotify([this]() { this->scripts_done(); });
     return md;
@@ -496,20 +500,36 @@ void Director::scripts_done(void) {
       return;
     } else {
       switch (md->input[0]) {
-      case 'T': // Trade
-      {
-        script = std::make_shared<ScriptTrader>(*this);
-        ScriptTrader *ts = static_cast<ScriptTrader *>(&((*script)));
-        ts->setNotify([this]() { this->proxy_deactivate(); });
-
-        // Set parameters
-        auto found = galaxy.find_trades(current_sector, false);
-        if (found.empty()) {
-          to_client(
-              "No Trades found.  Port burnt (CONFIG: lower burnt_percent?) "
-              "or no ports around.\n\r");
-          proxy_deactivate();
-          return; }
+        case 'T':  // Trade
+        {
+          script = std::make_shared<TraderDispatch>(*this);
+          TraderDispatch *ts = static_cast<TraderDispatch *>(&((*script)));
+          ts->setNotify([this]() { this->proxy_deactivate(); });
+
+          // Locate best trades 
+          auto found = galaxy.find_trades(current_sector, false);
+          if (found.empty()) {
+            to_client(
+                "No Trades found.  Port burnt (CONFIG: lower burnt_percent?) "
+                "or no ports around.\n\r");
+            proxy_deactivate();
+            return;
+          }
+          // sort first?
+          galaxy.sort_port_pair_type(found);
+
+          BUGZ_LOG(fatal) << "Found " << found.size() << " possible trade(s).";
+          BUGZ_LOG(fatal) << "Best: " << found[0].s1 << "," << found[0].s2 << " : "
+                          << found[0].type;
+
+          // Set parameters                          
+          ts->port[0] = found[0].s1;
+          ts->port[1] = found[0].s2;
+          ts->type = found[0].type;
+          ts->trades = found[0].trades;
+          chain = script;
+          chain->activate();
+          return;
         } break;
         case 'S': {
           script = std::make_shared<MoveDispatch>(*this);
@@ -521,6 +541,14 @@ void Director::scripts_done(void) {
           return;
         } break;
         case '!': {
+          script = std::make_shared<ScriptTerror>(*this);
+          ScriptTerror * st = static_cast<ScriptTerror *>(&(*script));
+          st->setNotify([this]() { this->proxy_deactivate(); });
+          chain = script;
+          chain->activate();
+          return;
+        }
+        case 'C': {
           auto best = galaxy.find_closest(current_sector);
           if (best.type != 0) {
             std::string text =
@@ -869,11 +897,16 @@ void Director::SL_densityline(const std::string &line) {
     replace(work, "%", "");
     auto dense = split(work);
     // Parse our data
-    int sector = std::stoi(dense.at(1));
-    int density = std::stoi(dense.at(3));
-    int warps = std::stoi(dense.at(5));
-    int navhaz = std::stoi(dense.at(7));
+    
+    sector_type sector = std::stoi(dense.at(1));
+    uint16_t density = std::stoi(dense.at(3));
+    uint16_t warps = std::stoi(dense.at(5));
+    uint16_t navhaz = std::stoi(dense.at(7));
     bool anom = in(dense.at(9), "Yes");
+
+    struct density d = {sector, density, warps, navhaz, anom, known};
+    galaxy.dscan.add_scan(d);
+
     // Commit data
     BUGZ_LOG(warning) << "densityline: {sector=" << sector
                       << " density=" << density << " warps=" << warps

+ 540 - 19
dispatchers.cpp

@@ -25,8 +25,8 @@ void Dispatch::notify(void) {
 }
 
 void Dispatch::chain_client_input(const std::string &input) {
-  if (chain) {
-    chain->chain_client_input(input);
+  if (director.chain) {
+    director.chain->chain_client_input(input);
   } else {
     client_input(input);
   }
@@ -34,16 +34,16 @@ void Dispatch::chain_client_input(const std::string &input) {
 
 void Dispatch::chain_server_line(const std::string &line,
                                  const std::string &raw_line) {
-  if (chain) {
-    chain->chain_server_line(line, raw_line);
+  if (director.chain) {
+    director.chain->chain_server_line(line, raw_line);
   } else {
     server_line(line, raw_line);
   }
 }
 
 void Dispatch::chain_server_prompt(const std::string &prompt) {
-  if (chain) {
-    chain->chain_server_prompt(prompt);
+  if (director.chain) {
+    director.chain->chain_server_prompt(prompt);
   } else {
     server_prompt(prompt);
   }
@@ -206,6 +206,7 @@ void MainDispatch::client_input(const std::string &input) {
 
 InputDispatch::InputDispatch(Director &d) : Dispatch(d) {
   BUGZ_LOG(warning) << "InputDispatch()";
+  numeric = false;
 }
 
 InputDispatch::~InputDispatch() { BUGZ_LOG(warning) << "~InputDispatch()"; }
@@ -264,7 +265,12 @@ void InputDispatch::client_input(const std::string &cinput) {
   // BUGZ_LOG(info) << "InputDispatch::client_input(" << cinput << ")";
   for (const char ch : cinput) {
     if (isprint(ch)) {
-      // Ok!
+      // Ok! 
+      if (numeric) {
+        // numbers only
+        if (!isdigit(ch)) 
+          continue;
+      }
       if (input.length() < max_length) {
         to_client(std::string(1, ch));
         input += ch;
@@ -556,12 +562,14 @@ MoveDispatch::MoveDispatch(Director &d) : Dispatch(d) {
 
 void MoveDispatch::activate(void) {
   starting = director.current_sector;
+  BUGZ_LOG(fatal) << "MoveDispatch::activate()";
   BUGZ_LOG(warning) << "Moving from " << starting << " to " << move_to;
   // Start with density scan
   to_server("SD");
   state = 1;
   warp_lane.clear();
   warp_pos = 0;
+  use_express = false;
 
   // build final string to match against
   at_destination = "Auto Warping to sector ";
@@ -577,13 +585,41 @@ void MoveDispatch::server_line(const std::string &line,
     if (endswith(line, "Relative Density Scan")) {
       state = 2;
     }
+
+    if (line == "You don't have a long range scanner.") {
+      // Ok, we'll have to express move our way there then.
+      use_express = true;
+      // state = 2;
+      std::string command = str(boost::format("M%1%\r") % move_to);
+      to_server(command);
+      state = 3;
+    }
   }
 
   if ((state != 2) && (state != 5)) {
     // hide the density scan part
     std::string temp = raw_line;
-    temp.append("\n\r");
-    to_client(temp);
+    if (line == "<Auto Pilot Engaging>") {
+      // trim out the stray cursor home
+      replace(temp, "\x1b[H", "");
+    }
+
+    // Replace progress bar with something else.
+    if (startswith(temp, "\x1b[1;33m\xb3"))
+      temp = "\x1b[1A\x1b[1;33m** SLOW MOVE **";
+
+    /*
+    if (in(temp, "\xb3")) {
+      std::string debug = repr(temp);
+      BUGZ_LOG(fatal) << "MOVE LINE: " << debug;
+    }
+    */
+
+    // Don't output raw_lines that move the cursor up 3.
+    if (!in(temp, "\x1b[3A")) {
+      temp.append("\n\r");
+      to_client(temp);
+    }
   }
 
   if (state == 3) {
@@ -596,11 +632,12 @@ void MoveDispatch::server_line(const std::string &line,
     if (line == "That Warp Lane is not adjacent.") {
       // ok!  Parse out the path that we need to take...
     }
+
     // [611 > 612 > 577 > 543 > 162 > 947 > 185 > 720 > 894 > 3 > 1]
     // multiple lines possible here?  Yes.
     // [344 > 23328 > 2981 > 10465 > 14016 > 8979 > 1916 > 18734 > 5477 > 131 >
     // 27464 >] watch for <Move> it contains >
-    if ((line != "<Move>") && in(line, ">")) {
+    if ((line != "<Move>") && in(line, " > ")) {
       bool more = false;
       std::string work = line;
 
@@ -629,8 +666,10 @@ void MoveDispatch::server_line(const std::string &line,
   }
 }
 
-bool MoveDispatch::density_clear(int sector, int density) {
-  switch (density) {
+bool MoveDispatch::density_clear(density d) {  // int sector, int density) {
+  if (d.sector == 0) return false;
+
+  switch (d.density) {
     case 0:
     case 1:
     case 100:
@@ -638,8 +677,7 @@ bool MoveDispatch::density_clear(int sector, int density) {
       return true;
   }
   // special case for sector 1:
-  if ((sector == 1) && (density == 601))
-    return true;
+  if ((d.sector == 1) && (d.density == 601)) return true;
   return false;
 }
 
@@ -650,6 +688,16 @@ void MoveDispatch::server_prompt(const std::string &prompt) {
     if (at_command_prompt(prompt)) {
       // Ok, density is done
       // BUG:  If the sector is adjacent, this doesn't check density!
+      density d = director.galaxy.dscan.find(move_to);
+      if (d.sector == move_to) {
+        // Yes! we found the sector in the scan!
+        if (!density_clear(d)) {
+          BUGZ_LOG(fatal) << "Failed density check on single move.";
+          success = false;
+          deactivate();
+          return;
+        }
+      }
       std::string command = str(boost::format("M%1%\r") % move_to);
       to_server(command);
       state = 3;
@@ -662,16 +710,29 @@ void MoveDispatch::server_prompt(const std::string &prompt) {
       deactivate();
       return;
     }
-  
+
   } else if (state == 4) {
     if (prompt == "Engage the Autopilot? (Y/N/Single step/Express) [Y] ") {
+      if (use_express) {
+        BUGZ_LOG(fatal) << "Using Express";
+        to_server("E");
+      } else {
       int to_check = warp_lane[warp_pos + 1];
       // check density scan
+      density d = director.galaxy.dscan.find(to_check);
+      /*
       int density =
           director.galaxy.meta["density"][to_check]["density"].as<int>();
-      if (density_clear(to_check, density)) {
+      */
+      if (density_clear(d)) {  // to_check, density)) {
         to_server("S");
         ++warp_pos;
+      } else {
+        to_server("N");
+        BUGZ_LOG(fatal) << "density_clear(" << to_check << ") : false";
+        success = false;
+        deactivate();
+      }
       }
     }
     if (prompt == "Stop in this sector (Y,N,E,I,R,S,D,P,?) (?=Help) [N] ? ") {
@@ -682,15 +743,18 @@ void MoveDispatch::server_prompt(const std::string &prompt) {
     // finished scan
     if (prompt == "Stop in this sector (Y,N,E,I,R,S,D,P,?) (?=Help) [N] ? ") {
       int to_check = warp_lane[warp_pos + 1];
+      density d = director.galaxy.dscan.find(to_check);
+      /*
       int density =
           director.galaxy.meta["density"][to_check]["density"].as<int>();
-      if (density_clear(to_check, density)) {
+      */
+      if (density_clear(d)) {
         to_server("N");
         ++warp_pos;
         state = 4;
       } else {
         to_server("Y");
-        BUGZ_LOG(fatal) << "Stopped by density: " << density;
+        BUGZ_LOG(fatal) << "Stopped by density: " << to_check;
         success = 0;
         deactivate();
       }
@@ -703,7 +767,464 @@ void MoveDispatch::server_prompt(const std::string &prompt) {
     }
   }
 }
-void MoveDispatch::client_input(const std::string &input) {}
+
+void MoveDispatch::client_input(const std::string &input) {
+  // This exits us out quickly -- should I stop at a better spot?
+  // As in answer "Y" to Stop in this sector?
+  success = 0;
+  deactivate();
+}
+
+TraderDispatch::TraderDispatch(Director &d) : Dispatch(d) {
+  BUGZ_LOG(fatal) << "TraderDispatch()";
+  state = 0;
+  success = false;
+};
+
+TraderDispatch::~TraderDispatch() { BUGZ_LOG(fatal) << "~TraderDispatch()"; }
+
+void TraderDispatch::activate(void) {
+  // ok, lookup port1 port2
+  BUGZ_LOG(fatal) << "TraderDispatch::activate " << port[0] << " & " << port[1];
+  auto port_info = director.galaxy.ports.find(port[0]);
+  int port0_type = port_info->second.type;
+  port_buysell[0] = get_buysell(port0_type);
+
+  // Special case - we just want to buy resources
+  if (port[1] != 0) {
+    port_info = director.galaxy.ports.find(port[1]);
+    int port1_type = port_info->second.type;
+    port_buysell[1] = get_buysell(port1_type);
+    BUGZ_LOG(fatal) << port0_type << " and " << port1_type;
+  } else {
+    BUGZ_LOG(fatal) << "Just buy from " << port[0];
+  }
+
+  /*
+  auto ttr = director.galaxy.trade_type_info(port0_type, port1_type);
+  trades = ttr.trades;
+  */
+
+  /*
+  if (trades.foe[0] && trades.foe[1] && trades.foe[2]) {
+    // it has all three -- use the last 2.
+    trades.foe[0] = false;
+  }
+  */
+
+  // Ok, what do we do first here?
+  // I - Info
+  state = 1;
+  percent = 5.0;
+  to_server("I");
+  director.galaxy.meta["help"]["stop_percent"] =
+      "ScriptTrader stop trading if below this percent.";
+
+  if (director.galaxy.config["stop_percent"]) {
+    stop_percent = director.galaxy.config["stop_percent"].as<int>();
+  } else {
+    stop_percent = 20;
+    director.galaxy.config["stop_percent"] = stop_percent;
+  }
+  director.galaxy.meta["help"]["trade_end_empty"] =
+      "ScriptTrader end trades with empty holds? Y/N";
+
+  if (director.galaxy.config["trade_end_empty"]) {
+    std::string tee =
+        director.galaxy.config["trade_end_empty"].as<std::string>();
+    if ((toupper(tee[0]) == 'Y') || (toupper(tee[0]) == 'T')) {
+      trade_end_empty = true;
+    } else {
+      trade_end_empty = false;
+      // director.galaxy.config["trade_end_empty"] = "N";
+    }
+  } else {
+    trade_end_empty = true;
+    director.galaxy.config["trade_end_empty"] = "Y";
+  }
+}
+
+void TraderDispatch::deactivate(void) { notify(); }
+
+void TraderDispatch::server_line(const std::string &line,
+                               const std::string &raw_line) {
+  // FUTURE:  powering up weapons check
+
+  // Show what's going on...
+  if (state > 1) {
+    std::string temp = raw_line;
+    temp.append("\n\r");
+    to_client(temp);
+  }
+
+  if (line == "Docking...") {
+    last_offer = 0;
+    final_offer = 0;
+    initial_offer = 0;
+    try_again = false;
+  }
+
+  static std::set<std::string> success_lines = {
+      "If only more honest traders would port here, we'll take them though.",
+      "You will put me out of business, I'll take your offer.",
+      "FINE, we'll take them, just leave!",
+      "Agreed, and a pleasure doing business with you!",
+      "You are a rogue! We'll take them anyway.",
+      "You insult my intelligence, but we'll buy them anyway.",
+      "Very well, we'll take that offer.",
+      "You drive a hard bargain, but we'll take them.",
+      "Done, we'll take the lot.",
+      "I hate haggling, they're all yours.",
+      "You are robbing me, but we'll buy them anyway.",
+      "SOLD!  Come back anytime!",
+      "Cheapskate.  Here, take them and leave me alone.",
+      "Very well, we'll buy them.",
+      "You are a shrewd trader, they're all yours.",
+      "I could have twice that much in the Androcan Empire, but they're yours.",
+      "Oh well, maybe I can sell these to some other fool, we'll take them.",
+      "I PAID more than that!  But we'll sell them to you anyway.",
+      "(Sigh) Very well, pay up and take them away.",
+      "Agreed! We'll purchase them!"};
+
+  if (success_lines.find(line) != success_lines.end()) {
+    BUGZ_LOG(fatal) << "Success " << buying << " " << initial_offer << " : "
+                    << last_offer;
+    // calculate % ?
+    success = true;
+    BUGZ_LOG(fatal) << "% " << (float)initial_offer / (float)last_offer * 100.0;
+    BUGZ_LOG(fatal) << "meta trade setting: " << percent << " for "
+                    << active_port << " " << product;
+    director.galaxy.meta["trade"][active_port][product] = percent;
+  }
+
+  // <P-Probe estimates your offer was
+
+  if (startswith(line, "Agreed, ")) {
+    last_offer = 0;
+    final_offer = 0;
+    if (director.galaxy.meta["trade"][active_port][product]) {
+      percent = director.galaxy.meta["trade"][active_port][product].as<float>();
+      percent += 1.0;
+      BUGZ_LOG(fatal) << "Percent for " << active_port << " now " << percent;
+    } else {
+      BUGZ_LOG(fatal) << "using default for " << active_port;
+      percent = 5.0;  // check meta for past trades information
+    }
+  }
+
+  if (startswith(line, "We'll buy them for ")) {
+    // I need the initial offer!
+    std::string offer = line.substr(19);
+    replace(offer, ",", "");
+    initial_offer = stoi(offer);
+    BUGZ_LOG(fatal) << "Buying, initial: " << initial_offer;
+    buying = true;  // Port is buying, we are selling.
+  }
+
+  if (startswith(line, "We'll sell them for ")) {
+    // I need the initial offer!
+    std::string offer = line.substr(20);
+    replace(offer, ",", "");
+    initial_offer = stoi(offer);
+    BUGZ_LOG(fatal) << "Selling, initial: " << initial_offer;
+    buying = false;  // Port is selling, we are buying.
+  }
+
+  // SL: [Our final offer is 1,263 credits.]
+  if (startswith(line, "Our final offer is ")) {
+    // Well snap!
+    std::string offer = line.substr(19);
+    replace(offer, ",", "");
+    final_offer = stoi(offer);
+    BUGZ_LOG(fatal) << "Final offer: " << final_offer;
+  }
+
+  if (line == "We're not interested.") {
+    // well rats.
+    try_again = true;
+  }
+
+  // SL: [You have 16,767 credits and 0 empty cargo holds.]
+  // trade accepted.  if not 0 empty cargo holds -- we failed!
+  // SL: [<P-Probe estimates your offer was  91.83% of best price>]
+  // SL: [You have 4,046 credits and 0 empty cargo holds.]
+
+  // this shows up at the initial docking of the port.
+
+  if (startswith(line, "You have ")) {
+    if (initial_offer != 0) {
+      // Ok, the offer was possibly accepted.
+      int success;
+      if (buying)
+        success = director.galaxy.meta["ship"]["holds"]["total"].as<int>();
+      else
+        success = 0;
+
+      std::string text = std::to_string(success);
+      text.append(" empty cargo holds.");
+      if (endswith(line, text)) {
+        BUGZ_LOG(fatal) << "Trade SUCCESS!";
+        // record this action somewhere in meta.
+      } else {
+        BUGZ_LOG(fatal) << "Trade FAIL";
+      }
+    }
+  }
+}
+
+void TraderDispatch::server_prompt(const std::string &prompt) {
+  // FUTURE:  check for "Surrender/Attack"
+
+  if (at_command_prompt(prompt)) {
+    if (state == 1) {
+      // Ok, decision time!
+      if (director.galaxy.meta["ship"]["holds"]["c"]) {
+        // holds contain colonists
+        success = false;
+        to_client("ScriptTrader FAIL: holds contain colonists.");
+        deactivate();
+        return;
+      }
+
+      // Which port to trade with first?  examine trades
+      BUGZ_LOG(fatal) << "trades: " << trades;
+      BUGZ_LOG(fatal) << "port0:" << text_from_buysell(port_buysell[0]);
+      if (port[1] != 0)
+        BUGZ_LOG(fatal) << "port1:" << text_from_buysell(port_buysell[1]);
+
+      // Ok, I might still need this (so I know what port to start with)
+      // which is selling?
+      // must set active port!
+
+      bool all_holds_empty = false;
+      active_port = 0;
+      // check the ship and holds here.  (MAYBE)
+      int holds = director.galaxy.meta["ship"]["holds"]["total"].as<int>();
+      if (director.galaxy.meta["ship"]["holds"]["empty"]) {
+        if (holds == director.galaxy.meta["ship"]["holds"]["empty"].as<int>())
+          all_holds_empty = true;
+      }
+
+      if (port[1] == 0) {
+        active_port = port[0];
+      } else {
+        if (!all_holds_empty) {
+          for (int x = 0; x < 3; ++x) {
+            if (director.galaxy.meta["ship"]["holds"][foe[x]]) {
+              if (!port_buysell[0].foe[x]) {
+                active_port = port[0];
+                break;
+              }
+              if (!port_buysell[1].foe[x]) {
+                active_port = port[1];
+              }
+            }
+          }
+          if (active_port == 0) {
+            success = false;
+            to_client(
+                "I don't see any ports that are buying what we have in our "
+                "holds.\n\r");
+            deactivate();
+            return;
+          };
+        } else {
+          // all holds empty, find selling port
+          for (int x = 0; x < 3; ++x) {
+            if (trades.foe[x]) {
+              // TRUE = BUY, so FALSE = sell
+              if (!port_buysell[0].foe[x]) {
+                active_port = port[0];
+                break;
+              }
+              if (!port_buysell[1].foe[x]) {
+                active_port = port[1];
+                break;
+              }
+            }
+          }
+        }
+      }
+
+      state = 2;
+      if (director.current_sector == active_port) {
+        // begin state 3
+        state = 3;
+        to_client("Trading...\n\r");
+        to_server("PT");
+        return;
+      } else {
+        // initiate move
+        std::string move = std::to_string(active_port);
+        to_client("Moving...\n\r");
+        move.append("\r");
+        to_server(move);
+        return;
+      }
+    }
+
+    if (state == 2) {
+      if (director.current_sector == active_port) {
+        // We're here
+        state = 3;
+        to_client("Trading...\n\r");
+        to_server("PT");
+        return;
+      } else {
+        // we failed to move to where we wanted to go?!
+        BUGZ_LOG(fatal) << "Expecting: " << active_port << " but got "
+                        << director.current_sector;
+        deactivate();
+        return;
+      }
+    }
+  }
+
+  if (state == 3) {
+    if (startswith(prompt, "How many holds of ") && endswith(prompt, "]? ")) {
+      char selling = tolower(prompt[18]);
+      for (int x = 0; x < 3; ++x) {
+        if (foe[x] == selling) product = x;
+      }
+
+      if (in(prompt, " to sell ")) {
+        // always sell everything
+        to_server("\r");
+        return;
+      }
+
+      if (in(prompt, " to buy ")) {
+        bool buy_ok = true;
+
+        if (trade_end_empty) {
+          // Ok, we want to end with empty holds...
+          int other_port;
+          if (active_port == port[0])
+            other_port = port[1];
+          else
+            other_port = port[0];
+
+          // Is target port burnt?
+          auto pos = director.galaxy.ports.find(other_port);
+          bool burnt = false;
+
+          if (pos != director.galaxy.ports.end()) {
+            // We'll find the port.  Really.
+
+            if (!pos->second.unknown()) {
+              // port isn't unknown, so check to see if it's burnt
+              for (int x = 0; x < 3; ++x) {
+                if (trades.foe[x]) {
+                  if (pos->second.percent[x] < stop_percent) burnt = true;
+                }
+              }
+            }
+          }
+
+          if (burnt) {
+            buy_ok = false;
+          }
+        }
+
+        // Ok, what are they selling?
+        // char selling = tolower(prompt[18]);
+        BUGZ_LOG(fatal) << "Selling: " << selling;
+        if (!buy_ok) {
+          // no!
+          to_server("0\r");
+        } else
+          for (int x = 0; x < 3; ++x) {
+            // if (foe[x] == selling) {
+            // We found the item ... is it something that we're trading?
+            if (foe[x] == selling) {
+              if (trades.foe[x]) {
+                // Yes!
+                to_server("\r");
+                product = x;
+              } else {
+                // No!
+                to_server("0\r");
+              }
+            }
+          }
+        // }
+      }
+    }
+
+    if (startswith(prompt, "Your offer [") && endswith(prompt, " ? ")) {
+      // Ok, things get weird here.  We also need to look for final offer.
+      if (last_offer != 0) percent -= 1.0;
+
+      if (buying)
+        last_offer = (int)(initial_offer * (100 + percent) / 100.0);
+      else
+        last_offer = (int)(initial_offer * (100 - percent) / 100.0);
+
+      BUGZ_LOG(fatal) << "Offer: " << buying << " offer " << last_offer << " % "
+                      << percent;
+      std::string text = std::to_string(last_offer);
+      text.append("\r");
+      to_server(text);
+    }
+
+    if (at_command_prompt(prompt)) {
+      // we're done trading...
+      // do we carry on, or stop?
+      // 1.) CHECK TURNS // need turn tracking
+      // 2.) PORTS BURNT?
+
+      if (try_again) {
+        state = 3;
+        to_client("Trading... Take 2!\n\r");
+        to_server("PT");
+        return;
+      }
+
+      if (active_port == port[0]) {
+        if (port[0] == 0) {
+          deactivate();
+          return;
+        }
+        active_port = port[1];
+      } else
+        active_port = port[0];
+
+      // Is target port burnt?
+      auto pos = director.galaxy.ports.find(active_port);
+      bool burnt = false;
+
+      if (pos != director.galaxy.ports.end()) {
+        // We'll find the port.  Really.
+
+        if (!pos->second.unknown()) {
+          // port isn't unknown, check to see if burnt
+          for (int x = 0; x < 3; ++x) {
+            if (trades.foe[x]) {
+              BUGZ_LOG(fatal) << x << " % " << (int)pos->second.percent[x]
+                              << " " << stop_percent;
+              if (pos->second.percent[x] < stop_percent) burnt = true;
+            }
+          }
+        }
+      }
+
+      if (burnt) {
+        to_client("Ports burnt.\n\r");
+        deactivate();
+        return;
+      }
+
+      std::string move = std::to_string(active_port);
+      to_client("Moving...\n\r");
+      move.append("\r");
+      to_server(move);
+
+      state = 2;
+    }
+  }
+}
+
+void TraderDispatch::client_input(const std::string &cinput) { deactivate(); };
 
 /*
  * CoreDispatch:  This is an example class that does dispatch.

+ 59 - 3
dispatchers.h

@@ -28,7 +28,6 @@ class Dispatch {
  protected:
   Director &director;
   notifyFunc notify_;
-  std::shared_ptr<Dispatch> chain;
 
  public:
   Dispatch(Director &);
@@ -68,7 +67,8 @@ class InputDispatch : public Dispatch {
   std::string prompt;
   size_t max_length;
   std::string input;
-
+  bool numeric;
+  
   void activate(void) override;
   void deactivate(void) override;
 
@@ -133,6 +133,7 @@ class MoveDispatch : public Dispatch {
   int state;
   int success;
   int warp_pos;
+  bool use_express;
   std::string at_destination;
   std::vector<int> warp_lane;
 
@@ -144,10 +145,65 @@ class MoveDispatch : public Dispatch {
   void server_prompt(const std::string &prompt) override;
   void client_input(const std::string &input) override;
  private:
-  bool density_clear(int sector, int density);  
+  bool density_clear(density d); // int sector, int density);  
+};
+
+class TraderDispatch : public Dispatch {
+ private:
+ public:
+  TraderDispatch(Director &);
+  ~TraderDispatch();
+
+  char foe[4] = "foe";
+  bool trade_end_empty;
+
+  // success / failure ?
+  bool success;
+
+  /**
+   * internal state
+   *
+   * 1 = <Info> query.
+   * 2 = move to active port
+   * 3 = trade
+   * 4 = if (burnt), stop, otherwise toggle active_port and state = 2
+   *
+   * NEW: set port[1] to 0 for buy-only ability.
+   * Percent 20 doesn't work with Cargo Trans' 250 holds.
+   * Maybe look at the amount instead?  If < holds = burnt.
+   */
+  int state;
+  float percent;
+  bool buying;
+  int initial_offer;
+  int last_offer;
+  int final_offer;
+  int product; // product we are buying/selling 0,1,2 foe.
+  int stop_percent;
+  bool try_again;
+  
+  // should this be 0/1 ?  right now it is the port's sector number.
+  int active_port;  // port trading with
+  // information from the find_best_trades function + others.
+  int port[2];
+  int active;
+  // I don't care about trade type, just trades.
+  int type;
+  
+  buysell trades;
+  buysell port_buysell[2];
+
+  void activate(void) override;
+  void deactivate(void) override;
+
+  void server_line(const std::string &line,
+                   const std::string &raw_line) override;
+  void server_prompt(const std::string &prompt) override;
+  void client_input(const std::string &cinput) override;
 };
 
 
+
 class CoreDispatch : public Dispatch {
  public:
   CoreDispatch(Director &);

+ 315 - 142
galaxy.cpp

@@ -26,78 +26,66 @@ std::ostream &operator<<(std::ostream &os, const port &p) {
   return os;
 }
 
+/**
+ * Is port unknown?
+ *
+ * As in we haven't visited it, we don't know what it has?
+ * We were checking percent to 0, but we've seen valid ports with 0 percent.
+ * We now check amount.  If this becomes an issue, we'll change to an unknown
+ * flag.
+ *
+ * @return true
+ * @return false
+ */
 bool port::unknown(void) {
   for (int x = 0; x < 3; ++x) {
-    if (percent[x] != 0) return false;
+    if (amount[x] != 0) return false;
   }
   return true;
 }
 
-trade_type_result trade_type_info(port_type port1, port_type port2) {
-  // NONE = 0
-  // GOOD = 1 = OE PAIR
-  // OK   = 2 = ?? Pair
-  // FAIR = 3 = B / S
-
-  buysell p1 = get_buysell(port1);
-  buysell p2 = get_buysell(port2);
-
-  buysell inv2 = invert_buysell(p2);
-  int matches = 0;  // or pos.size();
-  std::vector<int> pos;
+density_scan::density_scan() { reset(0); }
 
-  // find which FOE are flipped.  Save index pos.
-  for (int x = 0; x < 3; ++x) {
-    inv2.foe[x] = (p1.foe[x] == inv2.foe[x]);
-    if (inv2.foe[x]) {
-      matches++;
-      pos.push_back(x);
-    }
+void density_scan::reset(sector_type s) {
+  sector = s;
+  pos = 0;
+  for (int x = 0; x < (int)d.size(); ++x) {
+    d[x].sector = 0;
   }
+}
 
-  if (matches > 1) {
-    // O != E for both ports, and O != O
-    if ((p1.foe[ORG] != p1.foe[EQU]) && (p2.foe[ORG] != p2.foe[EQU]) &&
-        (p1.foe[ORG] != p2.foe[ORG])) {
-      return trade_type_result{1, inv2};
-    }
-
-    // at least 2 matches.  but are they trade pairs?
-    // I can tell by comparing the last two positions in the same port.
-    if (p1.foe[pos[matches - 1]] == p1.foe[pos[matches - 2]]) {
-      // they are NOT.
-      return trade_type_result{3, inv2};
-    }
-    return trade_type_result{2, inv2};
+void density_scan::add_scan(density den) {
+  if (pos > (int)d.size()) {
+    std::string message("density_scan::add_scan() exceeded max size ");
+    message.append(std::to_string(d.size()));
+    throw std::out_of_range(message);
   }
 
-  if (matches == 1) {
-    if (inv2.foe[FUEL]) return trade_type_result{4, inv2};
-    return trade_type_result{3, inv2};
-  }
-  return trade_type_result{0, inv2};
+  d[pos] = den;
+  ++pos;
 }
 
-int trade_type(port_type port1, port_type port2) {
-  trade_type_result r = trade_type_info(port1, port2);
-  return r.type;
+density density_scan::find(sector_type sector) {
+  for (int x = 0; x < pos; ++x) {
+    if (d[x].sector == sector) return d[x];
+  }
+  BUGZ_LOG(fatal) << "density_scan::find failed: " << sector << " we have "
+                  << pos << " scans.";
+  density den;
+  den.sector = 0;
+  return den;
 }
 
-/*
-// adding this breaks test-galaxy's port = {2, 2, {1,2,3}, {1,2,3}} code.
-port::port() {
-  sector = 0;
-  type = 0;
-  for (int x = 0; x < 3; x++) {
-    amount[x] = 0;
-    percent[x] = 0;
-  }
+bool operator==(const density lhs, const density rhs) {
+  return ((lhs.sector == rhs.sector) && (lhs.density == rhs.density) &&
+          (lhs.navhaz == rhs.navhaz) && (lhs.anomaly == rhs.anomaly) &&
+          (lhs.warps == rhs.warps) && (lhs.known == rhs.known));
 }
-*/
 
-sector_warps::sector_warps() {
-  sector = 0;
-  // for (int x = 0; x < MAX_WARPS; ++x) warps[x] = 0;
+sector_warps::sector_warps() { sector = 0; }
+sector_warps::sector_warps(sector_type s, std::set<sector_type> w) {
+  sector = s;
+  warps = w;
 }
 
 void sector_warps::add(sector_type new_sector) {
@@ -126,14 +114,6 @@ std::ostream &operator<<(std::ostream &os, const sector_warps &warps) {
       comma = true;
     os << warp;
   }
-  /*
-  for (int x = 0; x < MAX_WARPS; ++x) {
-    if (warps.warps[x] != 0) {
-      if (x != 0) os << ",";
-      os << warps.warps[x];
-    }
-  }
-  */
   return os;
 }
 
@@ -512,68 +492,6 @@ void Galaxy::save(void) {
     }
   }
   */
-
-#ifdef YAML_NODE_SLOW_OUTPUT
-  YAML::Node data;
-  // add some information to meta before saving.
-  meta["save_to"] = filename;
-  std::chrono::_V2::system_clock::time_point now =
-      std::chrono::system_clock::now();
-  meta["save_time"] = std::chrono::system_clock::to_time_t(now);  // time_t
-
-  data["meta"] = meta;
-  // data["meta"].SetStyle(YAML::EmitterStyle::Flow);
-  BUGZ_LOG(fatal) << "YAML config: " << config.size();
-  data["config"] = config;
-  data["config"].SetStyle(YAML::EmitterStyle::Flow);
-
-  /*
-  for (auto const &config_iter : config) {
-    data["config"][config_iter.first] = config_iter.second;
-  }
-  */
-  BUGZ_LOG(fatal) << "YAML warps: " << warps.size();
-  for (auto const &warp : warps) {
-    for (auto const &sector : warp.second.warps) {
-      data["warps"][warp.first].push_back(sector);
-    }
-    data["warps"][warp.first].SetStyle(YAML::EmitterStyle::Flow);
-    /*
-    for (int x = 0; x < MAX_WARPS; ++x) {
-      if (warp.second.warps[x] == 0) break;
-      data["warps"][warp.first].push_back(warp.second.warps[x]);
-    }
-    */
-  }
-  BUGZ_LOG(fatal) << "YAML ports: " << ports.size();
-  /*
-  When saving to yaml, my sector_type is like char.  So, it wants
-  to save the values as a character.  Cast to int.
-   */
-  for (auto const &port : ports) {
-    data["ports"][port.second.sector]["class"] = (int)port.second.type;
-    if (port.second.type == 0) {
-      data["ports"][port.second.sector].SetStyle(YAML::EmitterStyle::Flow);
-    } else {
-      // nothing to save for type = 0
-      for (int x = 0; x < 3; x++) {
-        data["ports"][port.second.sector]["amount"].push_back(
-            (int)port.second.amount[x]);
-        data["ports"][port.second.sector]["pct"].push_back(
-            (int)port.second.percent[x]);
-      }
-      data["ports"][port.second.sector]["amount"].SetStyle(
-          YAML::EmitterStyle::Flow);
-      data["ports"][port.second.sector]["pct"].SetStyle(
-          YAML::EmitterStyle::Flow);
-      data["ports"][port.second.sector].SetStyle(YAML::EmitterStyle::Flow);
-    }
-  }
-
-  std::ofstream fout(filename);
-  fout << data << std::endl;
-  BUGZ_LOG(fatal) << "YAML: " << filename;
-#endif
 }
 
 std::vector<port_pair_type> Galaxy::find_trades(sector_type sector,
@@ -613,23 +531,11 @@ std::vector<port_pair_type> Galaxy::find_trades(sector_type sector,
     BUGZ_LOG(trace) << "find_trades: Port " << sector << ","
                     << (int)port->second.type << " " << s
                     << (int)possible_port->second.type;
-    trade_type_result ttr =
-        trade_type_info(port->second.type, possible_port->second.type);
-    if ((ttr.type == 0) || (ttr.type == 4)) continue;
+    trade_type_result ttr = trade_type_info(
+        sector, s);  // port->second.type, possible_port->second.type);
+    if ((ttr.type == NONE) || (ttr.type == FAIR_F)) continue;
 
-    bool burnt = false;
-    for (int x = 0; x < 3; ++x) {
-      if (ttr.trades.foe[x]) {
-        // if port isn't unknown, check to see if it's burnt out.
-        if (!possible_port->second.unknown())
-          if (possible_port->second.percent[x] < burnt_percent) burnt = true;
-        if (!port->second.unknown())
-          if (port->second.percent[x] < burnt_percent) burnt = true;
-      }
-    }
-    if (burnt) continue;
-
-    pptv.push_back(port_pair_type{ttr.type, sector, s});
+    pptv.push_back(port_pair_type{ttr.type, ttr.trades, sector, s});
     BUGZ_LOG(trace) << "sector: " << sector << " and " << s
                     << " tt:" << ttr.type;
   }
@@ -674,6 +580,15 @@ std::vector<port_pair_type> Galaxy::find_best_trades(void) {
   return pptv;
 }
 
+/**
+ * Find_closest trade
+ *
+ * This works by getting all of the ports we know of.
+ * Then, we find the nearest.
+ *
+ * @param sector
+ * @return port_pair_type
+ */
 port_pair_type Galaxy::find_closest(int sector) {
   auto trades = find_best_trades();
   // int type, sector_type s1, s2;
@@ -737,4 +652,262 @@ port_pair_type Galaxy::find_closest(int sector) {
   port_pair_type ppt;
   ppt.type = 0;
   return ppt;
-}
+}
+
+/**
+ * Find closest and best trade
+ * 
+ * @param sector 
+ * @param lowest_trade_type 
+ * @return port_pair_type 
+ */
+port_pair_type Galaxy::find_closest_trade(int sector, int lowest_trade_type) {
+  // int type, sector_type s1, s2;
+  BUGZ_LOG(fatal) << "find_closest_trade(" << sector << ")";
+  std::vector<port_pair_type> vppt;
+  std::set<sector_type> seen;
+  std::set<sector_type> current;
+  current.insert(sector);
+  bool found = false;
+  bool added_new = false;
+  int depth = 0;
+
+  while (!found) {
+    /*
+    for (auto const &t : trades) {
+      if (t.type > 2) continue;
+      if (current.find(t.s1) != current.end()) {
+        // found one!
+        return t;
+      }
+      if (current.find(t.s2) != current.end()) {
+        return t;
+      }
+    }
+    */
+    ++depth;
+    BUGZ_LOG(info) << "depth: " << depth << " current:" << current.size()
+                   << " seen: " << seen.size();
+
+    // search current for trades
+    for (auto const &c : current) {
+      auto port = ports.find(c);
+      if (port == ports.end()) continue;
+      if (port->second.type == 0) continue;
+      auto port_warps = warps.find(c);
+      if (port_warps == warps.end()) continue;
+      for (auto const &s : port_warps->second.warps) {
+        // verify we have a way back
+        {
+          auto warpback = warps.find(s);
+          if (warpback == warps.end()) continue;
+          if (warpback->second.warps.find(c) == warpback->second.warps.end())
+            continue;
+        }
+
+        auto possible_port = ports.find(s);
+        if (possible_port == ports.end()) continue;
+        if (possible_port->second.type == 0) continue;
+        trade_type_result ttr = trade_type_info(c, s);
+        if ((ttr.type == NONE) || (ttr.type > lowest_trade_type)) continue;
+        // Ok! we found a trade that fits the criteria!
+        vppt.push_back(port_pair_type{ttr.type, ttr.trades, c, s});
+        found = true;
+      }
+    }
+    added_new = false;
+
+    if (found) {
+      // ok, we've found some trades, sort and return the best
+      sort_port_pair_type(vppt);
+      return port_pair_type{vppt[0]};
+    } else {
+      // update the seen
+      seen.insert(current.begin(), current.end());
+      auto look_in = current;
+      current.clear();
+      for (auto const &li : look_in) {
+        auto wi = warps.find(li);
+        if (wi == warps.end()) continue;
+        for (auto const &w : wi->second.warps) {
+          // have we already seen this sector?
+          if (seen.find(w) != seen.end()) continue;
+
+          // does it have a warp back to the original sector?
+          auto warp_back = warps.find(w);
+          if (warp_back != warps.end()) {
+            if (warp_back->second.warps.find(li) !=
+                warp_back->second.warps.end()) {
+              // Ok, this links back to the original sector...
+              current.insert(w);
+              added_new = true;
+            }
+          }
+        }
+      }
+      if (!added_new) {
+        BUGZ_LOG(warning) << "No new sectors added.  We're done!";
+        port_pair_type ppt;
+        ppt.type = 0;
+        return ppt;
+      }
+    }
+  }
+  port_pair_type ppt;
+  ppt.type = 0;
+  return ppt;
+}
+
+trade_type_result Galaxy::trade_type_info(sector_type port1, sector_type port2,
+                                          int burnt_percent) {
+  BUGZ_LOG(fatal) << "Trade_type_info(" << port1 << "," << port2 << ")";
+  // This only gives us one trade_type per pair.  There actually
+  // should be multiple values returned here!
+  // Like in case of BBB/SSS:  return 3, 4 and 5.
+
+  // NONE = 0
+  // GOOD = 1 = OE PAIR
+  // OK   = 2 = ?? Pair
+  // FAIR = 3 = B/S E
+  //        4 = B/S O
+  //        5 = B/S F
+  buysell inv2;
+
+  auto p1 = ports.find(port1);
+  if (p1 == ports.end()) {
+    BUGZ_LOG(fatal) << "Can't find port 1: " << (int)port1;
+    return {NONE, inv2};
+  }
+  BUGZ_LOG(fatal) << "port 1: " << p1->first << " " << p1->second.sector << ", "
+                  << (int)p1->second.type;
+  auto p2 = ports.find(port2);
+
+  if (p2 == ports.end()) {
+    BUGZ_LOG(fatal) << "Can't find port 2: " << (int)port2;
+    return {NONE, inv2};
+  }
+  BUGZ_LOG(fatal) << "port 2: " << p2->first << " " << p2->second.sector << ", "
+                  << (int)p2->second.type;
+
+  buysell bsp1 = get_buysell(p1->second.type);
+  buysell bsp2 = get_buysell(p2->second.type);
+
+  inv2 = invert_buysell(bsp2);
+  int matches = 0;  // or pos.size();
+  std::vector<int> pos;
+
+  // find which FOE are flipped.  Save index pos.
+  for (int x = 0; x < 3; ++x) {
+    inv2.foe[x] = (bsp1.foe[x] == inv2.foe[x]);
+    // Ok, these are possible trades (B->S or S->B)
+
+    // If known, check for burnt
+    if (!p1->second.unknown()) {
+      if (p1->second.percent[x] < burnt_percent) {
+        BUGZ_LOG(fatal) << "Marking Port 1: " << x << " (burnt)";
+        inv2.foe[x] = false;
+      }
+    } else {
+      BUGZ_LOG(fatal) << "Port 1 : unknown / skip burnt checks";
+    }
+    if (!p2->second.unknown()) {
+      if (p2->second.percent[x] < burnt_percent) {
+        BUGZ_LOG(fatal) << "Marking Port 2: " << x << " (burnt)";
+        inv2.foe[x] = false;
+      }
+    } else {
+      BUGZ_LOG(fatal) << "Port 2 : unknown / skip burnt checks";
+    }
+
+    if (inv2.foe[x]) {
+      matches++;
+      pos.push_back(x);
+    }
+  }
+
+  BUGZ_LOG(fatal) << "Matches: " << matches << " inv: " << inv2;
+
+  if (matches > 1) {
+    // Check for BEST
+    // O != E for both ports, and O != O, and ORG/EQU in inv2 list
+    if (inv2.foe[ORG] && inv2.foe[EQU] && (bsp1.foe[ORG] != bsp1.foe[EQU]) &&
+        (bsp2.foe[ORG] != bsp2.foe[EQU]) && (bsp1.foe[ORG] != bsp2.foe[ORG])) {
+      // verify that fuel isn't set.
+      inv2.foe[FUEL] = false;
+      BUGZ_LOG(fatal) << "result: " << BEST << " " << inv2;
+      return trade_type_result{BEST, inv2};
+    }
+
+    if (matches == 3) {
+      // This could be SBB / BSS (it's a pair, but not BEST)
+      // Is it FO or FE ?
+
+      if (bsp1.foe[FUEL] != bsp2.foe[EQU]) {
+        // OK, FE
+        inv2.foe[ORG] = false;
+        BUGZ_LOG(fatal) << "result: " << OK << " " << inv2;
+        return trade_type_result{OK, inv2};
+      }
+
+      if (bsp1.foe[FUEL] != bsp2.foe[ORG]) {
+        // OK, FO
+        inv2.foe[EQU] = false;
+        BUGZ_LOG(fatal) << "result: " << OK << " " << inv2;
+        return trade_type_result{OK, inv2};
+      }
+
+      // Ok, take the highest (EQU)
+      inv2.foe[FUEL] = false;
+      inv2.foe[ORG] = false;
+      BUGZ_LOG(fatal) << "result: " << FAIR_E << " " << inv2;
+      return trade_type_result{FAIR_E, inv2};
+    }
+
+    // 2 matches.  but are they trade pairs?
+    if (bsp1.foe[pos[matches - 1]] != bsp1.foe[pos[matches - 2]]) {
+      // yes!
+      BUGZ_LOG(fatal) << "result: " << OK << " " << inv2;
+      return trade_type_result{OK, inv2};
+    } else {
+      // they are NOT.  Use highest one.  clear the lower flag
+
+      inv2.foe[pos[0]] = false;
+
+      switch (pos[1]) {
+        case 0:
+          BUGZ_LOG(fatal) << "result: " << FAIR_F << " " << inv2;
+          return trade_type_result{FAIR_F, inv2};
+          break;
+        case 1:
+          BUGZ_LOG(fatal) << "result: " << FAIR_O << " " << inv2;
+          return trade_type_result{FAIR_O, inv2};
+          break;
+        case 2:
+          BUGZ_LOG(fatal) << "result: " << FAIR_E << " " << inv2;
+          return trade_type_result{FAIR_E, inv2};
+          break;
+      }
+    }
+  }
+
+  if (matches == 1) {
+    switch (pos[0]) {
+      case 0:
+        BUGZ_LOG(fatal) << "result: " << FAIR_F << " " << inv2;
+        return trade_type_result{FAIR_F, inv2};
+        break;
+      case 1:
+        BUGZ_LOG(fatal) << "result: " << FAIR_O << " " << inv2;
+        return trade_type_result{FAIR_O, inv2};
+        break;
+      case 2:
+        BUGZ_LOG(fatal) << "result: " << FAIR_E << " " << inv2;
+        return trade_type_result{FAIR_E, inv2};
+        break;
+    }
+  }
+
+  // no matches.
+  BUGZ_LOG(fatal) << "result: " << NONE << " " << inv2;
+  return trade_type_result{NONE, inv2};
+}

+ 46 - 5
galaxy.h

@@ -27,6 +27,7 @@ struct sector_warps {
   // planets
   // ctor that zeros everything out?
   sector_warps();
+  sector_warps(sector_type, std::set<sector_type>);
   void add(sector_type sector);
   // add() that adds warp to end of warps?
   friend std::ostream& operator<<(std::ostream& os, const sector_warps& warps);
@@ -49,19 +50,29 @@ struct sector_warps {
  * @param port2
  * @return int
  */
-int trade_type(port_type port1, port_type port2);
+// int trade_type(port_type port1, port_type port2);
+
+enum trade_types {
+  NONE,
+  BEST,    // OE PAIR
+  OK,      // PAIR
+  FAIR_E,  // BS E
+  FAIR_O,  // BS O
+  FAIR_F,  // BS F
+};
+
 struct trade_type_result {
-  int type;
+  trade_types type;
   buysell trades;
 };
-trade_type_result trade_type_info(port_type port1, port_type port2);
+// trade_type_result trade_type_info(port_type port1, port_type port2);
 
 struct port_pair_type {
   int type;
+  buysell trades;
   sector_type s1, s2;
 };
 
-
 struct port {
   sector_type sector;
   uint8_t type;
@@ -74,6 +85,33 @@ struct port {
 
 struct port parse_portcim(const std::string line);
 
+// store density scan information in galaxy
+
+struct density {
+  sector_type sector;
+  uint16_t density;
+  uint16_t warps;
+  uint16_t navhaz;
+  bool anomaly;
+  bool known;
+  friend bool operator==(const struct density lhs, const struct density rhs);
+};
+
+class density_scan {
+ public:
+  sector_type sector;
+  std::array<density, 6> d;
+  density_scan();
+
+  // resets the scan with a new sector
+  void reset(sector_type sector);
+  void add_scan(density d);
+  density find(sector_type sector);
+
+ private:
+  int pos;
+};
+
 // SPECIAL = 0
 // STARDOCK = 9
 
@@ -87,6 +125,7 @@ class Galaxy {
   YAML::Node meta;
 
   int burnt_percent;
+  density_scan dscan;
 
   // warps;
   // ports;
@@ -103,9 +142,11 @@ class Galaxy {
   std::vector<port_pair_type> find_best_trades(void);
   std::vector<port_pair_type> find_trades(sector_type sector,
                                           bool highest = true);
+  trade_type_result trade_type_info(sector_type port1, sector_type port2,
+                                    int burnt_percent = 20);
   void sort_port_pair_type(std::vector<port_pair_type>& pptv);
   port_pair_type find_closest(int sector);
-  
+  port_pair_type find_closest_trade(int sector, int lowest_trade_type);
   char game;
   std::string username;
 };

+ 219 - 17
scripts.cpp

@@ -1,6 +1,9 @@
 #include "scripts.h"
 
 #include "logging.h"
+#include <boost/format.hpp>
+
+#ifdef DEPRECATED_SEE_TRADER_DISPATCH
 
 ScriptTrader::ScriptTrader(Director &d) : Dispatch(d) {
   BUGZ_LOG(fatal) << "ScriptTrader()";
@@ -15,17 +18,28 @@ void ScriptTrader::activate(void) {
   auto port_info = director.galaxy.ports.find(port[0]);
   int port0_type = port_info->second.type;
   port_buysell[0] = get_buysell(port0_type);
-  port_info = director.galaxy.ports.find(port[1]);
-  int port1_type = port_info->second.type;
-  port_buysell[1] = get_buysell(port1_type);
-  BUGZ_LOG(fatal) << port0_type << " and " << port1_type;
-  auto ttr = trade_type_info(port0_type, port1_type);
+
+  // Special case - we just want to buy resources
+  if (port[1] != 0) {
+    port_info = director.galaxy.ports.find(port[1]);
+    int port1_type = port_info->second.type;
+    port_buysell[1] = get_buysell(port1_type);
+    BUGZ_LOG(fatal) << port0_type << " and " << port1_type;
+  } else {
+    BUGZ_LOG(fatal) << "Just buy from " << port[0];
+  }
+
+  /*
+  auto ttr = director.galaxy.trade_type_info(port0_type, port1_type);
   trades = ttr.trades;
+  */
 
+  /*
   if (trades.foe[0] && trades.foe[1] && trades.foe[2]) {
     // it has all three -- use the last 2.
     trades.foe[0] = false;
   }
+  */
 
   // Ok, what do we do first here?
   // I - Info
@@ -194,14 +208,15 @@ void ScriptTrader::server_prompt(const std::string &prompt) {
       // Which port to trade with first?  examine trades
       BUGZ_LOG(fatal) << "trades: " << trades;
       BUGZ_LOG(fatal) << "port0:" << text_from_buysell(port_buysell[0]);
-      BUGZ_LOG(fatal) << "port1:" << text_from_buysell(port_buysell[1]);
+      if (port[1] != 0)
+        BUGZ_LOG(fatal) << "port1:" << text_from_buysell(port_buysell[1]);
 
-      // Do we have what they are buying?
-      bool have_buy = false;
-      int active_buy = 0;
-      int active_sell = 0;
-      bool all_holds_empty = false;
+      // Ok, I might still need this (so I know what port to start with)
+      // which is selling?
+      // must set active port!
 
+      bool all_holds_empty = false;
+      active_port = 0;
       // check the ship and holds here.  (MAYBE)
       int holds = director.galaxy.meta["ship"]["holds"]["total"].as<int>();
       if (director.galaxy.meta["ship"]["holds"]["empty"]) {
@@ -209,6 +224,50 @@ void ScriptTrader::server_prompt(const std::string &prompt) {
           all_holds_empty = true;
       }
 
+      if (port[1] == 0) {
+        active_port = port[0];
+      } else {
+        if (!all_holds_empty) {
+          for (int x = 0; x < 3; ++x) {
+            if (director.galaxy.meta["ship"]["holds"][foe[x]]) {
+              if (port_buysell[0].foe[x]) {
+                active_port = port[0];
+                break;
+              }
+              if (port_buysell[1].foe[x]) {
+                active_port = port[1];
+              }
+            }
+          }
+          if (active_port == 0) {
+            to_client(
+                "I don't see any ports that are buying what we have in our "
+                "holds.\n\r");
+            deactivate();
+            return;
+          };
+        } else {
+          // all holds empty, find selling port
+          for (int x = 0; x < 3; ++x) {
+            if (trades.foe[x]) {
+              if (port_buysell[0].foe[x]) {
+                active_port = port[0];
+                break;
+              }
+              if (port_buysell[1].foe[x]) {
+                active_port = port[1];
+                break;
+              }
+            }
+          }
+        }
+      }
+#ifdef NO_JUST_TRADES
+      // Do we have what they are buying?
+      bool have_buy = false;
+      int active_buy = 0;
+      int active_sell = 0;
+
       for (int x = 0; x < 3; ++x) {
         if (trades.foe[x]) {
           // this is what they will be trading...
@@ -256,6 +315,7 @@ void ScriptTrader::server_prompt(const std::string &prompt) {
           return;
         }
       }
+#endif
 
       state = 2;
       if (director.current_sector == active_port) {
@@ -272,9 +332,6 @@ void ScriptTrader::server_prompt(const std::string &prompt) {
         to_server(move);
         return;
       }
-
-      // for now...
-      deactivate();
     }
 
     if (state == 2) {
@@ -348,8 +405,9 @@ void ScriptTrader::server_prompt(const std::string &prompt) {
           to_server("0\r");
         } else
           for (int x = 0; x < 3; ++x) {
+            // if (foe[x] == selling) {
+            // We found the item ... is it something that we're trading?
             if (foe[x] == selling) {
-              // We found the item ... is it something that we're trading?
               if (trades.foe[x]) {
                 // Yes!
                 to_server("\r");
@@ -360,6 +418,7 @@ void ScriptTrader::server_prompt(const std::string &prompt) {
               }
             }
           }
+        // }
       }
     }
 
@@ -385,9 +444,13 @@ void ScriptTrader::server_prompt(const std::string &prompt) {
       // 1.) CHECK TURNS // need turn tracking
       // 2.) PORTS BURNT?
 
-      if (active_port == port[0])
+      if (active_port == port[0]) {
+        if (port[0] == 0) {
+          deactivate();
+          return;
+        }
         active_port = port[1];
-      else
+      } else
         active_port = port[0];
 
       // Is target port burnt?
@@ -424,4 +487,143 @@ void ScriptTrader::server_prompt(const std::string &prompt) {
     }
   }
 }
+
 void ScriptTrader::client_input(const std::string &cinput) { deactivate(); };
+
+#endif
+
+ScriptTerror::ScriptTerror(Director &d) : Dispatch(d) {
+  BUGZ_LOG(warning) << "ScriptTerror()";
+  init();
+}
+
+ScriptTerror::~ScriptTerror() { BUGZ_LOG(warning) << "~ScriptTerror()"; }
+
+void ScriptTerror::init(void) {
+  BUGZ_LOG(fatal) << "ScriptTerror::init()";
+
+  move = std::make_shared<MoveDispatch>(director);
+  md = static_cast<MoveDispatch *>(&(*move));
+  // setup notify functions for results/completion.
+  md->setNotify([this]() { this->move_notify(); });
+
+  input = std::make_shared<InputDispatch>(director);
+  id = static_cast<InputDispatch *>(&(*input));
+  id->prompt = "Number of loops: ";
+  id->max_length = 4;
+  id->numeric = true;
+  id->setNotify([this]() { this->input_notify(); });
+
+  trader = std::make_shared<TraderDispatch>(director);
+  td = static_cast<TraderDispatch *>(&(*trader));
+  td->setNotify([this]() { this->trade_notify(); });
+  BUGZ_LOG(fatal) << "ScriptTerror::init() completed.";
+}
+
+void ScriptTerror::activate(void) {
+  BUGZ_LOG(warning) << "ScriptTerror::activate()";
+  // Need: InputDispatch, MoveDispatch, ScriptTrader
+
+  // Step 1:  Get number of loops of terror
+  director.chain = input;
+  input->activate();
+
+  // Step 2:  Look for closest trades, try ScriptTrade until none < some level.
+  // Step 3:  Move on, unless out of loops (or low on turns)
+
+  // deactivate();
+}
+
+void ScriptTerror::deactivate(void) {
+  BUGZ_LOG(warning) << "ScriptTerror::deactivate()";
+  notify();
+}
+
+void ScriptTerror::input_notify(void) {
+  if (id->input.empty()) {
+    deactivate();
+    return;
+  }
+  max_loops = sstoi(id->input, -1);
+  if (max_loops == -1) {
+    deactivate();
+    return;
+  }
+  id->input.clear();
+  BUGZ_LOG(warning) << "Loops of terror: " << max_loops;
+  loops = max_loops;
+  // find nearest
+  ppt = director.galaxy.find_closest_trade(director.current_sector, 3);
+  if (ppt.type == 0) {
+    to_client("No trades found!  You've burnt the galaxy!\n\r");
+    deactivate();
+    return;
+  }
+  // ok, step 2:  move!
+  // md->setNotify([this]() { this->proxy_deactivate(); });
+
+  BUGZ_LOG(fatal) << "Moving to: " << ppt.s1;          
+  md->move_to = ppt.s1;
+  director.chain = move;
+  director.chain->activate();
+  return;
+}
+
+void ScriptTerror::move_notify(void) {
+  BUGZ_LOG(fatal) << "move_notify()";
+  // Check for success, and start trading!
+  if (md->success) {
+    to_client("We're here, get trading!\n\r");
+
+    td->port[0] = ppt.s1;
+    td->port[1] = ppt.s2;
+    td->trades = ppt.trades;
+    td->type = ppt.type;
+    director.chain = trader;
+    director.chain->activate();
+    return;
+  } else {
+    to_client("Move FAILED.\n\r");
+    deactivate();
+  }
+}
+
+void ScriptTerror::trade_notify(void) {
+  // Done trading -- maybe! :P
+  if (td->success) {
+    // success!
+    ppt = director.galaxy.find_closest_trade(director.current_sector, 3);
+    if (ppt.type == 0) {
+      to_client("No trades found!  You've burnt the galaxy!\n\r");
+      deactivate();
+      return;
+    }
+    if ((director.current_sector == ppt.s1) || (director.current_sector == ppt.s2)) {
+      // We're still here...
+      BUGZ_LOG(fatal) << "Trade it again, Sam.";
+      to_client("Keep trading.\n\r");
+      td->port[0] = ppt.s1;
+      td->port[1] = ppt.s2;
+      td->trades = ppt.trades;
+      td->type = ppt.type;
+      director.chain = trader;
+      director.chain->activate();
+      return;
+    }
+    // Ok, this isn't a local trade.
+    if (loops == 0) {
+      to_client("We're done terrorizing, for now...\n\r");
+      deactivate();
+      return;
+    }
+    --loops;
+    // Move to our next target
+    BUGZ_LOG(fatal) << "Moving to: " << ppt.s1;
+    md->move_to = ppt.s1;
+    director.chain = move;
+    director.chain->activate();
+    return;
+  }
+  to_client("Ok, trade is done.\n\r");
+  deactivate();
+}

+ 33 - 2
scripts.h

@@ -5,6 +5,7 @@
 #include "dispatchers.h"
 #include "galaxy.h"
 
+#ifdef DEPRECATED_SEE_TRADER_DISPATCH
 class ScriptTrader : public Dispatch {
  private:
  public:
@@ -22,7 +23,9 @@ class ScriptTrader : public Dispatch {
    * 3 = trade
    * 4 = if (burnt), stop, otherwise toggle active_port and state = 2
    *
-   *
+   * NEW: set port[1] to 0 for buy-only ability.
+   * Percent 20 doesn't work with Cargo Trans' 250 holds.
+   * Maybe look at the amount instead?  If < holds = burnt.
    */
   int state;
   float percent;
@@ -38,18 +41,46 @@ class ScriptTrader : public Dispatch {
   // information from the find_best_trades function + others.
   int port[2];
   int active;
+  // I don't care about trade type, just trades.
   int type;
+  
   buysell trades;
   buysell port_buysell[2];
 
   void activate(void) override;
   void deactivate(void) override;
 
-  // optional here
   void server_line(const std::string &line,
                    const std::string &raw_line) override;
   void server_prompt(const std::string &prompt) override;
   void client_input(const std::string &cinput) override;
 };
+#endif
+
+class ScriptTerror : public Dispatch {
+ private:
+  MoveDispatch * md;
+  std::shared_ptr<Dispatch> move;
+  InputDispatch * id;
+  std::shared_ptr<Dispatch> input;
+  TraderDispatch * td;
+  std::shared_ptr<Dispatch> trader;
+
+ public:
+  ScriptTerror(Director &);
+  ~ScriptTerror();
+  int loops;
+  int max_loops;
+  port_pair_type ppt;
+  
+  void init(void);
+
+  void activate(void) override;
+  void deactivate(void) override;
+
+  void input_notify(void);
+  void move_notify(void);
+  void trade_notify(void);
+};
 
 #endif

+ 24 - 6
test-director.cpp

@@ -63,7 +63,7 @@ TEST(director, director_galaxy_save) {
 
   dir.galaxy.username = "test";
   dir.galaxy.game = 'Z';
-  // This causes a YAML::Node Sequence 
+  // This causes a YAML::Node Sequence
   dir.galaxy.meta["trade"][10963][0] = 1;
   dir.galaxy.save();
 
@@ -119,8 +119,21 @@ TEST(director, director_parsing_density) {
     }
   }
 
+  std::array<density, 6> dense = {{{70, 0, 6, 0, false, true},
+                                   {441, 100, 2, 0, false, true},
+                                   {575, 1000, 2, 15, true, false},
+                                   {600, 40, 6, 0, false, true},
+                                   {629, 1, 4, 0, false, true},
+                                   {711, 101, 6, 0, false, true}}};
+
+  for (auto const& s : dense) {
+    auto d = dir.galaxy.dscan.find(s.sector);
+    EXPECT_EQ(d, s) << "Sector " << s.sector << " density";
+  }
+
   // Check that each sector was correctly processed
   std::vector<int> sectors = {70, 441, 575, 600, 629, 711};
+
   YAML::Node sector_data;
   sector_data[70]["density"] = 0;
   sector_data[70]["warps"] = 6;
@@ -156,15 +169,20 @@ TEST(director, director_parsing_density) {
   for (auto sector : sectors) {
     // GTEST_COUT << "Testing Sector " << sector << std::endl;
     EXPECT_EQ(dir.galaxy.meta["density"][sector]["density"].as<int>(),
-              sector_data[sector]["density"].as<int>()) << "Sector " << sector << " density";
+              sector_data[sector]["density"].as<int>())
+        << "Sector " << sector << " density";
     EXPECT_EQ(dir.galaxy.meta["density"][sector]["warps"].as<int>(),
-              sector_data[sector]["warps"].as<int>()) << "Sector " << sector << " warps";
+              sector_data[sector]["warps"].as<int>())
+        << "Sector " << sector << " warps";
     EXPECT_EQ(dir.galaxy.meta["density"][sector]["navhaz"].as<int>(),
-              sector_data[sector]["navhaz"].as<int>()) << "Sector " << sector << " navhaz";
+              sector_data[sector]["navhaz"].as<int>())
+        << "Sector " << sector << " navhaz";
     EXPECT_EQ(dir.galaxy.meta["density"][sector]["anom"].as<bool>(),
-              sector_data[sector]["anom"].as<bool>()) << "Sector " << sector << " anom";
+              sector_data[sector]["anom"].as<bool>())
+        << "Sector " << sector << " anom";
     EXPECT_EQ(dir.galaxy.meta["density"][sector]["known"].as<bool>(),
-              sector_data[sector]["known"].as<bool>()) << "Sector " << sector << " known";
+              sector_data[sector]["known"].as<bool>())
+        << "Sector " << sector << " known";
   }
 }
 

+ 22 - 1
test-galaxy.cpp

@@ -4,9 +4,9 @@
 #include <string>
 #include <vector>
 
+#include "director.h"
 #include "galaxy.h"
 #include "gtest/gtest.h"
-#include "director.h"
 
 /*
 
@@ -110,6 +110,7 @@ struct pair {
   };
 };
 
+#ifdef NOT_ANYMORE
 TEST(ports, trade_types) {
   std::map<pair, int> expected;
   for (int x = 0; x < 9; ++x) {
@@ -157,6 +158,7 @@ TEST(ports, trade_types) {
     }
   }
 }
+#endif
 
 TEST(ports, parse_portcim) {
   // This really needs to be checked against real lines from a log file
@@ -239,4 +241,23 @@ TEST(ports, parse_portcim) {
   // EXPECT_EQ(parse.percent, ports[0].percent);
 }
 
+TEST(galaxy, trade_type_info) {
+  // test this code to see that we get the correct trade type results.
+  Galaxy g;
+  g.add_port(port{68, 1, {1290, 557, 561}, {100, 47, 56}});
+  g.add_port(port{706, 4, {2490, 2590, 743}, {100, 100, 94}});
+  trade_type_result ttr = g.trade_type_info(68, 706);
+  EXPECT_EQ(ttr.type, BEST) << "Expect Trade Type BEST";
+  // burn out equipment
+  g.add_port(port{68, 1, {1290, 557, 161}, {100, 47, 16}});
+  ttr = g.trade_type_info(68, 706);
+  EXPECT_EQ(ttr.type, FAIR_O) << "Expect Trade Type FAIR_O";
+  std::vector<port_pair_type> vppt = g.find_trades(68);
+  EXPECT_EQ(vppt.size(), 0) << "Should have no trades, because no warps";
+  g.add_warp(sector_warps{706, {68}});
+  g.add_warp(sector_warps{68, {51, 693, 706, 904}});
+  vppt = g.find_trades(68);
+  EXPECT_EQ(vppt.size(), 1) << "Found a trade";
+}
+
 }  // namespace

+ 18 - 4
utils.cpp

@@ -152,7 +152,7 @@ bool at_computer_prompt(const std::string &prompt) {
 }
 
 bool at_planet_prompt(const std::string &prompt) {
-  if(startswith(prompt, "Planet command (?=Help)"))
+  if (startswith(prompt, "Planet command (?=Help)"))
     if (endswith(prompt, " [D] ")) return true;
   return false;
 }
@@ -171,7 +171,21 @@ void str_tolower(std::string &str) {
 void remove_telnet_commands(std::string &text) {
   size_t pos;
 
-  while ( ( pos = text.find('\xff')) != std::string::npos ) {
-    text.erase(pos, pos+3 );
+  while ((pos = text.find('\xff')) != std::string::npos) {
+    text.erase(pos, pos + 3);
   }
-}
+}
+
+int sstoi(const std::string &text, int failure) {
+  int result;
+  try {
+    result = stoi(text);
+  } catch (const std::invalid_argument &e) {
+    // BUGZ_LOG(fatal) << e.what();
+    return failure;
+  } catch (const std::out_of_range &e) {
+    // BUGZ_LOG(fatal) << e.what();
+    return failure;
+  }
+  return result;
+}

+ 9 - 0
utils.h

@@ -29,5 +29,14 @@ void str_toupper(std::string &text);
 void str_tolower(std::string &text);
 
 void remove_telnet_commands(std::string &text);
+/**
+ * Safe stoi
+ * 
+ * returns 0 on error/exception.
+ * 
+ * @param text 
+ * @return int 
+ */
+int sstoi(const std::string &text, int failure=0);
 
 #endif