diff --git a/project.properties b/project.properties index 0ce6f35..7b97afb 100644 --- a/project.properties +++ b/project.properties @@ -1,6 +1,6 @@ -#Sun Jul 04 00:39:50 CDT 2010 +#Mon Jul 05 23:10:39 CDT 2010 app.version=0.3.0 +build.number=47 src.dir=src -build.number=4 build.dir=build app.name=team-maker diff --git a/src/com/jdbernard/teammaker/OddsCalculator.groovy b/src/com/jdbernard/teammaker/OddsCalculator.groovy new file mode 100644 index 0000000..84bb4f5 --- /dev/null +++ b/src/com/jdbernard/teammaker/OddsCalculator.groovy @@ -0,0 +1,86 @@ +package com.jdbernard.teammaker + +public class OddsCalculator { + + Map odds = [:] + + public void recalculate(List players, int spotsOpen, PlayerChooser chooser) { + + // reset computed player odds + odds = [:] + + /* this list will contain each possible selection path with + * the odds at each node. It is a list of lists of lists:: + * + * oddsList: [ + * path: [ + * node: [player, odds] + * node: [player, odds] + * ] + * path: [ + * node: [player, odds] + * node: [player, odds] + * ] + * ] + */ + + def oddsList = buildOdds([], players, spotsOpen) + + /* The oddsMap is a condensed version of the oddsList, calculating + * the overall probability of each path:: + * + * oddsMap: [ + * [player, player]: pathOdds, + * [player, player]: pathOdds + * ] + */ + def oddsMap = [:] + + // calculate the overall odds for each path + oddsList.each { path -> + def pathPlayers = [] + float pathOdds = 1f + + // the overall odds of the path is the product of the odds for + // each node in the path + path.each { node -> + pathPlayers << node[0] // player + pathOdds *= node[1] // node odds + } + + oddsMap[(pathPlayers)] = pathOdds + } + + // now we calculate the overall odds for each player. The odds for a + // player is the sum of the odds of each path that contains that player + // (since that means he was picked for one of the nodes, ie., open + // spots) + players.each { player -> + float playerOdds = 0f + + // check each path in the map + oddsMap.each { pathPlayers, pathOdds -> + // if we are in this path, add that probability to our own + if (pathPlayers.contains(player)) + playerOdds += pathOdds + } + + odds[(player)] = playerOdds + } + } + + protected List buildOdds(List chosenPath, List players, int spotsOpen) { + def oddsList = [] + + if (spotsOpen == 0) return [chosenPath] + + players.each { player -> + def odds = chooser.getOdds(players, player) + def newPath = chosenPath.clone() + newPath << [player, odds] + oddsList.addAll(buildOdds(newPath, players - [player], spotsOpen - 1)) + } + + return oddsList + } +} diff --git a/src/com/jdbernard/teammaker/PlayerChooser.groovy b/src/com/jdbernard/teammaker/PlayerChooser.groovy index 1875b2b..b040be9 100644 --- a/src/com/jdbernard/teammaker/PlayerChooser.groovy +++ b/src/com/jdbernard/teammaker/PlayerChooser.groovy @@ -1,14 +1,13 @@ package com.jdbernard.teammaker public abstract class PlayerChooser { + + protected static final Random rand = new Random(System.currentTimeMillis()) + public abstract Player choose (def players) - public abstract float calculateOdds(def players, def player) + public abstract float getOdds(def players, Player player) - public final static Random = new Random(System.currentTimeMillis()) - - public static final Player chooseRandomly(def choices) { + protected Player chooseRandomly(List choices) { choices.sort { rand.nextInt() } - return choices[0] } - } diff --git a/src/com/jdbernard/teammaker/PlayerListCellRenderer.groovy b/src/com/jdbernard/teammaker/PlayerListCellRenderer.groovy index c1914d6..75b65e8 100644 --- a/src/com/jdbernard/teammaker/PlayerListCellRenderer.groovy +++ b/src/com/jdbernard/teammaker/PlayerListCellRenderer.groovy @@ -1,33 +1,35 @@ package com.jdbernard.teammaker +import java.awt.BorderLayout import java.awt.Color import java.awt.Component -import javax.swing.EmptyBorder import javax.swing.JLabel import javax.swing.JList import javax.swing.JPanel -import javax.swing.LineBorder import javax.swing.ListCellRenderer +import javax.swing.BorderFactory public class PlayerListCellRenderer extends JPanel implements ListCellRenderer { boolean showStats boolean colored - private PlayerChooser chooser + private TeamMaker teamMaker + private JLabel nameLabel private JLabel statsLabel - private static def linedBorder = new LineBorder(Color.BLACK) - private static def emptyBorder = new EmptyBorder(1) + + private static def linedBorder = BorderFactory.createLineBorder(Color.BLACK) + private static def emptyBorder = BorderFactory.createEmptyBorder() private def colors = [Color.getHSBColor(0.32f, 1f, 1f), Color.getHSBColor(0.24f,1f,1f), Color.getHSBColor(0.16f, 1f, 1f), Color.getHSBColor(0.08f, 1f, 1f), Color.getHSBColor(0.0f, 1f, 1f)] - public PlayerListCellRenderer(Map params) { + public PlayerListCellRenderer(Map params, def tm) { + teamMaker = tm showStats = params.showStats ?: false colored = params.colored ?: false - chooser = params.chooser ?: WeightedChooser.getInstance() setLayout(new BorderLayout()) setOpaque(colored) @@ -39,7 +41,7 @@ public class PlayerListCellRenderer extends JPanel implements ListCellRenderer { if (showStats) { statsLabel = new JLabel() - statsLabel.horizontalAlignment = JLabel.TRAILING, + statsLabel.horizontalAlignment = JLabel.TRAILING statsLabel.opaque = false add(statsLabel, BorderLayout.EAST) } @@ -55,8 +57,11 @@ public class PlayerListCellRenderer extends JPanel implements ListCellRenderer { if (isSelected) setBorder(linedBorder) else setBorder(emptyBorder) - if (showStats) - statsLabel.text = value.gamesSat + if (showStats) { + int odds = Math.round(teamMaker.playerChooser.calculateOdds( + list.model, teamMaker.spotsOpen, value) * 100) + statsLabel.text = "${value.gamesSat} (${odds}%)" + } if (colored) { def c = colors[Math.min(4, value.gamesSat)] @@ -65,4 +70,5 @@ public class PlayerListCellRenderer extends JPanel implements ListCellRenderer { return this } + } diff --git a/src/com/jdbernard/teammaker/RandomChooser.groovy b/src/com/jdbernard/teammaker/RandomChooser.groovy deleted file mode 100644 index 0b5e8ad..0000000 --- a/src/com/jdbernard/teammaker/RandomChooser.groovy +++ /dev/null @@ -1,14 +0,0 @@ -package com.jdbernard.teammaker - -public class RandomChooser extends PlayerChooser { - - public Player choose(def players) { - def choices = [] - players.each { choices << it } - chooseRandomly(choices) - } - - public float calculateOdds(def players, def player) { - return 1f / (float) players - } -} diff --git a/src/com/jdbernard/teammaker/RandomPlayerChooser.groovy b/src/com/jdbernard/teammaker/RandomPlayerChooser.groovy new file mode 100644 index 0000000..ba40877 --- /dev/null +++ b/src/com/jdbernard/teammaker/RandomPlayerChooser.groovy @@ -0,0 +1,14 @@ +package com.jdbernard.teammaker + +public class RandomPlayerChooser extends AbstractPlayerChooser { + + public Player choose(def players) { + def choices = [] + players.each { choices << it } + return chooseRandomly(choices) + } + + public float getOdds(def players, Player player) { + return 1f / (float) players.size() + } +} diff --git a/src/com/jdbernard/teammaker/TeamMaker.groovy b/src/com/jdbernard/teammaker/TeamMaker.groovy index 0290a05..540c3d4 100644 --- a/src/com/jdbernard/teammaker/TeamMaker.groovy +++ b/src/com/jdbernard/teammaker/TeamMaker.groovy @@ -14,7 +14,7 @@ public class TeamMaker { /* ======== MODEL ======== */ public static def swing = new SwingBuilder() - public static final String version = "0.2" + public static final String version = "0.3" def frame def team1List @@ -24,10 +24,13 @@ public class TeamMaker { def newGameButton def sittingList - PlayerChooser chooser = new RandomChooser() + AbstractPlayerChooser playerChooser = GameWeightedChooser.getInstance() - @Bindable boolean inGame = false - @Bindable int teamSize = 4 + class Observables { + @Bindable boolean inGame = false + @Bindable int teamSize = 4 + } + def model = new Observables() @@ -77,8 +80,8 @@ public class TeamMaker { label('Team B', constraints: gbc(gridx: 1, gridy: 0, fill: GBC.BOTH, insets: [5, 5, 0, 5])) - def teamListRenderer = new PlayerListCellRenderer(showStats: false, - colored: false) + def teamListRenderer = new PlayerListCellRenderer(this, + showStats: false, colored: false) team1List = list(cellRenderer: teamListRenderer, constraints: gbc(gridx: 0, gridy: 1, fill: GBC.BOTH, @@ -88,20 +91,8 @@ public class TeamMaker { team1WinsButton = button('Team A Wins', constraints: gbc(gridx: 0, gridy: 2, anchor: GBC.CENTER, insets: [5, 5, 5, 0]), - enabled: false, - actionPerformed: { - sittingList.model.each { it.gamesSat++ } - team1List.model.each { player -> - player.gamesSat = 0 - sittingList.model.addElement(player) - } - team1List.model.clear() - team1List.repaint() - sittingList.repaint() - inGame = false - team1WinsButton.enabled = false - team2WinsButton.enabled = false - }) + enabled: bind { model.inGame }, + actionPerformed: { declareWinner(team1List)}) team2List = list(cellRenderer: teamListRenderer, constraints: gbc(gridx: 1, gridy: 1, fill: GBC.BOTH, @@ -111,20 +102,8 @@ public class TeamMaker { team2WinsButton = button('Team B Wins', constraints: gbc(gridx: 1, gridy: 2, anchor: GBC.CENTER, insets: [5, 5, 5, 5]), - enabled: false, - actionPerformed: { - sittingList.model.each { it.gamesSat++ } - team2List.model.each { player -> - player.gamesSat = 0 - sittingList.model.addElement(player) - } - team2List.model.clear() - team2List.repaint() - sittingList.repaint() - inGame = false - team1WinsButton.enabled = false - team2WinsButton.enabled = false - }) + enabled: bind { model.inGame }, + actionPerformed: { declareWinner(team2List)} ) } @@ -133,25 +112,18 @@ public class TeamMaker { border: titledBorder(title: 'Sitting Players')) { sittingList = list(cellRenderer: new PlayerListCellRenderer( - showStats: true, colored: true), + this, showStats: true, colored: true), model: new SortedPlayerModel()) } button('Next Game', constraints: gbc(gridx: 0, gridy: 1, anchor: GBC.CENTER, insets: [5, 5, 5, 0]), + enabled: bind { !model.inGame}, actionPerformed: { newGame() }) button('Add Player', constraints: gbc(gridx: 1, gridy: 1, anchor: GBC.CENTER, insets: [5, 5, 5, 0]), - actionPerformed: { - def name = JOptionPane.showInputDialog(frame, - "Enter the new player's name: ", "New Player...", - JOptionPane.QUESTION_MESSAGE) - def player = new Player() - player.name = name - player.gamesSat = 0 - sittingList.model.addElement(player) - }) + actionPerformed: { addPlayer() }) newGameButton = button('Delete Player', constraints: gbc(gridx: 2, gridy: 1, anchor: GBC.CENTER, @@ -178,41 +150,58 @@ public class TeamMaker { init() } - private void newGame() { - if (team1List.model.size() < teamSize) - populate(team1List, sittingList, teamSize) - - if (team2List.model.size() < teamSize) - populate(team2List, sittingList, teamSize) - - team1WinsButton.enabled = true - team2WinsButton.enabled = true - newGameButton.enabled = false + public int getSpotsOpen() { + def spots = (model.teamSize - team1List.model.size()) + + (model.teamSize - team2List.model.size()) + return (spots == 0 ? model.teamSize : spots) } - private static void populate(def teamList, def sittingList, int teamSize) { - while (teamList.model.size() < teamSize) { - def player = choosePlayer(sittingList) + private void addPlayer() { + def name = JOptionPane.showInputDialog(frame, + "Enter the new player's name: ", "New Player...", + JOptionPane.QUESTION_MESSAGE) + + if (!name) return + + def player = new Player() + player.name = name + player.gamesSat = 0 + sittingList.model.addElement(player) + } + + private void newGame() { + if (team1List.model.size() < model.teamSize) + populate(team1List) + + if (team2List.model.size() < model.teamSize) + populate(team2List) + + model.inGame = true + //team1WinsButton.enabled = true + //team2WinsButton.enabled = true + //newGameButton.enabled = false + } + + private void declareWinner(def teamList) { + sittingList.model.each { it.gamesSat++ } + teamList.model.each { player -> + player.gamesSat = 0 + sittingList.model.addElement(player) + } + teamList.model.clear() + teamList.repaint() + sittingList.repaint() + model.inGame = false + //team1WinsButton.enabled = false + //team2WinsButton.enabled = false + } + + private void populate(def teamList) { + while (teamList.model.size() < model.teamSize) { + def player = playerChooser.choose(sittingList.model) teamList.model.addElement(player) sittingList.model.removeElement(player) } } - private static Player choosePlayer(def list) { - def choices = [] - list.model.each { player -> - player.gamesSat.times { choices << player } - } - - // if no players have sat at least one game, add all once - if (choices.size() == 0) list.model.each { player -> choices << player } - - choices.sort { rand.nextInt() } - println choices - return choices[0] - } - - private float calculatePercentage(Player p, def sittingList) { - - } } diff --git a/src/com/jdbernard/teammaker/WeightedChooser.groovy b/src/com/jdbernard/teammaker/WeightedChooser.groovy new file mode 100644 index 0000000..15f4fbd --- /dev/null +++ b/src/com/jdbernard/teammaker/WeightedChooser.groovy @@ -0,0 +1,28 @@ +package com.jdbernard.teammaker + +public abstract class WeightedChooser extends AbstractPlayerChooser { + + int hardLimit = 5 + + public Player choose(def players) { + def choices = [] + + // find sitting threshold (longest sat - hard limit) + int threshold = (player.max { it.gamesSat }) - hardLimit + + // add players, ignoring those past the hard limit + players.each { player -> + if (player.gamesSat >= threshold) { + choices << player + player.gamesSat.times { choices << player } + } + } + return chooseRandomly(choices) + } + + public float getOdds(def players, Player player) { + def playerGames = player.gamesSat + 1 + def totalGames = players.sum { it.gamesSat + 1 } + return (float) playerGames / (float) totalGames + } +}