Teams of individual players: Teams composition page ok

This commit is contained in:
Claude Brisson
2025-01-26 01:28:44 +01:00
parent 8f8e23d5b1
commit 169546ae66
15 changed files with 193 additions and 44 deletions

View File

@@ -6,12 +6,12 @@ import org.jeudego.pairgoth.model.Game
import org.jeudego.pairgoth.model.MacMahon import org.jeudego.pairgoth.model.MacMahon
import org.jeudego.pairgoth.model.Pairable import org.jeudego.pairgoth.model.Pairable
import org.jeudego.pairgoth.model.PairingType import org.jeudego.pairgoth.model.PairingType
import org.jeudego.pairgoth.model.Player
import org.jeudego.pairgoth.model.Tournament import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.model.getID import org.jeudego.pairgoth.model.getID
import org.jeudego.pairgoth.model.historyBefore import org.jeudego.pairgoth.model.historyBefore
import org.jeudego.pairgoth.pairing.HistoryHelper import org.jeudego.pairgoth.pairing.HistoryHelper
import org.jeudego.pairgoth.pairing.solver.MacMahonSolver import org.jeudego.pairgoth.pairing.solver.MacMahonSolver
import kotlin.math.ceil
import kotlin.math.floor import kotlin.math.floor
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@@ -129,20 +129,23 @@ fun Tournament<*>.getSortedPairables(round: Int, includePreliminary: Boolean = f
return sortedPairables return sortedPairables
} }
fun Tournament<*>.populateFrozenStandings(sortedPairables: List<Json.Object>, round: Int = rounds) { fun Tournament<*>.populateStandings(sortedPairables: List<Json.Object>, round: Int = rounds) {
val sortedMap = sortedPairables.associateBy { val sortedMap = sortedPairables.associateBy {
it.getID()!! it.getID()!!
} }
// refresh name, firstname, club and level // refresh name, firstname, club and level
sortedMap.forEach { (id, player) -> sortedMap.forEach { (id, pairable) ->
val mutable = player as Json.MutableObject val mutable = pairable as Json.MutableObject
val live = players[id]!! pairables[id]?.let {
mutable["name"] = live.name mutable["name"] = it.name
mutable["firstname"] = live.firstname if (it is Player) {
mutable["club"] = live.club mutable["firstname"] = it.firstname
mutable["rating"] = live.rating }
mutable["rank"] = live.rank mutable["club"] = it.club
mutable["rating"] = it.rating
mutable["rank"] = it.rank
}
} }
// fill result // fill result

View File

@@ -2,8 +2,13 @@ package org.jeudego.pairgoth.api
import com.republicate.kson.Json import com.republicate.kson.Json
import com.republicate.kson.toJsonArray import com.republicate.kson.toJsonArray
import com.republicate.kson.toMutableJsonArray
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
import org.jeudego.pairgoth.model.Game import org.jeudego.pairgoth.model.Game
import org.jeudego.pairgoth.model.ID
import org.jeudego.pairgoth.model.Player
import org.jeudego.pairgoth.model.TeamTournament
import org.jeudego.pairgoth.model.TeamTournament.Team
import org.jeudego.pairgoth.model.Tournament import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.model.getID import org.jeudego.pairgoth.model.getID
import org.jeudego.pairgoth.model.toID import org.jeudego.pairgoth.model.toID
@@ -21,8 +26,8 @@ object PairingHandler: PairgothApiHandler {
val playing = tournament.games(round).values.flatMap { val playing = tournament.games(round).values.flatMap {
listOf(it.black, it.white) listOf(it.black, it.white)
}.toSet() }.toSet()
val unpairables = tournament.pairables.values.filter { it.final && it.skip.contains(round) }.sortedByDescending { it.rating }.map { it.id }.toJsonArray() val unpairables = tournament.pairables.values.filter { it.final && !it.canPlay(round) }.sortedByDescending { it.rating }.map { it.id }.toJsonArray()
val pairables = tournament.pairables.values.filter { it.final && !it.skip.contains(round) && !playing.contains(it.id) }.sortedByDescending { it.rating }.map { it.id }.toJsonArray() val pairables = tournament.pairables.values.filter { it.final && it.canPlay(round) && !playing.contains(it.id) }.sortedByDescending { it.rating }.map { it.id }.toJsonArray()
val games = tournament.games(round).values.sortedBy { val games = tournament.games(round).values.sortedBy {
if (it.table == 0) Int.MAX_VALUE else it.table if (it.table == 0) Int.MAX_VALUE else it.table
} }

View File

@@ -22,7 +22,6 @@ import java.nio.charset.StandardCharsets
import java.text.DecimalFormat import java.text.DecimalFormat
import java.text.Normalizer import java.text.Normalizer
import java.util.* import java.util.*
import kotlin.collections.ArrayList
object StandingsHandler: PairgothApiHandler { object StandingsHandler: PairgothApiHandler {
override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? { override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? {
@@ -31,7 +30,7 @@ object StandingsHandler: PairgothApiHandler {
val includePreliminary = request.getParameter("include_preliminary")?.let { it.toBoolean() } ?: false val includePreliminary = request.getParameter("include_preliminary")?.let { it.toBoolean() } ?: false
val sortedPairables = tournament.getSortedPairables(round, includePreliminary) val sortedPairables = tournament.getSortedPairables(round, includePreliminary)
tournament.populateFrozenStandings(sortedPairables, round) tournament.populateStandings(sortedPairables, round)
val acceptHeader = request.getHeader("Accept") as String? val acceptHeader = request.getHeader("Accept") as String?
val accept = acceptHeader?.substringBefore(";") val accept = acceptHeader?.substringBefore(";")

View File

@@ -26,6 +26,8 @@ sealed class Pairable(val id: ID, val name: String, val rating: Int, val rank: I
fun equals(other: Pairable): Boolean { fun equals(other: Pairable): Boolean {
return id == other.id return id == other.id
} }
open fun canPlay(round: Int) = !skip.contains(round)
} }
object ByePlayer: Pairable(0, "bye", 0, Int.MIN_VALUE, true) { object ByePlayer: Pairable(0, "bye", 0, Int.MIN_VALUE, true) {

View File

@@ -255,10 +255,14 @@ class TeamTournament(
json["rank"] = rank json["rank"] = rank
country?.also { json["country"] = it } country?.also { json["country"] = it }
club?.also { json["club"] = it } club?.also { json["club"] = it }
json["names"] = playerIds.mapNotNull { players[it]?.fullName() }.toJsonArray()
json["ranks"] = playerIds.mapNotNull { players[it]?.rank }.toJsonArray()
} }
val teamOfIndividuals: Boolean get() = type.individual val teamOfIndividuals: Boolean get() = type.individual
override val skip get() = playerIds.map { players[it]!!.skip }.reduce { left, right -> (left union right) as MutableSet<Int> } // override val skip get() = playerIds.map { players[it]!!.skip }.reduce { left, right -> (left union right) as MutableSet<Int> }
override fun canPlay(round: Int) = teamPlayers.filter { it.canPlay(round) }.size == type.playersNumber
} }
fun teamFromJson(json: Json.Object, default: TeamTournament.Team? = null): Team { fun teamFromJson(json: Json.Object, default: TeamTournament.Team? = null): Team {
@@ -288,7 +292,8 @@ class TeamTournament(
fun Tournament.Companion.fromJson(json: Json.Object, default: Tournament<*>? = null): Tournament<*> { fun Tournament.Companion.fromJson(json: Json.Object, default: Tournament<*>? = null): Tournament<*> {
val type = json.getString("type")?.uppercase()?.let { Tournament.Type.valueOf(it) } ?: default?.type ?: badRequest("missing type") val type = json.getString("type")?.uppercase()?.let { Tournament.Type.valueOf(it) } ?: default?.type ?: badRequest("missing type")
// No clean way to avoid this redundancy // No clean way to avoid this redundancy
val tournament = if (type.playersNumber == 1) val tournament =
if (type.playersNumber == 1)
StandardTournament( StandardTournament(
id = json.getInt("id") ?: default?.id ?: nextTournamentId, id = json.getInt("id") ?: default?.id ?: nextTournamentId,
type = type, type = type,
@@ -301,7 +306,7 @@ fun Tournament.Companion.fromJson(json: Json.Object, default: Tournament<*>? = n
location = json.getString("location") ?: default?.location ?: badRequest("missing location"), location = json.getString("location") ?: default?.location ?: badRequest("missing location"),
online = json.getBoolean("online") ?: default?.online ?: false, online = json.getBoolean("online") ?: default?.online ?: false,
komi = json.getDouble("komi") ?: default?.komi ?: 7.5, komi = json.getDouble("komi") ?: default?.komi ?: 7.5,
rules = json.getString("rules")?.let { Rules.valueOf(it) } ?: default?.rules ?: Rules.FRENCH, rules = json.getString("rules")?.let { Rules.valueOf(it) } ?: default?.rules ?: if (json.getString("country")?.lowercase(Locale.ROOT) == "fr") Rules.FRENCH else Rules.AGA,
gobanSize = json.getInt("gobanSize") ?: default?.gobanSize ?: 19, gobanSize = json.getInt("gobanSize") ?: default?.gobanSize ?: 19,
timeSystem = json.getObject("timeSystem")?.let { TimeSystem.fromJson(it) } ?: default?.timeSystem ?: badRequest("missing timeSystem"), timeSystem = json.getObject("timeSystem")?.let { TimeSystem.fromJson(it) } ?: default?.timeSystem ?: badRequest("missing timeSystem"),
rounds = json.getInt("rounds") ?: default?.rounds ?: badRequest("missing rounds"), rounds = json.getInt("rounds") ?: default?.rounds ?: badRequest("missing rounds"),
@@ -317,7 +322,7 @@ fun Tournament.Companion.fromJson(json: Json.Object, default: Tournament<*>? = n
startDate = json.getString("startDate")?.let { LocalDate.parse(it) } ?: default?.startDate ?: badRequest("missing startDate"), startDate = json.getString("startDate")?.let { LocalDate.parse(it) } ?: default?.startDate ?: badRequest("missing startDate"),
endDate = json.getString("endDate")?.let { LocalDate.parse(it) } ?: default?.endDate ?: badRequest("missing endDate"), endDate = json.getString("endDate")?.let { LocalDate.parse(it) } ?: default?.endDate ?: badRequest("missing endDate"),
director = json.getString("director") ?: default?.director ?: "", director = json.getString("director") ?: default?.director ?: "",
country = json.getString("country") ?: default?.country ?: badRequest("missing country"), country = (json.getString("country") ?: default?.country ?: "fr").let { if (it.isEmpty()) "fr" else it },
location = json.getString("location") ?: default?.location ?: badRequest("missing location"), location = json.getString("location") ?: default?.location ?: badRequest("missing location"),
online = json.getBoolean("online") ?: default?.online ?: false, online = json.getBoolean("online") ?: default?.online ?: false,
komi = json.getDouble("komi") ?: default?.komi ?: 7.5, komi = json.getDouble("komi") ?: default?.komi ?: 7.5,

View File

@@ -284,6 +284,21 @@
max-width: max(50vw, 20em); max-width: max(50vw, 20em);
} }
#composition.multi-select .listitem {
cursor: default;
i.icon {
cursor: pointer;
@media (hover: hover) {
display: none;
}
}
&:hover {
i.icon {
display: inline;
}
}
}
/* pairing section */ /* pairing section */
#pairing-content { #pairing-content {
@@ -345,7 +360,7 @@
} }
} }
} }
#pairables { #pairables, #teams {
margin-bottom: 1em; margin-bottom: 1em;
} }
#paired { #paired {
@@ -379,7 +394,7 @@
gap: 1em; gap: 1em;
max-width: max(10em, 20vw); max-width: max(10em, 20vw);
} }
#unpairables, #previous_games { #unpairables, #previous_games, #composition {
display: flex; display: flex;
flex-flow: column nowrap; flex-flow: column nowrap;
min-height: 10vh; min-height: 10vh;

View File

@@ -124,6 +124,7 @@ Surround winner's name or ½-½ Namen des Gewinners oder 0,5 - 0,5 umrahmen
Signature: Unterschrift: Signature: Unterschrift:
Swiss Schweizer System Swiss Schweizer System
Table Tisch Table Tisch
Teams Teams
Team of 2 individual players Team aus 2 Einzelspielern Team of 2 individual players Team aus 2 Einzelspielern
Team of 3 individual players Team aus 3 Einzelspielern Team of 3 individual players Team aus 3 Einzelspielern
Team of 4 individual players Team aus 4 Einzelspielern Team of 4 individual players Team aus 4 Einzelspielern

View File

@@ -120,6 +120,7 @@ Standings after round Classement après la ronde
Stay in the browser Rester dans le navigateur&nbsp; Stay in the browser Rester dans le navigateur&nbsp;
Sudden death Mort subite Sudden death Mort subite
Swiss Suisse Swiss Suisse
Teams Équipes
Team of 2 individual players Équipe de 2 joueurs individuels Team of 2 individual players Équipe de 2 joueurs individuels
Team of 3 individual players Équipe de 3 joueurs individuels Team of 3 individual players Équipe de 3 joueurs individuels
Team of 4 individual players Équipe de 4 joueurs individuels Team of 4 individual players Équipe de 4 joueurs individuels

View File

@@ -124,6 +124,7 @@ Sudden death 서든 데스
Surround winner's name or ½-½ 승자의 이름 또는 0.5 - 0.5을 둘러싸기 Surround winner's name or ½-½ 승자의 이름 또는 0.5 - 0.5을 둘러싸기
Signature: 서명: Signature: 서명:
Swiss 스위스 Swiss 스위스
Teams 팀
Team of 2 individual players 2인 팀 Team of 2 individual players 2인 팀
Team of 3 individual players 3인 팀 Team of 3 individual players 3인 팀
Team of 4 individual players 4인 팀 Team of 4 individual players 4인 팀

View File

@@ -135,6 +135,8 @@ function hideOpponents() {
onLoad(()=>{ onLoad(()=>{
// note - this handler is also in use for lists on Mac Mahon super groups and teams pages // note - this handler is also in use for lists on Mac Mahon super groups and teams pages
// CB TODO - there is some code cleaning to to around the listitems reuse and events:
// the on('click') method should not define specific behaviors for this page, just dispatch custom events
$('.listitem').on('click', e => { $('.listitem').on('click', e => {
let listitem = e.target.closest('.listitem'); let listitem = e.target.closest('.listitem');
let box = e.target.closest('.multi-select'); let box = e.target.closest('.multi-select');
@@ -156,17 +158,23 @@ onLoad(()=>{
if (e.detail === 1) { if (e.detail === 1) {
// single click // single click
focused = listitem.toggleClass('selected').attr('draggable', listitem.hasClass('selected')); focused = listitem.toggleClass('selected').attr('draggable', listitem.hasClass('selected'));
if (box.getAttribute('id') === 'pairables') showOpponents(focused) if (box.getAttribute('id') === 'pairables') {
} else if (listitem.closest('#pairing-lists')) { if (focused.hasClass('selected')) showOpponents(focused);
// on pairing page else hideOpponents();
if (listitem.closest('#paired')) {
// double click
hideOpponents()
focused = listitem.attr('draggable', listitem.hasClass('selected'));
editGame(focused);
} else if (listitem.closest('#pairables')) {
editPairable(focused);
} }
} else {
if (listitem.closest('#pairing-lists')) {
// on pairing page
if (listitem.closest('#paired')) {
// double click
hideOpponents()
focused = listitem.attr('draggable', listitem.hasClass('selected'));
editGame(focused);
} else if (listitem.closest('#pairables')) {
editPairable(focused);
}
}
box.dispatchEvent(new CustomEvent('listitem-dblclk', { 'detail': parseInt(listitem.data('id')) }));
} }
} }
box.dispatchEvent(new CustomEvent('listitems')); box.dispatchEvent(new CustomEvent('listitems'));

View File

@@ -105,6 +105,11 @@ function parseRank(rank) {
return ''; return '';
} }
function displayRank(rank) {
rank = parseInt(rank);
return rank < 0 ? `${-rank}k` : `${rank + 1}d`;
}
function fillPlayer(player) { function fillPlayer(player) {
// hack UK / GB // hack UK / GB
let country = player.country.toLowerCase(); let country = player.country.toLowerCase();

View File

@@ -24,6 +24,44 @@ function split(teams) {
}); });
} }
function join(players, team) {
console.log(team)
console.log(teams.get(team))
api.putJson(`tour/${tour_id}/team/${team}`, {
"players": teams.get(team).players.concat(players)
}).then(rst => {
if (rst !== 'error') {
document.location.reload();
}
});
}
function leave(teamId, playerId) {
let team = teams.get(teamId);
let index = team.players.indexOf(playerId);
if (index > -1) {
team.players.splice(index, 1);
api.putJson(`tour/${tour_id}/team/${teamId}`, {
"players": team.players
}).then(rst => {
if (rst !== 'error') {
document.location.reload();
}
});
}
}
function showTeam(teamId) {
let team = teams.get(teamId);
$('#composition')[0].clearChildren();
$('#composition').attr('title', team.name).removeClass('hidden');
$('#composition').data('id', teamId);
for (i = 0; i < team.players.length; ++i) {
let listitem = `<div data-id="${team.players[i]}" class="listitem"><span>${team.names[i]}</span><span>${displayRank(team.ranks[i])}&nbsp;<i class="ui red sign out icon"></i></span></div>`
$('#composition')[0].insertAdjacentHTML('beforeend', listitem);
}
}
onLoad(() => { onLoad(() => {
$('#teamup').on('click', e => { $('#teamup').on('click', e => {
let rows = $('#teamables .selected.listitem') let rows = $('#teamables .selected.listitem')
@@ -39,14 +77,65 @@ onLoad(() => {
let teams = rows.map(item => parseInt(item.data("id"))); let teams = rows.map(item => parseInt(item.data("id")));
if (teams.length !== 0) split(teams); if (teams.length !== 0) split(teams);
}); });
$('#teamables').on('listitems', () => { $('#join').on('click', e => {
let rows = $('#teamables .selected.listitem'); let rows = $('#teamables .selected.listitem');
if (rows.length === teamSize) { let players = rows.map(item => parseInt(item.data("id")));
$('#team-name')[0].value = rows.map(row => row.data('name')).join('-'); let teams = $('#teams .selected.listitem');
$('#teamup').removeClass('disabled'); if (players.length !== 0 && teams.length === 1) {
} else { join(players, parseInt(teams[0].data("id")));
$('#team-name')[0].value = '';
$('#teamup').addClass('disabled');
} }
}); });
}); $('#team-name').on('input', () => {
if ($('#team-name')[0].value === '') {
$('#teamup').addClass('disabled');
} else if ($('#teamables .selected.listitem').length > 0) {
$('#teamup').removeClass('disabled');
}
$('#team-name').data('manual', true);
});
$('#teamables, #teams').on('listitems', () => {
let players = $('#teamables .selected.listitem');
let teams = $('#teams .selected.listitem');
if (players.length === 0) {
$('#teamup').addClass('disabled');
$('#join').addClass('disabled');
if(!$('#team-name').data('manual')) {
$('#team-name')[0].value = '';
}
} else {
if(!$('#team-name').data('manual')) {
$('#team-name')[0].value = players.map(row => row.data('name')).join('-');
}
if ($('#team-name')[0].value !== '') {
$('#teamup').removeClass('disabled');
}
if (teams.length === 1) {
$('#join').removeClass('disabled');
} else {
$('#join').addClass('disabled');
}
}
if (teams.length === 0) {
$('#split').addClass('disabled');
$('#composition').addClass('hidden');
} else {
$('#split').removeClass('disabled');
if (focused && focused.closest('.multi-select').attr('id') === 'teams') {
showTeam(parseInt(focused.data('id')));
}
}
});
/* If we want double-click...
$('#teams').on('listitem-dblclk', e => {
console.log(teams.get(e.detail));
});
*/
$('#composition').on('click', e => {
console.log('click')
if (e.target.matches('.listitem i')) {
let team = parseInt(e.target.closest('.multi-select').data('id'));
let player = parseInt(e.target.closest('.listitem').data('id'));
leave(team, player);
}
});
});

View File

@@ -71,12 +71,10 @@
<option value="PAIRGO" #if($tour && $tour.type == 'PAIRGO') selected #end>Pair-go tournament</option> <option value="PAIRGO" #if($tour && $tour.type == 'PAIRGO') selected #end>Pair-go tournament</option>
<option value="RENGO2" #if($tour && $tour.type == 'RENGO2') selected #end>Rengo with 2 players teams</option> <option value="RENGO2" #if($tour && $tour.type == 'RENGO2') selected #end>Rengo with 2 players teams</option>
<option value="RENGO3" #if($tour && $tour.type == 'RENGO3') selected #end>Rengo with 3 players team</option> <option value="RENGO3" #if($tour && $tour.type == 'RENGO3') selected #end>Rengo with 3 players team</option>
#* TODO
<option value="TEAM2" #if($tour && $tour.type == 'TEAM2') selected #end>Team of 2 individual players</option> <option value="TEAM2" #if($tour && $tour.type == 'TEAM2') selected #end>Team of 2 individual players</option>
<option value="TEAM3" #if($tour && $tour.type == 'TEAM3') selected #end>Team of 3 individual players</option> <option value="TEAM3" #if($tour && $tour.type == 'TEAM3') selected #end>Team of 3 individual players</option>
<option value="TEAM4" #if($tour && $tour.type == 'TEAM4') selected #end>Team of 4 individual players</option> <option value="TEAM4" #if($tour && $tour.type == 'TEAM4') selected #end>Team of 4 individual players</option>
<option value="TEAM5" #if($tour && $tour.type == 'TEAM5') selected #end>Team of 5 individual players</option> <option value="TEAM5" #if($tour && $tour.type == 'TEAM5') selected #end>Team of 5 individual players</option>
*#
</select> </select>
</div> </div>
<div class="right four wide field"> <div class="right four wide field">

View File

@@ -1,7 +1,9 @@
#set($parts = $api.get("tour/${params.id}/part")) #set($parts = $api.get("tour/${params.id}/part"))
#if($tour.type == 'INDIVIDUAL' || $tour.type.startsWith('TEAM')) #if($tour.type == 'INDIVIDUAL')
## Standard tournament
#set($pmap = $utils.toMap($parts)) #set($pmap = $utils.toMap($parts))
#else #else
## Pairgo, rengo and teams of individuals
#set($teams = $api.get("tour/${params.id}/team")) #set($teams = $api.get("tour/${params.id}/team"))
#set($pmap = $utils.toMap($teams)) #set($pmap = $utils.toMap($teams))
#end #end

View File

@@ -18,17 +18,32 @@
Team up Team up
</button> </button>
</div> </div>
<button id="split" class="ui orange right labeled icon floating button"> <button id="join" class="ui green right labeled icon floating disabled button">
<i class="chevron right icon"></i>
Join
</button>
<button id="split" class="ui orange right labeled icon floating disabled button">
<i class="angle double left icon"></i> <i class="angle double left icon"></i>
Split Split
</button> </button>
</div> </div>
<div id="teams" class="multi-select" title="teams"> <div>
<div id="teams" class="multi-select" title="teams">
#foreach($team in $teams) #foreach($team in $teams)
<div data-id="$team.id" class="listitem team"><span class="name">$team.name</span><span>#rank($team.rank)#if($team.country) $team.country#end</span></div> <div data-id="$team.id" class="listitem team"><span class="name">$team.name</span><span>#rank($team.rank)#if($team.country) $team.country#end</span></div>
#end #end
</div>
<div id="composition" class="hidden multi-select" title="">
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<script type="text/javascript">
// XXXXXX
// $teams.class.name
// $teams
let teams = new Map(${teams}.map(team => [team.id, team]));
</script>