├── .gitignore ├── .npmignore ├── .github └── workflows │ └── ci.yml ├── package.json ├── LICENSE ├── robin.js ├── README.md └── test └── robin.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | robin-cov.js 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | examples 3 | .gitignore 4 | .travis.yml 5 | .npmignore 6 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | pull_request: 5 | 6 | jobs: 7 | npm-test: 8 | runs-on: ubuntu-16.04 9 | strategy: 10 | matrix: 11 | node: [ '14' ] 12 | name: Node ${{ matrix.node }} 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v2 16 | with: 17 | node-version: ${{ matrix.node }} 18 | - run: npm install 19 | - run: npm test 20 | - run: npm install coveralls 21 | - run: npm run coverage > lcov.info 22 | - name: Coveralls Finished 23 | uses: coverallsapp/github-action@master 24 | with: 25 | github-token: ${{ secrets.GITHUB_TOKEN }} 26 | path-to-lcov: ./lcov.info 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roundrobin", 3 | "description": "A round-robin scheduler used in different tournaments", 4 | "author": "Eirik Albrigtsen ", 5 | "version": "2.0.0", 6 | "repository": { 7 | "type": "git", 8 | "url": "clux/roundrobin" 9 | }, 10 | "keywords": [ 11 | "round", 12 | "robin", 13 | "tournament" 14 | ], 15 | "main": "robin.js", 16 | "scripts": { 17 | "test": "bndg test/*.js", 18 | "precoverage": "istanbul cover bndg test/*.test.js", 19 | "coverage": "cat coverage/lcov.info && rm -rf coverage" 20 | }, 21 | "devDependencies": { 22 | "bandage": "^0.4.0", 23 | "interlude": "~1.0.2", 24 | "istanbul": "^0.4.1" 25 | }, 26 | "bugs": { 27 | "url": "http://github.com/clux/roundrobin/issues" 28 | }, 29 | "license": "MIT" 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2013 Eirik Albrigtsen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /robin.js: -------------------------------------------------------------------------------- 1 | const DUMMY = -1; 2 | // returns an array of round representations (array of player pairs). 3 | // http://en.wikipedia.org/wiki/Round-robin_tournament#Scheduling_algorithm 4 | module.exports = function (n, ps) { // n = num players 5 | const rs = []; // rs = round array 6 | if (!ps) { 7 | ps = []; 8 | for (let k = 1; k <= n; k += 1) { 9 | ps.push(k); 10 | } 11 | } else { 12 | ps = ps.slice(); 13 | } 14 | 15 | if (n % 2 === 1) { 16 | ps.push(DUMMY); // so we can match algorithm for even numbers 17 | n += 1; 18 | } 19 | for (let j = 0; j < n - 1; j += 1) { 20 | rs[j] = []; // create inner match array for round j 21 | for (let i = 0; i < n / 2; i += 1) { 22 | const o = n - 1 - i; 23 | if (ps[i] !== DUMMY && ps[o] !== DUMMY) { 24 | // flip orders to ensure everyone gets roughly n/2 home matches 25 | const isHome = i === 0 && j % 2 === 1; 26 | // insert pair as a match - [ away, home ] 27 | rs[j].push([isHome ? ps[o] : ps[i], isHome ? ps[i] : ps[o]]); 28 | } 29 | } 30 | ps.splice(1, 0, ps.pop()); // permutate for next round 31 | } 32 | return rs; 33 | }; 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Round Robin 2 | [![npm status](http://img.shields.io/npm/v/roundrobin.svg)](https://www.npmjs.org/package/roundrobin) 3 | [![build status](https://github.com/clux/roundrobin/actions/workflows/ci.yml/badge.svg)](https://github.com/clux/roundrobin/actions/workflows/ci.yml) 4 | [![dependency status](https://david-dm.org/clux/roundrobin.svg)](https://david-dm.org/clux/roundrobin) 5 | [![coverage status](http://img.shields.io/coveralls/clux/roundrobin.svg)](https://coveralls.io/r/clux/roundrobin) 6 | 7 | A simple round robin tournament match scheduler using the standard [scheduling algorithm](http://en.wikipedia.org/wiki/Round-robin_tournament#Scheduling_algorithm). 8 | 9 | ## Usage 10 | Simply give the number of players (with an optional players array), and it will spit out the array of rounds necessary: 11 | 12 | ```js 13 | var robin = require('roundrobin'); 14 | robin(6); 15 | [ [ [ 1, 6 ], [ 2, 5 ], [ 3, 4 ] ], 16 | [ [ 5, 1 ], [ 6, 4 ], [ 2, 3 ] ], 17 | [ [ 1, 4 ], [ 5, 3 ], [ 6, 2 ] ], 18 | [ [ 3, 1 ], [ 4, 2 ], [ 5, 6 ] ], 19 | [ [ 1, 2 ], [ 3, 6 ], [ 4, 5 ] ] ] 20 | 21 | // or with names supplied 22 | robin(6, ['clux', 'lockjaw', 'pibbz', 'xeno', 'e114', 'eclipse']); 23 | [ [ [ 'clux', 'eclipse' ], [ 'lockjaw', 'e114' ], [ 'pibbz', 'xeno' ] ], 24 | [ [ 'e114', 'clux' ], [ 'eclipse', 'xeno' ], [ 'lockjaw', 'pibbz' ] ], 25 | [ [ 'clux', 'xeno' ], [ 'e114', 'pibbz' ], [ 'eclipse', 'lockjaw' ] ], 26 | [ [ 'pibbz', 'clux', ], [ 'xeno', 'lockjaw' ], [ 'e114', 'eclipse' ] ], 27 | [ [ 'clux', 'lockjaw' ], [ 'pibbz', 'eclipse' ], [ 'xeno', 'e114' ] ] ] 28 | ``` 29 | 30 | ### Home/Away Matches 31 | In version `2.0.0` or greater, the outputted order of the match arrays denote which player is "home" or "away": 32 | ```js 33 | [ 'away', 'home' ] // index 0 is away and index 1 is home 34 | ``` 35 | This can be used to indicate home/away in sports, white/black in chess, etc. 36 | 37 | ## Installation 38 | Install from npm: 39 | 40 | ```bash 41 | $ npm install roundrobin 42 | ``` 43 | 44 | ## License 45 | MIT-Licensed. See LICENSE file for details. 46 | -------------------------------------------------------------------------------- /test/robin.test.js: -------------------------------------------------------------------------------- 1 | var $ = require('interlude') 2 | , test = require('bandage') 3 | , robin = require('..'); 4 | 5 | test('robin', function *(t) { 6 | $.range(20).forEach(function (n) { 7 | var rs = robin(n); 8 | var expected = $.odd(n) ? n : n-1; 9 | t.eq(expected, rs.length, 'correct number of rounds'); 10 | 11 | var pMaps = []; 12 | $.range(n).forEach(function (p) { 13 | pMaps[p] = []; 14 | }); 15 | 16 | rs.forEach(function (rnd, r) { 17 | t.eq(rnd.length, Math.floor(n/2), 'number of matches in round '+ (r+1)); 18 | 19 | var plrs = $.flatten(rnd); 20 | t.eq(plrs, $.nub(plrs), 'players listed only once per round'); 21 | 22 | // keep track of who everyone is playing as well 23 | rnd.forEach(function (p) { 24 | var a = p[0] 25 | , b = p[1]; 26 | pMaps[a].push(b); 27 | pMaps[b].push(a); 28 | }); 29 | }); 30 | 31 | Object.keys(pMaps).forEach(function (p) { 32 | var val = pMaps[p].sort($.compare()); 33 | var exp = $.delete($.range(n), Number(p)); 34 | // if this true, then each play all exactly once by previous test 35 | t.eq(val, exp, 'player ' + p + ' plays every enemy'); 36 | }); 37 | }); 38 | }); 39 | 40 | test('names', function *(t) { 41 | var ps = ['clux', 'lockjaw', 'pibbz', 'xeno', 'e114', 'eclipse']; 42 | var pscopy = ps.slice(); 43 | t.eq(robin(6, ps), [ 44 | [ [ 'clux', 'eclipse' ], [ 'lockjaw', 'e114' ], [ 'pibbz', 'xeno' ] ], 45 | [ [ 'e114', 'clux' ], [ 'eclipse', 'xeno' ], [ 'lockjaw', 'pibbz' ] ], 46 | [ [ 'clux', 'xeno' ], [ 'e114', 'pibbz' ], [ 'eclipse', 'lockjaw' ] ], 47 | [ [ 'pibbz', 'clux', ], [ 'xeno', 'lockjaw' ], [ 'e114', 'eclipse' ] ], 48 | [ [ 'clux', 'lockjaw' ], [ 'pibbz', 'eclipse' ], [ 'xeno', 'e114' ] ] ], 49 | 'expected even output' 50 | ); 51 | t.eq(ps, pscopy, 'have not modified even input'); 52 | 53 | ps.pop(); 54 | pscopy = ps.slice(); 55 | t.eq(robin(5, ps), [ 56 | [ [ 'lockjaw', 'e114' ], [ 'pibbz', 'xeno' ] ], 57 | [ [ 'e114', 'clux' ], [ 'lockjaw', 'pibbz' ] ], 58 | [ [ 'clux', 'xeno' ], [ 'e114', 'pibbz' ] ], 59 | [ [ 'pibbz', 'clux' ], [ 'xeno', 'lockjaw' ] ], 60 | [ [ 'clux', 'lockjaw' ], [ 'xeno', 'e114' ] ] ], 61 | 'expected odd output' 62 | ); 63 | t.eq(ps, pscopy, 'have not modified odd input'); 64 | }); 65 | 66 | test('home-away', function *(t) { 67 | function hasCorrectHomeAwayOutput(n) { 68 | const awayPlayers = []; 69 | const homePlayers = []; 70 | 71 | for (const round of robin(n)) { 72 | for (const match of round) { 73 | awayPlayers.push(match[0]); 74 | homePlayers.push(match[1]); 75 | } 76 | } 77 | 78 | for (let i = 1; i <= n; i += 1) { 79 | const isOddPlayer = i % 2 === 1; 80 | const fairAmount = (n - 1) / 2; 81 | const [ roundedUp, roundedDown ] = [ Math.ceil(fairAmount), Math.floor(fairAmount) ]; 82 | const [ awayAmount, homeAmount ] = isOddPlayer ? [ roundedUp, roundedDown ] : [ roundedDown, roundedUp ]; 83 | t.eq( 84 | awayPlayers.filter(player => player === i).length, 85 | awayAmount, 86 | `player ${i} of ${n} plays the correct amount of away matches (${awayAmount})` 87 | ); 88 | t.eq( 89 | homePlayers.filter(player => player === i).length, 90 | homeAmount, 91 | `player ${i} of ${n} plays the correct amount of home matches (${homeAmount})` 92 | ); 93 | } 94 | } 95 | 96 | hasCorrectHomeAwayOutput(9); 97 | hasCorrectHomeAwayOutput(10); 98 | }); 99 | --------------------------------------------------------------------------------