diff --git a/evolver/evolver.cpp b/evolver/evolver.cpp
deleted file mode 100644
index fb80d4f..0000000
--- a/evolver/evolver.cpp
+++ /dev/null
@@ -1,361 +0,0 @@
-#include <lib94/lib94.hpp>
-#include <filesystem>
-#include <algorithm>
-#include <iostream>
-#include <cstdlib>
-#include <fstream>
-#include <sstream>
-#include <mpi.h>
-
-constexpr size_t max_good_warriors = 100;
-constexpr int rounds_per_matchup = 100;
-constexpr int steps_to_tie = 1000000;
-
-constexpr lib94::instruction background_instruction = {
-  .op = lib94::DAT, .mod = lib94::F,
-  .amode = lib94::DIRECT, .bmode = lib94::DIRECT,
-  .anumber = 0, .bnumber = 0
-};
-
-lib94::warrior *warrior_to_beat;
-int comm_rank, comm_size;
-
-std::filesystem::path output_dir;
-
-std::vector<lib94::warrior *> seed_warriors;
-
-void head_main();
-void child_main();
-
-int main(int argc, char **argv) {
-
-  MPI_Init(&argc, &argv);
-
-  MPI_Comm_rank(MPI_COMM_WORLD, &comm_rank);
-  MPI_Comm_size(MPI_COMM_WORLD, &comm_size);
-
-  if (comm_size < 2) {
-    std::cerr << "you need at least two processes." << std::endl;
-    return 1;
-  }
-
-  if (argc < 4) {
-    if (comm_rank == 0)
-      std::cerr << "usage: " << argv[0] << " <warrior to beat> <output directory> <seed 1> <seed 2> ..." << std::endl;
-    return 2;
-  }
-
-  try {
-    warrior_to_beat = lib94::compile_warrior_from_file(argv[1]);
-  }
-  catch (const lib94::compiler_exception &ex) {
-    if (comm_rank == 0)
-      std::cerr
-        << "syntax error on line " << ex.source_line_number
-        << " of " << argv[1] << ":\n  " << ex.message << std::endl;
-    return 3;
-  }
-
-  if (warrior_to_beat == 0) {
-    if (comm_rank == 0)
-      std::cerr << "could not read file " << argv[1] << std::endl;
-    return 4;
-  }
-
-  output_dir = argv[2];
-
-  for (int i = 3; i < argc; ++i) {
-
-    try {
-      seed_warriors.push_back(lib94::compile_warrior_from_file(argv[i]));
-    }
-    catch (const lib94::compiler_exception &ex) {
-      if (comm_rank == 0)
-        std::cerr
-          << "syntax error on line " << ex.source_line_number
-          << " of " << argv[i] << ":\n  " << ex.message << std::endl;
-      return 3;
-    }
-
-    if (seed_warriors.back() == 0) {
-      if (comm_rank == 0)
-        std::cerr << "could not read file " << argv[i] << std::endl;
-      return 4;
-    }
-
-  }
-
-  if (comm_rank == 0)
-    head_main();
-
-  else
-    child_main();
-
-  return 0;
-
-}
-
-struct scored_warrior {
-  int score;
-  lib94::warrior *w;
-  bool operator <(const scored_warrior &other) const {
-    return score > other.score || (score == other.score && w->instructions.size() < other.w->instructions.size());
-  }
-};
-
-void head_main() {
-
-  std::cout << "\x1b""7\x1b[?47h\x1b[2J" << std::flush;
-
-  std::vector<scored_warrior> warriors;
-  for (lib94::warrior *const &w : seed_warriors)
-    warriors.push_back({.score = -1, .w = w});
-
-  int *warrior_counts = new int[comm_size];
-
-  int round = 1;
-
-  while (true) {
-
-    std::cout << "\x1b[1;1Hhead node: sending warriors\x1b[0K" << std::flush;
-
-    int warrior_count = warriors.size();
-    MPI_Bcast(&warrior_count, 1, MPI_INT, 0, MPI_COMM_WORLD);
-
-    for (scored_warrior &sw : warriors) {
-      std::vector<uint8_t> serialization;
-      lib94::serialize_warrior(sw.w, serialization);
-      int serialization_length = serialization.size();
-      MPI_Bcast(&serialization_length, 1, MPI_INT, 0, MPI_COMM_WORLD);
-      MPI_Bcast(serialization.data(), serialization_length, MPI_UINT8_T, 0, MPI_COMM_WORLD);
-    }
-
-    if (round == 1) {
-      for (scored_warrior &sw : warriors)
-        delete sw.w;
-      warriors.clear();
-    }
-
-    std::cout << "\x1b[1;1Hhead node: waiting for child nodes\x1b[0K" << std::flush;
-
-    MPI_Gather(warrior_counts, 1, MPI_INT, warrior_counts, 1, MPI_INT, 0, MPI_COMM_WORLD);
-
-    for (int i = 1; i < comm_size; ++i) {
-      std::cout << "\x1b[1;1Hhead node: receiving warriors from child node " << i << "\x1b[0K" << std::flush;
-      for (int j = 0; j < warrior_counts[i]; ++j) {
-        int score;
-        MPI_Recv(&score, 1, MPI_INT, i, 0, MPI_COMM_WORLD, 0);
-        int serialization_length;
-        MPI_Recv(&serialization_length, 1, MPI_INT, i, 0, MPI_COMM_WORLD, 0);
-        uint8_t *buffer = new uint8_t[serialization_length];
-        MPI_Recv(buffer, serialization_length, MPI_UINT8_T, i, 0, MPI_COMM_WORLD, 0);
-        warriors.push_back({.score = score, .w = lib94::deserialize_warrior(buffer)});
-        delete[] buffer;
-      }
-    }
-
-    std::sort(warriors.begin(), warriors.end());
-
-    std::filesystem::path out_path = output_dir / (std::to_string(round) + ".red");
-
-    int total_score = 0;
-    for (const scored_warrior &sw : warriors)
-      total_score += sw.score;
-
-    std::cout << "\x1b[" << (comm_size + 2) << ";1Hmin/avg/max score from round " << round << ":\n  "
-      << (float)(warriors[warriors.size() - 1].score * 100) / (rounds_per_matchup * 2) << "% / "
-      << (float)(total_score * 100) / (warriors.size() * rounds_per_matchup * 2) << "% / "
-      << (float)(warriors[0].score * 100) / (rounds_per_matchup * 2) << "%\x1b[0K\n\nbest was:\n";
-
-    std::ofstream out_file(out_path);
-    if (!out_file) {
-      std::cout << "\x1b[?47l\x1b""8" << std::flush;
-      std::cerr << "could not open file " << out_path << std::endl;
-      int zero = 0;
-      MPI_Bcast(&zero, 1, MPI_INT, 0, MPI_COMM_WORLD);
-      MPI_Finalize();
-      exit(5);
-    }
-
-    out_file << ";author evolver\n;name evolved\n";
-    out_file << ";score was " << (float)(warriors[0].score * 100) / (rounds_per_matchup * 2) << "%\n";
-    out_file << "org " << warriors[0].w->org << '\n';
-    std::cout << "  org " << warriors[0].w->org << "\x1b[0K\n";
-    for (const lib94::instruction &i : warriors[0].w->instructions) {
-      out_file << lib94::instruction_to_string(i) << '\n';
-      std::cout << "  " << lib94::instruction_to_string(i) << "\x1b[0K\n";
-    }
-
-    std::cout << "\x1b[0J" << std::flush;
-
-    out_file.close();
-
-    if (warriors[0].score == rounds_per_matchup * 2) {
-      std::cout << "\x1b[?47l\x1b""8completed in " << round << " rounds\n";
-      int zero = 0;
-      MPI_Bcast(&zero, 1, MPI_INT, 0, MPI_COMM_WORLD);
-      MPI_Finalize();
-      exit(0);
-    }
-
-    size_t good_warriors = std::min(max_good_warriors, warriors.size());
-
-    for (size_t i = good_warriors; i < warriors.size(); ++i)
-      delete warriors[i].w;
-
-    warriors.resize(good_warriors);
-
-    ++round;
-
-  }
-
-}
-
-void randomize_in_range(lib94::instruction &instr, int min, int max) {
-  instr.op = (lib94::opcode)(rand() % 16);
-  instr.mod = (lib94::modifier)(rand() % 7);
-  instr.amode = (lib94::mode)(rand() % 8);
-  instr.bmode = (lib94::mode)(rand() % 8);
-  instr.anumber = (rand() % (max - min + 1) + min + LIB94_CORE_SIZE) % LIB94_CORE_SIZE;
-  instr.bnumber = (rand() % (max - min + 1) + min + LIB94_CORE_SIZE) % LIB94_CORE_SIZE;
-}
-
-void randomize(lib94::instruction &instr) {
-  instr.op = (lib94::opcode)(rand() % 16);
-  instr.mod = (lib94::modifier)(rand() % 7);
-  instr.amode = (lib94::mode)(rand() % 8);
-  instr.bmode = (lib94::mode)(rand() % 8);
-  instr.anumber = rand() % LIB94_CORE_SIZE;
-  instr.bnumber = rand() % LIB94_CORE_SIZE;
-}
-
-void mutate_step(lib94::warrior *w) {
-
-  if (w->instructions.size() == 0) {
-    bool in_range = rand() % 2 == 0;
-    lib94::instruction instruction;
-    if (in_range)
-      randomize_in_range(instruction, 0, 0);
-    else
-      randomize(instruction);
-    w->instructions.push_back(instruction);
-    return;
-  }
-
-  int rand_inclusive = rand() % (w->instructions.size() + 1);
-  int rand_exclusive = rand() % w->instructions.size();
-  bool in_range = rand() % 2 == 0;
-
-  switch (rand() % 4) {
-
-  //org
-  case 0:
-    w->org = rand_exclusive;
-    return;
-
-  //insert
-  case 1:
-    w->instructions.insert(w->instructions.cbegin() + rand_inclusive, (lib94::instruction){});
-    if (w->org > rand_inclusive)
-      ++w->org;
-    rand_exclusive = rand_inclusive;
-    //FALLTHRU
-
-  //modify
-  case 2:
-    if (in_range)
-      randomize_in_range(w->instructions[rand_exclusive], -rand_exclusive, w->instructions.size() - rand_exclusive - 1);
-    else
-      randomize(w->instructions[rand_exclusive]);
-    return;
-
-  //remove
-  case 3:
-    w->instructions.erase(w->instructions.cbegin() + rand_exclusive);
-    if (w->org > rand_exclusive)
-      --w->org;
-    return;
-
-  }
-
-}
-
-void mutate(lib94::warrior *w) {
-  do
-    mutate_step(w);
-  while (rand() % 2 == 0);
-}
-
-int score(const lib94::warrior *w) {
-  int the_score = 0;
-  const lib94::warrior *const warriors[2] = {warrior_to_beat, w};
-  for (int i = 0; i < rounds_per_matchup; ++i) {
-    const lib94::warrior *winner = lib94::do_round(background_instruction, warriors, 2, 0, true, steps_to_tie);
-    if (winner == 0)
-      ++the_score;
-    else if (winner == w)
-      the_score += 2;
-  }
-  return the_score;
-}
-
-void child_main() {
-
-  srand(time(0));
-
-  std::vector<scored_warrior> warriors;
-
-  while (true) {
-
-    int warrior_count;
-    MPI_Bcast(&warrior_count, 1, MPI_INT, 0, MPI_COMM_WORLD);
-
-    if (warrior_count == 0) {
-      MPI_Finalize();
-      exit(0);
-    }
-
-    warriors.resize(warrior_count);
-
-    std::cout << "\x1b[" << (comm_rank + 1) << ";1Hchild node " << comm_rank << ": receiving warriors\x1b[0K" << std::flush;
-
-    for (int i = 0; i < warrior_count; ++i) {
-      int serialization_length;
-      MPI_Bcast(&serialization_length, 1, MPI_INT, 0, MPI_COMM_WORLD);
-      uint8_t *buffer = new uint8_t[serialization_length];
-      MPI_Bcast(buffer, serialization_length, MPI_UINT8_T, 0, MPI_COMM_WORLD);
-      warriors[i].w = lib94::deserialize_warrior(buffer);
-      delete[] buffer;
-    }
-
-    std::cout << "\x1b[" << (comm_rank + 1) << ";1Hchild node " << comm_rank << ": mutating warriors\x1b[0K" << std::flush;
-
-    for (scored_warrior &sw : warriors)
-      mutate(sw.w);
-
-    for (size_t i = 0; i < warriors.size(); ++i) {
-      std::cout << "\x1b[" << (comm_rank + 1) << ";1Hchild node " << comm_rank << ": scoring warriors " << (float)(i * 100) / warriors.size() << "%\x1b[0K" << std::flush;
-      warriors[i].score = score(warriors[i].w);
-    }
-
-    std::cout << "\x1b[" << (comm_rank + 1) << ";1Hchild node " << comm_rank << ": waiting for other child nodes\x1b[0K" << std::flush;
-
-    MPI_Gather(&warrior_count, 1, MPI_INT, 0, 0, 0, 0, MPI_COMM_WORLD);
-
-    for (scored_warrior &sw : warriors) {
-      std::vector<uint8_t> serialization;
-      lib94::serialize_warrior(sw.w, serialization);
-      int serialization_length = serialization.size();
-      MPI_Send(&sw.score, 1, MPI_INT, 0, 0, MPI_COMM_WORLD);
-      MPI_Send(&serialization_length, 1, MPI_INT, 0, 0, MPI_COMM_WORLD);
-      MPI_Send(serialization.data(), serialization_length, MPI_UINT8_T, 0, 0, MPI_COMM_WORLD);
-    }
-
-    for (scored_warrior &sw : warriors)
-      delete sw.w;
-
-    warriors.clear();
-
-  }
-
-}
diff --git a/include/lib94/lib94.hpp b/include/lib94/lib94.hpp
index 71ec68e..9b4147c 100644
--- a/include/lib94/lib94.hpp
+++ b/include/lib94/lib94.hpp
@@ -6,6 +6,7 @@
 #include <string>
 #include <vector>
 #include <deque>
