/rswiss

Ruby class to manage a Swiss-like Tournament

Primary LanguageRuby

# ----------------------------------------------------------------------
# "THE BEER-WARE LICENSE" (Revision 43):
# <pablo@propus.com.br> wrote this file and it's provided AS-IS, no
# warranties. As long as you retain this notice you can do whatever you
# want with this stuff. If we meet some day, and you think this stuff is
# worth it, you can buy me a beer in return."
# ----------------------------------------------------------------------

RSwiss
------

This is a simple implementation of Swiss Tournament (actually it's more like
a "King of The Hill" Tournament) focused on simplicity and no manual
interaction. The goal is to generate the simplest thing that could possible
work (and not replace FIDE rules - if you're thinking about chess - or any
other software dealing with the same issue).


Requirements
------------

Sequel: tested with 3.9.0. Probably will work with earlier versions.


How it works
------------

We're using a database under Sequel to store our tournaments, players and
matches data. The model RSwiss::Tournament is the central piece here. A new
instance of that is generated passing the number of players. An array of
player ids have to be passed with #inject_players. With that in place, new
matches are generated automatically for each round and are checked-out with
the #checkout_match method. Since every round depends on the results of
previous matches, checked-out matches have to be commited back with the
#commit_match method (just saving the object RSwiss::Match also will work).

This goes on until RSwiss::Tournament raises an RSwiss::EndOfTournament
exception, which marks... well... the end of the tournament (at any given
point #ended? can be called if you want to prevent exceptions being raised).

The result table can be obtained with #players(:score) or with
#players(:criteria). You can set the criteria with #criteria=. The default
#criteria can be accessed by criteria, and equals to the following array:

[ :score, :buchholz_score, :neustadtl_score, :c_score, :opp_c_score, :wins ]

Where:
   * :score is the conventional score (1.0 for win, 0.5 for draw and 0.0 for
     defeat;
   * :buchholz_score is the one calculated by Median Buchholz method
     (sometimes called Harkness), which is the sum of the players opponents
     scores, discarding one or two lowest and highest;
   * :neustadtl_score is the one calculated by adding the scores of every
     opponent the player beats and half of the score of every opponent the
     player draws. Also called Sonneborn-Berger;
   * :c_score is the cumulative score, which sums the running score for each
     round;
   * :opp_c_score is the sum of the cumulative score of the opponents; and
   * :wins is the number of wins.


Ok... But how it works under the hood?
--------------------------------------

It's quite simple, actually. First round order is decided at random. After
that, every round is generated by sorting players by the score and descending
the sorted array of players from the highest to the lowest, generating one
match for each player in each round against those with similar scores.

If we have an odd number of players, one is always left out. This is called
"bye", and can only happen to a player once. It sums 1.0 to the score, and
counts as a match, but adds no opponent (so it gives in the score, but takes
in every other criteria, including number of wins). The "bye" is given each
round to the last player that has not received one yet.

By default, RSwiss generates just the number of rounds that ensure a fairly
evaluated tournament (the binary logarithm of the number of players). You can
use #additional_rounds= to set the number of additional rounds to be generated
(just in case you need to "overevaluate" your tournament). Use this feature
with care...

Traditionally, there's a restriction on repeating matches. Unfortunatelly, this
can generate deadlocks (where we simply cannot generate a round without
repeated matches given the previous results). This usually happens in as low as
0.3% of the tournaments, but, according to the number of players, can happen in
up to 2%!

When this happens, the only way to break out of it is to throw everything
away and start over (not a good thing). Well... there's, of course, less
destructive measures, but it requires bending the rules a little.

The first way RSwiss uses to try to break the deadlock is to discarding some
of the last few unexposed matches, shuffling their players and trying to build
different matches. But even that has limitations.

Also, when creating a new tournament, you can also use #allow_repeat= as a
bloolean flag. When set to "true", it will allow one of the previous matches
to be repeated as a way to break the deadlock. This can only happen once for
each match. In my experience, when it happens is also usually happens once in
the whole tournament, so it's not a big deal.

Other solutions have been proposed, such as giving a "bye" to the involved
players, or generating a really unbalanced match (with players with too
different scores), but I think that the implemented solution is the one with
less "side-effects" (distributing "byes" has effects on criterias and
unbalanced matches can generate less fair results).


Tests
-----

This is, mostly, a proof-of-concept code, so there are "tests" (not Unit Tests)
that can be run against it. The script "test.rb" accepts the number of players
as the first argument and begin recurrently testing tournaments with that
number of players, recording the "problem rate" (tournaments that have reached
the deadlock); the presence of any other argument will render tournaments with
"allow repeat" flag on (in this case, the number of repeated matches will be
recorded).


XML-RPC
-------

The "xml-sswiss.rb" is a simple XML-RPC server that accepts the creation of
tournaments and all the interface to exchange matches with a XML-RPC client.
You can use it by running "simple_serve.rb" (for the native XMLRPC server that
ships with Ruby) or as a Rack app - try running "rack_serve.ru". This also
have a test in "xml-test.rb", that actually does the same "test.rb" does, but
acting as a XML-RPC client to "xml-sswiss.rb" server.  If you're using the
Rack version with "xml-test.rb", run it with "-p 9090" (or change the source
for "xml-test.rb").