big tournament refactoring

This commit is contained in:
Thibault Duplessis 2012-09-15 14:12:49 +02:00
parent 99610be216
commit f2ba016161
22 changed files with 275 additions and 154 deletions

View file

@ -41,7 +41,7 @@ object Tournament extends LilaController {
private def showCreated(tour: Created)(implicit ctx: Context) = for {
roomHtml messenger render tour
users userRepo byIds tour.data.users
users userRepo byIds tour.userIds
} yield html.tournament.show.created(
tour = tour,
roomHtml = Html(roomHtml),
@ -69,12 +69,10 @@ object Tournament extends LilaController {
def join(id: String) = Auth { implicit ctx
implicit me
IOptionIORedirect(repo createdById id) { tour
api.join(tour, me) map { result
result.fold(
err { println(err.shows); routes.Tournament.home() },
_ routes.Tournament.show(tour.id)
)
}
api.join(tour, me).fold(
err putStrLn(err.shows) map (_ routes.Tournament.home()),
res res map (_ routes.Tournament.show(tour.id))
)
}
}
@ -86,15 +84,33 @@ object Tournament extends LilaController {
}
def reload(id: String) = Open { implicit ctx
IOptionIOk(repo startedById id) { tour
gameRepo.recentTournamentGames(tour.id, 4) map { games
val pairings = html.tournament.pairings(tour)
val inner = html.tournament.show.startedInner(tour, games)
html.tournament.show.inner(pairings.some)(inner)
}
IOptionIOk(repo byId id) {
case tour: Created reloadCreated(tour)
case tour: Started reloadStarted(tour)
case tour: Finished reloadFinished(tour)
}
}
private def reloadCreated(tour: Created)(implicit ctx: Context) =
userRepo byIds tour.userIds map { users
val inner = html.tournament.show.createdInner(tour, users)
html.tournament.show.inner(none)(inner)
}
private def reloadStarted(tour: Started)(implicit ctx: Context) =
gameRepo.recentTournamentGames(tour.id, 4) map { games
val pairings = html.tournament.pairings(tour)
val inner = html.tournament.show.startedInner(tour, games)
html.tournament.show.inner(pairings.some)(inner)
}
private def reloadFinished(tour: Finished)(implicit ctx: Context) =
gameRepo.recentTournamentGames(tour.id, 4) map { games
val pairings = html.tournament.pairings(tour)
val inner = html.tournament.show.finishedInner(tour, games)
html.tournament.show.inner(pairings.some)(inner)
}
def form = Auth { implicit ctx
me
Ok(html.tournament.form(forms.create))

View file

@ -17,7 +17,7 @@ sealed abstract class Context(val req: RequestHeader, val me: Option[User]) {
def isGranted(permission: Permission): Boolean =
me.fold(Granter(permission), false)
def is(user: User) = me == Some(user)
def is(user: User): Boolean = me == Some(user)
def userId = me map (_.id)
}

View file

@ -8,13 +8,18 @@ import play.api.data.validation.Constraints._
final class DataForm {
import lila.core.Form._
import Tournament._
val create = Form(mapping(
"minutes" -> numberIn(Tournament.minuteChoices),
"minUsers" -> numberIn(Tournament.minUserChoices)
"clockTime" -> numberIn(clockTimeChoices),
"clockIncrement" -> numberIn(clockIncrementChoices),
"minutes" -> numberIn(minuteChoices),
"minPlayers" -> numberIn(minPlayerChoices)
)(TournamentSetup.apply)(TournamentSetup.unapply)) fill TournamentSetup()
}
case class TournamentSetup(
clockTime: Int = Tournament.clockTimeDefault,
clockIncrement: Int = Tournament.clockIncrementDefault,
minutes: Int = Tournament.minuteDefault,
minUsers: Int = Tournament.minUserDefault)
minPlayers: Int = Tournament.minPlayerDefault)

View file

@ -64,7 +64,7 @@ final class Organizer(
def startPairing(tour: Started) {
(hubMaster ? GetTournamentUsernames(tour.id)).mapTo[Iterable[String]] onSuccess {
case usernames
(tour.users intersect usernames.toList) |> { users
(tour.activeUserIds intersect usernames.toList) |> { users
Pairing.createNewPairings(users, tour.pairings).toNel foreach { pairings
api.makePairings(tour, pairings).unsafePerformIO
}

View file

@ -3,25 +3,41 @@ package tournament
import com.mongodb.casbah.query.Imports._
import user.User
case class Player(
id: String,
username: String,
nbWin: Int,
nbLoss: Int,
winStreak: Int,
score: Int) {
elo: Int,
withdraw: Boolean = false,
nbWin: Int = 0,
nbLoss: Int = 0,
winStreak: Int = 0,
score: Int = 0) {
def active = !withdraw
def is(user: User) = id == user.id
def doWithdraw = copy(withdraw = true)
}
object Standing {
object Player {
def of(tour: Tournament): Standing = tour.users.map { user
def apply(user: User): Player = new Player(
id = user.id,
username = user.username,
elo = user.elo)
def refresh(tour: Tournament): Players = tour.players.map { player
tour.pairings
.filter(_ contains user)
.foldLeft(Builder(user))(_ + _.winner)
.player
.filter(_ contains player.id)
.foldLeft(Builder(player))(_ + _.winner)
.toPlayer
} sortBy (p -p.score)
private case class Builder(
username: String,
player: Player,
nbWin: Int = 0,
nbLoss: Int = 0,
score: Int = 0,
@ -31,7 +47,7 @@ object Standing {
def +(winner: Option[String]) = {
val (win, loss) = winner.fold(
w if (w == username) true -> false else false -> true,
w if (w == player.id) true -> false else false -> true,
false -> false)
val newWinSeq = if (win) prevWin.fold(winSeq + 1, 1) else 0
val points = win.fold(newWinSeq * 2, loss.fold(0, 1))
@ -44,6 +60,10 @@ object Standing {
prevWin = win)
}
def player = Player(username, nbWin, nbLoss, bestWinSeq, score)
def toPlayer = player.copy(
nbWin = nbWin,
nbLoss = nbLoss,
winStreak = bestWinSeq,
score = score)
}
}

View file

@ -29,7 +29,7 @@ final class Socket(
implicit private val timeout = Timeout(timeoutDuration)
def reload(tournamentId: String) = io {
hubMaster ! Forward(tournamentId, Reload)
hubMaster ! Forward(tournamentId, Reload).pp
}
def notifyPairing(game: DbGame) = io {

View file

@ -12,11 +12,12 @@ import user.User
case class Data(
name: String,
clock: TournamentClock,
minutes: Int,
minUsers: Int,
minPlayers: Int,
createdAt: DateTime,
createdBy: String,
users: List[String])
players: Players)
sealed trait Tournament {
@ -30,19 +31,21 @@ sealed trait Tournament {
def name = data.name
def nameT = name + " tournament"
def clock = data.clock
def minutes = data.minutes
lazy val duration = new Duration(minutes * 60 * 1000)
def users = data.users
def nbUsers = users.size
def minUsers = data.minUsers
def contains(username: String): Boolean = users contains username
def players = data.players
def userIds = players map (_.id)
def activeUserIds = players filter (_.active) map (_.id)
def nbPlayers = players.size
def minPlayers = data.minPlayers
def playerRatio = "%d/%d".format(nbPlayers, minPlayers)
def contains(userId: String): Boolean = userIds contains userId
def contains(user: User): Boolean = contains(user.id)
def contains(user: Option[User]): Boolean =
user.fold(u contains(u.id), false)
def missingUsers = minUsers - users.size
def contains(user: Option[User]): Boolean = user.fold(u contains(u.id), false)
def missingPlayers = minPlayers - players.size
def showClock = "2+0"
def createdBy = data.createdBy
def createdAt = data.createdAt
}
@ -51,10 +54,9 @@ sealed trait StartedOrFinished extends Tournament {
def startedAt: DateTime
def standing: Standing
def rankedStanding = (1 to standing.size) zip standing
def rankedPlayers = (1 to players.size) zip players
def winner = standing.headOption
def winner = players.headOption
def winnerUserId = winner map (_.username)
def encode(status: Status) = new RawTournament(
@ -62,7 +64,8 @@ sealed trait StartedOrFinished extends Tournament {
status = status.id,
data = data,
startedAt = startedAt.some,
pairings = pairings map (_.encode))
pairings = pairings map (_.encode),
players = players)
def finishedAt = startedAt + duration
}
@ -73,10 +76,9 @@ case class Created(
import data._
def readyToStart = users.size >= minUsers
def readyToStart = players.size >= minPlayers
def pairings = Nil
lazy val standing = Standing of this
def encode = new RawTournament(
id = id,
@ -85,17 +87,17 @@ case class Created(
def join(user: User): Valid[Created] = contains(user).fold(
!!("User %s is already part of the tournament" format user.id),
withUsers(users :+ user.id).success
withPlayers(players :+ Player(user)).success
)
def withdraw(user: User): Valid[Created] = contains(user).fold(
withUsers(users filterNot (user.id ==)).success,
withPlayers(players filterNot (_ is user)).success,
!!("User %s is not part of the tournament" format user.id)
)
def start = Started(id, data, DateTime.now, Nil)
private def withPlayers(s: Players) = copy(data = data.copy(players = s))
private def withUsers(x: List[String]) = copy(data = data.copy(users = x))
def start = Started(id, data, DateTime.now, Nil)
}
case class Started(
@ -104,8 +106,6 @@ case class Started(
startedAt: DateTime,
pairings: List[Pairing]) extends StartedOrFinished {
lazy val standing = Standing of this
def addPairings(ps: NonEmptyList[Pairing]) =
copy(pairings = ps.list ::: pairings)
@ -117,24 +117,36 @@ case class Started(
def remainingSeconds: Int = math.max(0, finishedAt.getSeconds - nowSeconds).toInt
def finish = Finished(
id = id,
data = data,
startedAt = startedAt,
pairings = pairings,
standing = standing)
def finish = refreshPlayers |> { tour
Finished(
id = tour.id,
data = tour.data,
startedAt = tour.startedAt,
pairings = tour.pairings)
}
def encode = encode(Status.Started)
def withdraw(user: User): Valid[Started] = contains(user).fold(
withPlayers(players map {
case p if p is user p.doWithdraw
case p p
}).success,
!!("User %s is not part of the tournament" format user.id)
)
private def withPlayers(s: Players) = copy(data = data.copy(players = s))
private def refreshPlayers = withPlayers(Player refresh this)
def encode = refreshPlayers.encode(Status.Started)
}
case class Finished(
id: String,
data: Data,
startedAt: DateTime,
pairings: List[Pairing],
standing: Standing) extends StartedOrFinished {
pairings: List[Pairing]) extends StartedOrFinished {
def encode = encode(Status.Finished) withStanding standing
def encode = encode(Status.Finished)
}
case class RawTournament(
@ -143,7 +155,7 @@ case class RawTournament(
data: Data,
startedAt: Option[DateTime] = None,
pairings: List[RawPairing] = Nil,
standing: List[Player] = Nil) {
players: List[Player] = Nil) {
def created: Option[Created] = (status == Status.Created.id) option Created(
id = id,
@ -165,8 +177,7 @@ case class RawTournament(
id = id,
data = data,
startedAt = stAt,
decodePairings,
standing = standing)
decodePairings)
def decodePairings = pairings map (_.decode) flatten
@ -176,7 +187,7 @@ case class RawTournament(
case Status.Finished finished
}
def withStanding(s: Standing) = copy(standing = s)
def withPlayers(s: Players) = copy(players = s)
}
object Tournament {
@ -184,24 +195,34 @@ object Tournament {
import lila.core.Form._
def apply(
createdBy: String,
createdBy: User,
clock: TournamentClock,
minutes: Int,
minUsers: Int): Created = Created(
minPlayers: Int): Created = Created(
id = Random nextString 8,
data = Data(
name = RandomName(),
createdBy = createdBy,
clock = clock,
createdBy = createdBy.id,
createdAt = DateTime.now,
minutes = minutes,
minUsers = minUsers,
users = List(createdBy))
minPlayers = minPlayers,
players = List(Player(createdBy)))
)
val clockTimes = 0 to 10 by 1
val clockTimeDefault = 2
val clockTimeChoices = options(clockTimes, "%d minute{s}")
val clockIncrements = 0 to 5 by 1
val clockIncrementDefault = 0
val clockIncrementChoices = options(clockIncrements, "%d second{s}")
val minutes = 5 to 60 by 5
val minuteDefault = 10
val minuteChoices = options(minutes, "%d minute{s}")
val minUsers = (2 to 4) ++ (5 to 30 by 5)
val minUserDefault = 10
val minUserChoices = options(minUsers, "%d player{s}")
val minPlayers = (2 to 4) ++ (5 to 30 by 5)
val minPlayerDefault = 10
val minPlayerChoices = options(minPlayers, "%d player{s}")
}

View file

@ -20,7 +20,7 @@ final class TournamentApi(
(tour addPairings pairings) |> { tour2
for {
_ repo saveIO tour2
games (pairings map makeGame(tour.id)).sequence
games (pairings map makeGame(tour)).sequence
_ (games map socket.notifyPairing).sequence
} yield ()
}
@ -30,9 +30,10 @@ final class TournamentApi(
tournament hasTournament.fold(
io(alreadyInATournament(me)),
Tournament(
createdBy = me.id,
createdBy = me,
clock = TournamentClock(setup.clockTime, setup.clockIncrement),
minutes = setup.minutes,
minUsers = setup.minUsers
minPlayers = setup.minPlayers
) |> { created repo saveIO created map (_ created.success) }
)
} yield tournament
@ -43,20 +44,13 @@ final class TournamentApi(
def finish(started: Started): IO[Unit] =
repo saveIO started.finish doIf started.readyToFinish
def join(tour: Created, me: User): IO[Valid[Unit]] = for {
hasTournament repo userHasRunningTournament me.id
tour2Valid = for {
tour2 tour join me
_ hasTournament.fold(alreadyInATournament(me), true.success)
} yield tour2
result tour2Valid.fold(
err io(failure(err)),
tour2 for {
_ repo saveIO tour2
_ io(socket reload tour.id)
} yield ().success
): IO[Valid[Unit]]
} yield result
def join(tour: Created, me: User): Valid[IO[Unit]] = for {
tour2 tour join me
} yield for {
withdrawIds repo withdraw me
_ repo saveIO tour2
_ ((tour.id :: withdrawIds) map socket.reload).sequence
} yield ()
def withdraw(tour: Created, me: User): IO[Unit] = (tour withdraw me).fold(
err putStrLn(err.shows),
@ -79,14 +73,14 @@ final class TournamentApi(
} | io(none)
} yield result
private def makeGame(tournamentId: String)(pairing: Pairing): IO[DbGame] = for {
private def makeGame(tour: Started)(pairing: Pairing): IO[DbGame] = for {
user1 getUser(pairing.user1) map (_ err "No such user " + pairing)
user2 getUser(pairing.user2) map (_ err "No such user " + pairing)
variant = chess.Variant.Standard
game = DbGame(
game = chess.Game(
board = chess.Board init variant,
clock = chess.Clock(1, 0).some
clock = tour.clock.chessClock.some
),
ai = None,
whitePlayer = DbPlayer.white withUser user1,
@ -94,7 +88,7 @@ final class TournamentApi(
creatorColor = chess.Color.White,
mode = chess.Mode.Rated,
variant = variant
).withTournamentId(tournamentId)
).withTournamentId(tour.id)
.withId(pairing.gameId)
.start
.startClock(2)

View file

@ -0,0 +1,14 @@
package lila
package tournament
// All durations are expressed in seconds
case class TournamentClock(
limit: Int,
increment: Int) {
def limitInMinutes = limit / 60
def show = limitInMinutes.toString + " + " + increment.toString
def chessClock = chess.Clock(limit, increment)
}

View file

@ -6,9 +6,12 @@ import com.novus.salat.dao._
import com.mongodb.casbah.MongoCollection
import com.mongodb.casbah.query.Imports._
import scalaz.effects._
import scalaz.Success
import org.joda.time.DateTime
import org.scala_tools.time.Imports._
import user.User
class TournamentRepo(collection: MongoCollection)
extends SalatDAO[RawTournament, String](collection) {
@ -18,7 +21,7 @@ class TournamentRepo(collection: MongoCollection)
def startedById(id: String): IO[Option[Started]] = byIdAs(id, _.started)
private def byIdAs[A](id: String, as: RawTournament => Option[A]): IO[Option[A]] = io {
private def byIdAs[A](id: String, as: RawTournament Option[A]): IO[Option[A]] = io {
findOneById(id) flatMap as
}
@ -59,6 +62,17 @@ class TournamentRepo(collection: MongoCollection)
save(tournament.encode)
}
def withdraw(user: User): IO[List[String]] = for {
createds created
createdIds (createds map (_ withdraw user) collect {
case Success(tour) saveIO(tour) map (_ tour.id)
}).sequence
starteds started
startedIds (starteds map (_ withdraw user) collect {
case Success(tour) saveIO(tour) map (_ tour.id)
}).sequence
} yield createdIds ::: startedIds
private def idSelector(id: String): DBObject = DBObject("_id" -> id)
private def idSelector(tournament: Tournament): DBObject = idSelector(tournament.id)
}

View file

@ -4,5 +4,5 @@ package object tournament {
type Pairings = List[tournament.Pairing]
type Standing = List[Player]
type Players = List[tournament.Player]
}

View file

@ -7,8 +7,24 @@ title = "New tournament") {
<div id="tournament">
<div class="content_box tournament_box">
<h1>New tournament</h1>
<form action="@routes.Tournament.create" method="POST">
<form class="plain" action="@routes.Tournament.create" method="POST">
<table>
<tr>
<th>
<label for="@form("clockTime").id">Clock time</label>
</th>
<td>
@base.select(form("clockTime"), Tournament.clockTimeChoices)
</td>
</tr>
<tr>
<th>
<label for="@form("clockIncrement").id">Clock increment</label>
</th>
<td>
@base.select(form("clockIncrement"), Tournament.clockIncrementChoices)
</td>
</tr>
<tr>
<th>
<label for="@form("minutes").id">Duration</label>
@ -19,10 +35,10 @@ title = "New tournament") {
</tr>
<tr>
<th>
<label for="@form("minUsers").id">Players</label>
<label for="@form("minPlayers").id">Players</label>
</th>
<td>
@base.select(form("minUsers"), Tournament.minUserChoices)
@base.select(form("minPlayers"), Tournament.minPlayerChoices)
</td>
</tr>
<tr>

View file

@ -4,8 +4,8 @@
<div class="box">
@showDate(tour.createdAt)
<br /><br />
@tour.nbUsers / @tour.minUsers players
@tour.minPlayers players
<br /><br />
<span class="s16 clock">@tour.showClock</span>
<span class="s16 clock">@tour.clock.show</span>
</div>
</div>

View file

@ -7,25 +7,5 @@ goodies = tournament.infoBox(tour),
version = version,
title = tour.nameT) {
<h1>@tour.nameT</h1>
<div>
Waiting for @tour.missingUsers players
</div>
<div class="user_list">
@tournament.userList(users)
</div>
@ctx.me.map { me =>
<div>
@if(tour contains me) {
<form action="@routes.Tournament.withdraw(tour.id)" method="POST">
<input type="submit" class="submit button" value="Withdraw" />
</form>
} else {
<form action="@routes.Tournament.join(tour.id)" method="POST">
<input type="submit" class="submit button" value="Join tournament" />
</form>
}
</div>
}
@tournament.show.createdInner(tour, users)
}

View file

@ -0,0 +1,41 @@
@(tour: lila.tournament.Created, users: List[User])(implicit ctx: Context)
<h1>@tour.nameT</h1>
<div class="user_list">
<table class="data user_list">
<thead>
<tr>
<th class="large">
@if(tour.readyToStart) {
Tournament is starting!
} else {
Waiting for @tour.missingPlayers players
}
</th>
@ctx.me.map { me =>
<th>
@if(tour contains me) {
<form class="inline" action="@routes.Tournament.withdraw(tour.id)" method="POST">
<input type="submit" class="submit button strong" value="Withdraw" />
</form>
} else {
<form class="inline" action="@routes.Tournament.join(tour.id)" method="POST">
<input type="submit" class="submit button strong" value="Join tournament" />
</form>
}
</th>
}
</tr>
</thead>
<tbody>
@users.map { user =>
<tr>
<td colspan="2">@userLink(user)</td>
</tr>
}
</tbody>
</table>
</div>
<br />
The tournament will start as soon as @tour.minPlayers players join it.

View file

@ -11,13 +11,13 @@
</tr>
</thead>
<tbody>
@tour.rankedStanding.map {
@tour.rankedPlayers.map {
case (rank, player) => {
<tr>
<td><span class="rank">@rank</span></td>
<td>
<a class="username" href="@routes.User.show(player.username)">
@userIdToUsername(player.username)
<a class="username" href="@routes.User.show(player.id)">
@player.username
</a>
</td>
<td><strong>@player.score</strong></td>
@ -33,4 +33,3 @@
}
</tbody>
</table>

View file

@ -13,16 +13,16 @@
@tours.map { tour =>
<tr>
<td>@linkTo(tour)</td>
<td><span class="s16 clock">@tour.showClock</span></td>
<td><span class="s16 clock">@tour.clock.show</span></td>
<td>@{tour.minutes}m</td>
<td>@tour.nbUsers/@tour.minUsers</td>
<td>@tour.nbPlayers/@tour.minPlayers</td>
<td>
@if(tour contains ctx.me) {
<span class="label">JOINED</span>
}
</td>
</tr>
@moreTr
}
@moreTr
</tbody>
}

View file

@ -1,5 +0,0 @@
@(users: List[User])(implicit ctx: Context)
Players: @users.map { user =>
@userLink(user)
}

View file

@ -15,11 +15,11 @@ $.widget("lichess.game", {
self.options.socketUrl = self.element.data('socket-url');
self.socketAckTimeout;
setInterval(function() {
$('a.view_tournament').each(function() {
location.href = $(this).attr("href");
});
}, 3000);
//setInterval(function() {
//$('a.view_tournament').each(function() {
//location.href = $(this).attr("href");
//});
//}, 3000);
if (self.options.game.started) {
self.indicateTurn();
@ -697,7 +697,6 @@ $.widget("lichess.clock", {
self.element.addClass('running');
var end_time = new Date().getTime() + self.options.time;
self.options.interval = setInterval(function() {
console.debug(this);
if (self.options.state == 'running') {
var current_time = Math.round(end_time - new Date().getTime());
if (current_time <= 0) {

View file

@ -575,7 +575,7 @@ div.pagination span.current,
div.hooks td.action:hover,
div.hooks table.empty_table tr:hover,
div.progressbar.flashy div,
div.lichess_table .button.strong:hover,
.button.strong:hover,
div.locale_menu a.active
{
color: #fff;
@ -591,7 +591,7 @@ div.locale_menu a.active
/* strong inactive gradient */
div.hooks td.action,
.button:hover,
div.lichess_table .button.strong,
.button.strong,
div.notification
{
background: #ffffff;
@ -619,7 +619,7 @@ div.notification
div.lichess_chat,
div.undertable,
div.lichess_board_wrap,
div.lichess_table .button.strong,
.button.strong,
div.content_box
{
box-shadow: 0 0 7px #d0d0d0;

View file

@ -26,7 +26,7 @@ body.dark div.undergame_box,
body.dark div.clock,
body.dark #GameText,
body.dark #GameBoard table.boardTable,
body.dark div.lichess_table .button.strong,
body.dark .button.strong,
body.dark #tournament_side,
body.dark div.content_box
{
@ -86,6 +86,7 @@ body.dark div.adv_chart,
body.dark #top .dropdown,
body.dark div.lichess_overboard p.explanations,
body.dark div.search_status,
body.dark #tournament table.data,
body.dark #tournament table.data thead th,
#tournament table.data
{
@ -294,7 +295,7 @@ body.dark #tournament table.data thead
}
/* strong inactive gradient */
body.dark div.hooks td.action,
body.dark div.lichess_table .button.strong,
body.dark .button.strong,
body.dark .button:hover
{
background: #505050;
@ -315,7 +316,7 @@ body.dark div.pagination span.current,
body.dark #top span.new_messages.unread,
body.dark div.hooks td.action:hover,
body.dark div.hooks table.empty_table tr:hover,
body.dark div.lichess_table .button.strong:hover,
body.dark .button.strong:hover,
body.dark div.progressbar.flashy div,
body.dark div.locale_menu a.active
{

View file

@ -59,34 +59,40 @@
width: 511px;
}
#tournament form {
#tournament form.inline {
display: inline;
float: right;
padding-right: 0.6em;
}
#tournament form.plain {
margin-top: 2em;
}
#tournament form td {
#tournament form.plain td {
padding: 0.6em;
}
#tournament form label {
#tournament form.plain label {
margin-right: 10px;
}
#tournament form input {
#tournament form.plain input {
width: 96%;
padding: 0.5% 1%;
}
#tournament form select {
#tournament form.plain select {
padding: 0.5% 1%;
text-transform: capitalize;
width: 99%;
}
#tournament form input.submit {
#tournament form.plain input.submit {
width: 99%;
}
#tournament form .error {
#tournament form.plain .error {
margin-left: 160px;
color: red;
}