+#include <mpi.h>
 #include <set>
 
 #ifndef LIB94_CORE_SIZE
@@ -22,7 +23,8 @@ namespace lib94 {
   //it has to be at least big enough to represent LIB94_CORE_SIZE squared,
   //since it is used as an intermediate for all of the instructions,
   //including in particular MUL.
-  typedef int_least32_t number_t;
+  typedef int32_t number_t;
+  #define LIB94_MPI_NUMBER_T MPI_INT32_T
 
   enum opcode : uint8_t {
     DAT, MOV, ADD, SUB,
@@ -162,10 +164,17 @@ namespace lib94 {
 
   //convenience method - calls clear_core(background), then init_round(warriors, count,
   //offsets, shuffle), then single_step<false> until either one warrior remains or that
-  //has been called rounds_to_tie times. if one warrior remains, returns the pointer to
+  //has been called steps_to_tie times. if one warrior remains, returns the pointer to
   //that warrior. if a tie is reached, returns a null pointer. this asserts that the call
   //to init_round returns true. see comment on init_round.
-  const warrior *do_round(const instruction &background, const warrior *const *warriors, size_t count, const number_t *offsets, bool shuffle, long rounds_to_tie);
+  const warrior *do_round(const instruction &background, const warrior *const *warriors, size_t count, const number_t *offsets, bool shuffle, long steps_to_tie);
+
+  //runs every possible offset and turn order using do_round.
+  //asserts that total length is not more than core size.
+  //should be called from everyone in a communicator.
+  //assumes comm ranks go from 0 to comm size - 1.
+  template <bool print_progress>
+  void tabulate_mpi(const instruction &background, const warrior *w1, const warrior *w2, long steps_to_tie, int &w1_wins_out, int &w2_wins_out, int &rounds_out, MPI_Comm communicator);
 
 }
 
diff --git a/lib94/core.cpp b/lib94/core.cpp
index 5bbe00f..790205e 100644
--- a/lib94/core.cpp
+++ b/lib94/core.cpp
@@ -1,8 +1,10 @@
 #include <lib94/lib94.hpp>
 #include <functional>
 #include <algorithm>
+#include <iostream>
 #include <cassert>
 #include <random>
+#include <mpi.h>
 
 namespace lib94 {
 
@@ -319,13 +321,13 @@ namespace lib94 {
   template const warrior *single_step<true>();
   template const warrior *single_step<false>();
 
-  const warrior *do_round(const instruction &background, const warrior *const *warriors, size_t count, const number_t *offsets, bool shuffle, long rounds_to_tie) {
+  const warrior *do_round(const instruction &background, const warrior *const *warriors, size_t count, const number_t *offsets, bool shuffle, long steps_to_tie) {
 
     clear_core(background);
     assert(init_round(warriors, count, offsets, shuffle));
     size_t warriors_left = count;
 
-    for (long i = 0; i < rounds_to_tie; ++i)
+    for (long i = 0; i < steps_to_tie; ++i)
       if (single_step<false>() != 0) {
         --warriors_left;
         if (warriors_left == 1)
@@ -336,4 +338,95 @@ namespace lib94 {
 
   }
 
+  template <bool print_progress>
+  void tabulate_mpi(const instruction &background, const warrior *w1, const warrior *w2, long steps_to_tie, int &w1_wins_out, int &w2_wins_out, int &rounds_out, MPI_Comm communicator) {
+
+    assert(w1->instructions.size() + w2->instructions.size() <= LIB94_CORE_SIZE);
+
+    std::vector<number_t> w2_offsets;
+    for (number_t i = w1->instructions.size(); i + w2->instructions.size() <= LIB94_CORE_SIZE; ++i)
+      w2_offsets.push_back(i);
+
+    int comm_size;
+    int comm_rank;
+
+    MPI_Comm_size(communicator, &comm_size);
+    MPI_Comm_rank(communicator, &comm_rank);
+
+    if constexpr (print_progress)
+      if (comm_rank == 0)
+        for (int i = 0; i < comm_size; ++i)
+          std::cerr << '\n';
+
+    int actual_count = w2_offsets.size();
+    int per_process = (actual_count - 1) / comm_size + 1;
+
+    w2_offsets.resize(per_process * comm_size);
+    for (int i = actual_count; i < per_process * comm_size; ++i)
+      w2_offsets[i] = LIB94_CORE_SIZE + 1;
+
+    std::shuffle(w2_offsets.begin(), w2_offsets.end(), prng);
+
+    number_t *recv_buffer = new number_t[per_process];
+    MPI_Scatter(w2_offsets.data(), per_process, LIB94_MPI_NUMBER_T, recv_buffer, per_process, LIB94_MPI_NUMBER_T, 0, communicator);
+
+    int wins[2] = {0, 0};
+
+    const warrior *w1_first_warriors[2] = {w1, w2};
+    number_t w1_first_offsets[2] = {0, 0};
+
+    const warrior *w2_first_warriors[2] = {w2, w1};
+    number_t w2_first_offsets[2] = {0, 0};
+
+    int last_percent = -1;
+
+    for (int i = 0; i < per_process; ++i)
+      if (recv_buffer[i] != LIB94_CORE_SIZE + 1) {
+
+        if constexpr (print_progress) {
+          int percent = i * 100 / per_process;
+          if (percent != last_percent) {
+            std::cout << "\x1b[" << comm_size - comm_rank << "A\x1b[3G";
+            std::cout << "worker " << comm_rank + 1 << ": " << percent << "%\x1b[0K";
+            std::cout << "\x1b[" << comm_size - comm_rank << "B\x1b[1G";
+            std::cout << std::flush;
+          }
+        }
+
+        w1_first_offsets[1] = recv_buffer[i];
+        const warrior *w = do_round(background, w1_first_warriors, 2, w1_first_offsets, false, steps_to_tie);
+        if (w == w1)
+          ++wins[0];
+        else if (w == w2)
+          ++wins[1];
+
+        w2_first_offsets[0] = recv_buffer[i];
+        w = do_round(background, w2_first_warriors, 2, w2_first_offsets, false, steps_to_tie);
+        if (w == w1)
+          ++wins[0];
+        else if (w == w2)
+          ++wins[1];
+
+      }
+
+
+    if constexpr (print_progress) {
+      std::cout << "\x1b[" << comm_size - comm_rank << "A\x1b[3G";
+      std::cout << "worker " << comm_rank + 1 << ": complete\x1b[0K";
+      std::cout << "\x1b[" << comm_size - comm_rank << "B\x1b[1G";
+      std::cout << std::flush;
+    }
+
+    int win_sums[2];
+
+    MPI_Allreduce(wins, win_sums, 2, MPI_INT, MPI_SUM, communicator);
+    w1_wins_out = win_sums[0];
+    w2_wins_out = win_sums[1];
+    rounds_out = actual_count * 2;
+
+  }
+
+  template void tabulate_mpi<true>(const instruction &background, const warrior *w1, const warrior *w2, long steps_to_tie, int &w1_wins_out, int &w2_wins_out, int &rounds_out, MPI_Comm communicator);
+  template void tabulate_mpi<false>(const instruction &background, const warrior *w1, const warrior *w2, long steps_to_tie, int &w1_wins_out, int &w2_wins_out, int &rounds_out, MPI_Comm communicator);
+
 }
diff --git a/makefile b/makefile
index 5cfcef9..b634f2d 100644
--- a/makefile
+++ b/makefile
@@ -1,26 +1,20 @@
-CPP_ARGS = -std=c++20 -O2 -Wall -Wextra -Iinclude -ggdb
+# https://github.com/open-mpi/ompi/issues/5157
+CPP_ARGS = -std=c++20 -O2 -Wall -Wextra -Iinclude -ggdb $(shell mpic++ --showme:compile) -DOMPI_SKIP_MPICXX
+MPI_LD_ARGS = $(shell mpic++ --showme:link)
 
 GTKMM_CPP_ARGS = $(shell pkg-config --cflags gtkmm-4.0)
 GTKMM_LD_ARGS = $(shell pkg-config --libs gtkmm-4.0)
 
-# https://github.com/open-mpi/ompi/issues/5157
-MPI_CPP_ARGS = $(shell mpic++ --showme:compile) -DOMPI_SKIP_MPICXX
-MPI_LD_ARGS = $(shell mpic++ --showme:link)
-
-default: bin/bench bin/evolver bin/tabulator-mpi
+default: bin/bench bin/tabulator
 
 clean:
 	rm -r obj bin
 
 bin/bench: obj/bench/main.o obj/bench/bench_window.o obj/bench/core_widget.o obj/lib94.o
 	@mkdir -p $(dir $@)
-	g++ ${CPP_ARGS} $^ ${GTKMM_LD_ARGS} -o $@
+	g++ ${CPP_ARGS} $^ ${MPI_LD_ARGS} ${GTKMM_LD_ARGS} -o $@
 
-bin/evolver: obj/evolver/evolver.o obj/lib94.o
-	@mkdir -p $(dir $@)
-	g++ ${CPP_ARGS} $^ ${MPI_LD_ARGS} -o $@
-
-bin/tabulator-mpi: obj/tabulator-mpi/source.o obj/lib94.o
+bin/tabulator: obj/tabulator/tabulator.o obj/lib94.o
 	@mkdir -p $(dir $@)
 	g++ ${CPP_ARGS} $^ ${MPI_LD_ARGS} -o $@
 
@@ -31,14 +25,6 @@ obj/bench/%.o: bench/%.cpp
 	@mkdir -p $(dir $@)
 	g++ -c ${CPP_ARGS} ${GTKMM_CPP_ARGS} $^ -o $@
 
-obj/evolver/%.o: evolver/%.cpp
-	@mkdir -p $(dir $@)
-	g++ -c ${CPP_ARGS} ${MPI_CPP_ARGS} $^ -o $@
-
-obj/tabulator-mpi/%.o: tabulator-mpi/%.cpp
-	@mkdir -p $(dir $@)
-	g++ -c ${CPP_ARGS} ${MPI_CPP_ARGS} $^ -o $@
-
 obj/%.o: %.cpp
 	@mkdir -p $(dir $@)
 	g++ -c ${CPP_ARGS} $^ -o $@
diff --git a/readme.txt b/readme.txt
index fa275a1..89cf3e6 100644
--- a/readme.txt
+++ b/readme.txt
@@ -30,22 +30,7 @@ To open bench, just run bin/bench after building as above.
 
 The "tabulator" program runs every possible pairing of warriors from a selection against each other with every possible separation
 and turn order, and then shows the number of wins of each warrior against each other warrior in a table format. The column header
-is the winner, and the row header is the loser. This program uses MPI to run batches of these rounds in different processes, and
-communicate the results back to a head process.
+is the winner, and the row header is the loser. This program uses MPI to run this across multiple processes.
 
 To run all of the included warriors against each other, run
-  mpirun bin/tabulator-mpi warriors/*.red
-
-Note that tabulator expects at least two processes (one head process and at least one worker). If you only have one core, you may run
-  mpirun -np 2 --oversubscribe bin/tabulator-mpi warriors/*.red
-
-=== evolver ===
-
-The experimental "evolver" program attempts to create warriors who are good against a particular other
-warrior via a process similar to genetic evolution. I may or may not attempt to improve it in the future.
-
-The evolver takes an arbitrary number of input warriors as "seeds". To evolve a warrior against Dwarf, using
-all of the included warriors as seeds, and storing the outputs from each round in a directory named output, run
-  mpirun bin/evolver warriors/dwarf.red output warriors/*.red
-
-As with tabulator, evolver requires at least two processes, and you will have to oversubscribe if you only have one core.
+  mpirun bin/tabulator warriors/*.red
diff --git a/tabulator-mpi/mpi.txt b/tabulator-mpi/mpi.txt
deleted file mode 100644
index 9ee7ff4..0000000
--- a/tabulator-mpi/mpi.txt
+++ /dev/null
@@ -1,15 +0,0 @@
-message tag 0:
-  head -> worker
-  four ints:
-    warrior 1 index
-    warrior 2 index
-    start round
-    end round (exclusive, 0 for exit)
-
-message tag 1:
-  worker -> head
-  four ints:
-    warrior 1 index
-    warrior 2 index
-    w1 wins against w2
-    w2 wins against w1
diff --git a/tabulator-mpi/source.cpp b/tabulator-mpi/source.cpp
deleted file mode 100644
index f7fe54f..0000000
--- a/tabulator-mpi/source.cpp
+++ /dev/null
@@ -1,214 +0,0 @@
-#include <lib94/lib94.hpp>
-#include <iostream>
-#include <fstream>
-#include <sstream>
-#include <cstdio>
-#include <vector>
-#include <mpi.h>
-
-const int default_rounds_per_chunk = 250;
-const int steps_to_tie = 1000000;
-
-int error(std::string msg, int rank) {
-  if (rank == 0) {
-    std::cerr << msg << std::endl;
-    return 1;
-  }
-  return 0;
-}
-
-int main(int argc, char **argv) {
-
-  MPI_Init(&argc, &argv);
-
-  int rank;
-  int size;
-
-  MPI_Comm_rank(MPI_COMM_WORLD, &rank);
-  MPI_Comm_size(MPI_COMM_WORLD, &size);
-
-  if (size < 2)
-    return error("this must be run under mpi with at least two processes.", rank);
-
-  std::vector<std::string> filenames = {};
-  int rounds_per_chunk = default_rounds_per_chunk;
-
-  for (int i = 1; i < argc; ++i) {
-    if (atoi(argv[i]) > 0)
-      rounds_per_chunk = atoi(argv[i]);
-    else
-      filenames.push_back(argv[i]);
-  }
-
-  if (filenames.size() == 0)
-    return error("no files specified.", rank);
-  if (filenames.size() == 1)
-    return error("only one file specified.", rank);
-
-  int count = filenames.size();
-  lib94::warrior **warriors = new lib94::warrior *[count];
-  for (int i = 0; i < count; ++i) {
-    std::ifstream file(filenames[i]);
-    if (!file)
-      return error("could not open " + filenames[i] + ".", rank);
-    std::stringstream stream;
-    stream << file.rdbuf();
-    try {
-      warriors[i] = lib94::compile_warrior(stream.str());
-    }
-    catch (const lib94::compiler_exception &ex) {
-      return error("could not compile " + filenames[i] + ": " + ex.message + " on line " + std::to_string(ex.source_line_number) + ".", rank);
-    }
-  }
-
-  //w1 * count + w2
-  int *placements = new int[count * count];
-  for (int i = 0; i < count; ++i)
-    for (int j = 0; j < count; ++j) {
-      int p = LIB94_CORE_SIZE - warriors[i]->instructions.size() - warriors[j]->instructions.size() + 1;
-      if (p <= 0)
-        return error(filenames[i] + " and " + filenames[j] + " do not fit in core together.", rank);
-      placements[i * count + j] = p;
-    }
-
-  if (rank == 0) {
-
-    std::cerr << "\x1b""7\x1b[?47h\x1b[2J" << std::flush;
-
-    //w1 * count + w2
-    int *wins_array = new int[count * count];
-    for (int i = 0; i < count * count; ++i)
-      wins_array[i] = 0;
-
-    for (int i = 0; i < count; ++i)
-      for (int j = 0; j < count; ++j) {
-        if (i == j)
-          continue;
-        int rounds = placements[i * count + j];
-        for (int r = 0; r < rounds; r += rounds_per_chunk) {
-          int e = std::min(r + rounds_per_chunk, rounds);
-
-          MPI_Status status;
-          int buffer[4];
-          MPI_Recv(buffer, 4, MPI_INT, MPI_ANY_SOURCE, 1, MPI_COMM_WORLD, &status);
-          int source = status.MPI_SOURCE;
-
-          wins_array[buffer[0] * count + buffer[1]] += buffer[2];
-          wins_array[buffer[1] * count + buffer[0]] += buffer[3];
-
-          buffer[0] = i;
-          buffer[1] = j;
-          buffer[2] = r;
-          buffer[3] = e;
-          MPI_Send(buffer, 4, MPI_INT, source, 0, MPI_COMM_WORLD);
-
-          std::cerr << "\x1b[" << source << ";1Hworker " << source << ": " << warriors[i]->name << " vs " << warriors[j]->name << " rounds " << r + 1 << " to " << e << " of " << rounds << ".\x1b[0K" << std::flush;
-        }
-      }
-
-    for (int i = 1; i < size; ++i) {
-      MPI_Status status;
-      int buffer[4];
-      MPI_Recv(buffer, 4, MPI_INT, MPI_ANY_SOURCE, 1, MPI_COMM_WORLD, &status);
-      int source = status.MPI_SOURCE;
-
-      wins_array[buffer[0] * count + buffer[1]] += buffer[2];
-      wins_array[buffer[1] * count + buffer[0]] += buffer[3];
-
-      buffer[3] = 0;
-      MPI_Send(buffer, 4, MPI_INT, source, 0, MPI_COMM_WORLD);
-      std::cerr << "\x1b[" << source << ";1Hworker " << source << ": complete.\x1b[0K" << std::flush;
-    }
-
-    std::cerr << "\x1b[?47l\x1b""8";
-
-    int col_width = 13;
-    for (int i = 0; i < count; ++i)
-      if ((int)warriors[i]->name.size() > col_width)
-        col_width = (int)warriors[i]->name.size();
-
-    printf(" %*s", col_width, "");
-    for (int i = 0; i < count; ++i)
-      printf(" | %*s", col_width, warriors[i]->name.c_str());
-    putchar('\n');
-    for (int k = 0; k < col_width + 2; ++k)
-      putchar('-');
-    for (int i = 0; i < count; ++i) {
-      putchar('+');
-      for (int k = 0; k < col_width + 2; ++k)
-        putchar('-');
-    }
-    putchar('\n');
-    for (int j = 0; j < count; ++j) {
-      printf(" %*s", col_width, warriors[j]->name.c_str());
-      for (int i = 0; i < count; ++i)
-        if (i == j)
-          printf(" | %*s", col_width, "");
-        else
-          printf(" | %5d / %5d%*s", wins_array[i * count + j], placements[i * count + j] * 2, col_width - 13, "");
-      putchar('\n');
-    }
-
-  }
-
-  else {
-
-    lib94::instruction core_background = {
-      .op = lib94::DAT,
-      .mod = lib94::F,
-      .amode = lib94::DIRECT,
-      .bmode = lib94::DIRECT,
-      .anumber = 0,
-      .bnumber = 0
-    };
-
-    int buffer[4] = { 0, 0, 0, 0 };
-    MPI_Send(buffer, 4, MPI_INT, 0, 1, MPI_COMM_WORLD);
-
-    while (true) {
-
-      MPI_Recv(buffer, 4, MPI_INT, 0, 0, MPI_COMM_WORLD, 0);
-      if (buffer[3] == 0)
-        break;
-
-      auto w1 = warriors[buffer[0]];
-      auto w2 = warriors[buffer[1]];
-      int start_offset = w1->instructions.size();
-      int w1_wins = 0;
-      int w2_wins = 0;
-
-      const lib94::warrior *const wlist[2] = { w1, w2 };
-      int offsets[2] = {};
-
-      for (int round = buffer[2]; round < buffer[3]; ++round) {
-
-        offsets[1] = start_offset + round;
-        lib94::clear_core(core_background);
-        lib94::init_round(wlist, 2, offsets, false);
-
-        for (int step = 0; step < steps_to_tie; ++step) {
-          auto lost = lib94::single_step<false>();
-          if (lost == w1) {
-            ++w2_wins;
-            break;
-          }
-          if (lost == w2) {
-            ++w1_wins;
-            break;
-          }
-        }
-
-      }
-
-      buffer[2] = w1_wins;
-      buffer[3] = w2_wins;
-      MPI_Send(buffer, 4, MPI_INT, 0, 1, MPI_COMM_WORLD);
-
-    }
-
-  }
-
-  MPI_Finalize();
-  return 0;
-
-}
diff --git a/tabulator/tabulator.cpp b/tabulator/tabulator.cpp
new file mode 100644
index 0000000..60cd680
--- /dev/null
+++ b/tabulator/tabulator.cpp
@@ -0,0 +1,159 @@
+#include <lib94/lib94.hpp>
+#include <iostream>
+#include <fstream>
+#include <sstream>
+#include <cstdio>
+#include <vector>
+
+const int steps_to_tie = 1000000;
+
+int error(std::string msg, int rank) {
+  if (rank == 0) {
+    std::cerr << msg << std::endl;
+    return 1;
+  }
+  return 0;
+}
+
+constexpr lib94::instruction background_instruction = {
+  .op = lib94::DAT, .mod = lib94::F,
+  .amode = lib94::DIRECT, .bmode = lib94::DIRECT,
+  .anumber = 0, .bnumber = 0
+};
+
+int main(int argc, char **argv) {
+
+  MPI_Init(&argc, &argv);
+
+  int rank;
+  int size;
+
+  MPI_Comm_rank(MPI_COMM_WORLD, &rank);
+  MPI_Comm_size(MPI_COMM_WORLD, &size);
+
+  lib94::seed_prng(time(0) + rank);
+
+  std::vector<std::string> filenames = {};
+
+  for (int i = 1; i < argc; ++i)
+    filenames.push_back(argv[i]);
+
+  if (filenames.size() == 0)
+    return error("no files specified.", rank);
+  if (filenames.size() == 1)
+    return error("only one file specified.", rank);
+
+  int count = filenames.size();
+  lib94::warrior **warriors = new lib94::warrior *[count];
+  for (int i = 0; i < count; ++i) {
+    try {
+      warriors[i] = lib94::compile_warrior_from_file(filenames[i]);
+    }
+    catch (const lib94::compiler_exception &ex) {
+      return error("could not compile " + filenames[i] + ": " + ex.message + " on line " + std::to_string(ex.source_line_number) + ".", rank);
+    }
+    if (!warriors[i])
+      return error("could not open " + filenames[i] + ".", rank);
+  }
+
+  int **wins_against = new int *[count];
+  for (int i = 0; i < count; ++i)
+    wins_against[i] = new int[count];
+
+  int **rounds_against = new int *[count];
+  for (int i = 0; i < count; ++i)
+    rounds_against[i] = new int[count];
+
+  for (int i = 0; i < count; ++i)
+    for (int j = i + 1; j < count; ++j) {
+
+      const lib94::warrior *w1 = warriors[i];
+      const lib94::warrior *w2 = warriors[j];
+
+      if (rank == 0)
+        std::cout << "running " << w1->name << " vs " << w2->name << "..." << std::endl;
+
+      int w1_wins, w2_wins, rounds;
+      lib94::tabulate_mpi<true>(background_instruction, w1, w2, steps_to_tie, w1_wins, w2_wins, rounds, MPI_COMM_WORLD);
+
+      if (rank == 0)
+        std::cout
+          << "results:\n  " << w1->name << ": " << w1_wins << "\n  "
+          << w2->name << ": " << w2_wins << "\n  ties: "
+          << (rounds - w1_wins - w2_wins) << "\n\n";
+
+      wins_against[i][j] = w1_wins;
+      wins_against[j][i] = w2_wins;
+
+      rounds_against[i][j] = rounds;
+      rounds_against[j][i] = rounds;
+
+    }
+
+  if (rank == 0) {
+
+    unsigned column_width = 0;
+    unsigned name_width = 0;
+    for (int i = 0; i < count; ++i)
+      if (warriors[i]->name.size() > name_width) {
+        name_width = warriors[i]->name.size();
+        column_width = name_width;
+      }
+
+    unsigned number_width = 0;
+    for (int i = 0; i < count; ++i)
+      for (int j = 0; j < count; ++j)
+        if (i != j) {
+          unsigned width = std::to_string(rounds_against[i][j]).size();
+          if (width > number_width)
+            number_width = width;
+          if (width * 2 + 3 > column_width)
+            column_width = width * 2 + 3;
+        }
+
+    for (int i = 0; i < count; ++i)
+      warriors[i]->name.insert(0, column_width - warriors[i]->name.size(), ' ');
+
+    std::string spaces, dashes;
+    spaces.resize(column_width, ' ');
+    dashes.resize(column_width, '-');
+
+    std::string first_spaces;
+    first_spaces.resize(name_width, ' ');
+    std::cout << ' ' << first_spaces;
+    for (int i = 0; i < count; ++i) {
+      std::cout << " | " << warriors[i]->name;
+    }
+
+    std::string first_dashes;
+    first_dashes.resize(name_width, '-');
+    std::cout << "\n-" << first_dashes;
+    for (int i = 0; i < count; ++i)
+      std::cout << "-+-" << dashes;
+    std::cout << "-\n";
+
+    for (int i = 0; i < count; ++i) {
+      std::string name = warriors[i]->name;
+      name.erase(0, column_width - name_width);
+      std::cout << ' ' << name;
+      for (int j = 0; j < count; ++j)
+        if (i == j)
+          std::cout << " | " << spaces;
+        else {
+          std::string num = std::to_string(wins_against[j][i]);
+          std::string den = std::to_string(rounds_against[j][i]);
+          num.insert(0, number_width - num.size(), ' ');
+          den.insert(0, number_width - den.size(), ' ');
+          std::string content = num + " / " + den;
+          content.insert(0, column_width - content.size(), ' ');
+          std::cout << " | " << content;
+        }
+      std::cout << '\n';
+    }
+
+  }
+
+  MPI_Finalize();
+  return 0;
+
+}