Drop7 Research
approaches/lifetime-objective/scenario/scenario-io.hppC++232 lines · 7.6 KB
#pragma once

// JSONL serialization for scenarios.
//
// One scenario per line, so a suite is diffable, streamable, and readable from
// any language.  The reader ignores unknown keys, which lets `mint` write the
// scenario fields and its difficulty labels into the same object while `solve`
// and any later consumer still load the scenario alone.

#include "scenario.hpp"

#include <cctype>
#include <cstdlib>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>

namespace drop7::scenario {

inline std::string joinBytes(const std::uint8_t* values, std::size_t count) {
  std::string out;
  out.reserve(count * 3);
  for (std::size_t index = 0; index < count; ++index) {
    if (index != 0) out.push_back(',');
    out += std::to_string(static_cast<int>(values[index]));
  }
  return out;
}

// The scenario's own fields, without the enclosing braces, so a caller can
// append label fields to the same JSON object.
inline std::string scenarioFields(const Scenario& scenario) {
  std::ostringstream out;
  out << "\"schema\":\"drop7-scenario-v1\"";
  out << ",\"id\":\"" << std::string(scenario.id) << "\"";
  out << ",\"board\":[" << joinBytes(scenario.board.data(), kCellCount) << "]";
  out << ",\"latent\":[" << joinBytes(scenario.latent.data(), kCellCount)
      << "]";
  out << ",\"movesRemaining\":" << static_cast<int>(scenario.moves_remaining);
  out << ",\"horizon\":" << static_cast<int>(scenario.horizon);
  out << ",\"discTape\":["
      << joinBytes(scenario.disc_tape.data(), scenario.disc_tape.size()) << "]";
  out << ",\"riseLatent\":[";
  for (std::size_t row = 0; row < scenario.rise_latent.size(); ++row) {
    if (row != 0) out << ',';
    out << '[' << joinBytes(scenario.rise_latent[row].data(), kBoardSize)
        << ']';
  }
  out << ']';
  return out.str();
}

inline std::string serializeScenario(const Scenario& scenario) {
  return "{" + scenarioFields(scenario) + "}";
}

namespace io_detail {

inline std::size_t findKey(const std::string& line, const std::string& key) {
  const std::string needle = "\"" + key + "\"";
  std::size_t at = line.find(needle);
  if (at == std::string::npos) return std::string::npos;
  at = line.find(':', at + needle.size());
  if (at == std::string::npos) return std::string::npos;
  ++at;
  while (at < line.size() &&
         std::isspace(static_cast<unsigned char>(line[at]))) {
    ++at;
  }
  return at;
}

inline bool parseString(const std::string& line, const std::string& key,
                        std::string& out) {
  const std::size_t at = findKey(line, key);
  if (at == std::string::npos || line[at] != '"') return false;
  const std::size_t end = line.find('"', at + 1);
  if (end == std::string::npos) return false;
  out = line.substr(at + 1, end - at - 1);
  return true;
}

inline bool parseInt(const std::string& line, const std::string& key,
                     long long& out) {
  const std::size_t at = findKey(line, key);
  if (at == std::string::npos) return false;
  char* end = nullptr;
  out = std::strtoll(line.c_str() + at, &end, 10);
  return end != line.c_str() + at;
}

// Parses `[a,b,c]` starting at `at`; returns the index just past the ']'.
inline std::size_t parseFlatArray(const std::string& line, std::size_t at,
                                  std::vector<std::uint8_t>& out) {
  out.clear();
  if (at >= line.size() || line[at] != '[') return std::string::npos;
  ++at;
  while (at < line.size()) {
    while (at < line.size() &&
           (std::isspace(static_cast<unsigned char>(line[at])) ||
            line[at] == ',')) {
      ++at;
    }
    if (at < line.size() && line[at] == ']') return at + 1;
    char* end = nullptr;
    const long value = std::strtol(line.c_str() + at, &end, 10);
    if (end == line.c_str() + at) return std::string::npos;
    out.push_back(static_cast<std::uint8_t>(value));
    at = static_cast<std::size_t>(end - line.c_str());
  }
  return std::string::npos;
}

inline bool parseFlatArrayKey(const std::string& line, const std::string& key,
                              std::vector<std::uint8_t>& out) {
  const std::size_t at = findKey(line, key);
  if (at == std::string::npos) return false;
  return parseFlatArray(line, at, out) != std::string::npos;
}

inline bool parseNestedArrayKey(const std::string& line, const std::string& key,
                                std::vector<RiseRow>& out) {
  out.clear();
  std::size_t at = findKey(line, key);
  if (at == std::string::npos || line[at] != '[') return false;
  ++at;
  while (at < line.size()) {
    while (at < line.size() &&
           (std::isspace(static_cast<unsigned char>(line[at])) ||
            line[at] == ',')) {
      ++at;
    }
    if (at < line.size() && line[at] == ']') return true;
    std::vector<std::uint8_t> row;
    at = parseFlatArray(line, at, row);
    if (at == std::string::npos || row.size() != kBoardSize) return false;
    RiseRow fixed{};
    for (int index = 0; index < kBoardSize; ++index) fixed[index] = row[index];
    out.push_back(fixed);
  }
  return false;
}

}  // namespace io_detail

inline bool deserializeScenario(const std::string& line, Scenario& scenario,
                                std::string& error) {
  std::string schema;
  if (!io_detail::parseString(line, "schema", schema) ||
      schema != "drop7-scenario-v1") {
    error = "missing or unknown schema";
    return false;
  }
  std::string id;
  if (!io_detail::parseString(line, "id", id) || id.size() != 16) {
    error = "missing or malformed id";
    return false;
  }
  std::vector<std::uint8_t> board;
  std::vector<std::uint8_t> latent;
  std::vector<std::uint8_t> tape;
  if (!io_detail::parseFlatArrayKey(line, "board", board) ||
      board.size() != kCellCount) {
    error = "board must hold 49 values";
    return false;
  }
  if (!io_detail::parseFlatArrayKey(line, "latent", latent) ||
      latent.size() != kCellCount) {
    error = "latent must hold 49 values";
    return false;
  }
  if (!io_detail::parseFlatArrayKey(line, "discTape", tape)) {
    error = "discTape missing";
    return false;
  }
  long long moves_remaining = 0;
  long long horizon = 0;
  if (!io_detail::parseInt(line, "movesRemaining", moves_remaining) ||
      !io_detail::parseInt(line, "horizon", horizon)) {
    error = "movesRemaining or horizon missing";
    return false;
  }
  std::vector<RiseRow> rises;
  if (!io_detail::parseNestedArrayKey(line, "riseLatent", rises)) {
    error = "riseLatent missing or malformed";
    return false;
  }
  scenario = Scenario{};
  for (int index = 0; index < kCellCount; ++index) {
    scenario.board[index] = board[index];
    scenario.latent[index] = latent[index];
  }
  scenario.moves_remaining = static_cast<std::uint8_t>(moves_remaining);
  scenario.horizon = static_cast<std::uint8_t>(horizon);
  scenario.disc_tape = tape;
  scenario.rise_latent = rises;
  for (std::size_t index = 0; index < id.size(); ++index) {
    scenario.id[index] = id[index];
  }
  scenario.id[16] = '\0';
  if (!scenarioIdMatches(scenario)) {
    error = "content hash does not match the stored id";
    return false;
  }
  return true;
}

inline bool loadScenarioFile(const std::string& path,
                             std::vector<Scenario>& out, std::string& error) {
  std::ifstream input(path);
  if (!input) {
    error = "cannot open " + path;
    return false;
  }
  std::string line;
  int line_number = 0;
  while (std::getline(input, line)) {
    ++line_number;
    if (line.find_first_not_of(" \t\r\n") == std::string::npos) continue;
    Scenario scenario;
    std::string line_error;
    if (!deserializeScenario(line, scenario, line_error)) {
      error = path + ":" + std::to_string(line_number) + ": " + line_error;
      return false;
    }
    out.push_back(scenario);
  }
  return true;
}

}  // namespace drop7::scenario