From 82fb5051dcfaec007114883a2c3ad815e227cdfc Mon Sep 17 00:00:00 2001 From: gbtami Date: Tue, 24 Dec 2024 09:10:24 +0100 Subject: [PATCH] Auto pairing rating range --- client/lobby.ts | 53 +++++++++++++++++++++++++----- server/auto_pair.py | 5 +-- server/pychess_global_app_state.py | 12 ++++--- server/seek.py | 2 +- server/user.py | 38 ++++++++++++++++++--- server/wsl.py | 15 ++++++--- static/lobby.css | 22 +++++++++---- 7 files changed, 116 insertions(+), 31 deletions(-) diff --git a/client/lobby.ts b/client/lobby.ts index 3a9e35f2a..f4a375f55 100644 --- a/client/lobby.ts +++ b/client/lobby.ts @@ -503,9 +503,9 @@ export class LobbyController implements ChatController { on: { input: e => this.setRatingMin(parseInt((e.target as HTMLInputElement).value)) }, hook: { insert: vnode => this.setRatingMin(parseInt((vnode.elm as HTMLInputElement).value)) }, }), - h('span.rating-min', '-500'), - '/', - h('span.rating-max', '+500'), + h('div.rating-min', '-1000'), + h('span', '/'), + h('div.rating-max', '+1000'), h('input#rating-max.slider', { props: { name: "rating-max", type: "range", min: 0, max: 1000, step: 50, value: vRatingMax }, on: { input: e => this.setRatingMax(parseInt((e.target as HTMLInputElement).value)) }, @@ -596,9 +596,15 @@ export class LobbyController implements ChatController { if (inp.checked) tcs.push(autoPairingTCs[index]); }) - // console.log('autoPairingSubmit()', variants); - // console.log('autoPairingSubmit()', tcs); - this.doSend({ type: "create_auto_pairing", variants: variants, tcs: tcs }); + const minEle = document.getElementById('auto-rating-min') as HTMLInputElement; + const rrMin = Number(minEle.value); + localStorage.auto_rating_min = minEle.value; + + const maxEle = document.getElementById('auto-rating-max') as HTMLInputElement; + const rrMax = Number(maxEle.value); + localStorage.auto_rating_max = maxEle.value; + + this.doSend({ type: "create_auto_pairing", variants: variants, tcs: tcs, rrmin: rrMin, rrmax: rrMax }); } preSelectVariant(variantName: string, chess960: boolean=false) { @@ -749,10 +755,16 @@ export class LobbyController implements ChatController { this.setStartButtons(); } private setRatingMin(val: number) { - document.querySelector("span.rating-min")!.innerHTML = String(val); + document.querySelector("div.rating-min")!.innerHTML = '-' + String(Math.abs(val)); } private setRatingMax(val: number) { - document.querySelector("span.rating-max")!.innerHTML = String(val); + document.querySelector("div.rating-max")!.innerHTML = '+' + String(val); + } + private setAutoRatingMin(val: number) { + document.querySelector("div.auto-rating-min")!.innerHTML = '-' + String(Math.abs(val)); + } + private setAutoRatingMax(val: number) { + document.querySelector("div.auto-rating-max")!.innerHTML = '+' + String(val); } private setFen() { const e = document.getElementById('fen') as HTMLInputElement; @@ -1005,7 +1017,31 @@ export class LobbyController implements ChatController { const checked = localStorage[`tc_${tcName}`] ?? "false"; tcList.push(h('label', [h('input', { props: { name: `tc_${tcName}`, type: "checkbox" }, attrs: { checked: checked === "true" } }), tcName])); }) + patch(document.querySelector('div.timecontrols') as Element, h('div.timecontrols', tcList)); + + const aRatingMin = localStorage.auto_rating_min ?? -1000; + const aRatingMax = localStorage.auto_rating_max ?? 1000; + const aRatingRange = [ + _('Rating range'), + h('div.rating-range', [ + h('input#auto-rating-min.slider', { + props: { name: "rating-min", type: "range", min: -1000, max: 0, step: 50, value: aRatingMin }, + on: { input: e => this.setAutoRatingMin(parseInt((e.target as HTMLInputElement).value)) }, + hook: { insert: vnode => this.setAutoRatingMin(parseInt((vnode.elm as HTMLInputElement).value)) }, + }), + h('div.auto-rating-min', '-1000'), + h('span', '/'), + h('div.auto-rating-max', '+1000'), + h('input#auto-rating-max.slider', { + props: { name: "rating-max", type: "range", min: 0, max: 1000, step: 50, value: aRatingMax }, + on: { input: e => this.setAutoRatingMax(parseInt((e.target as HTMLInputElement).value)) }, + hook: { insert: vnode => this.setAutoRatingMax(parseInt((vnode.elm as HTMLInputElement).value)) }, + }), + ]), + ]; + + patch(document.querySelector('div.auto-rating-range') as Element, h('div.auto-rating-range', aRatingRange)); } onMessage(evt: MessageEvent) { @@ -1289,6 +1325,7 @@ export function lobbyView(model: PyChessModel): VNode[] { h('div.auto-container', {attrs: {id: 'panel-4', role: 'tabpanel', tabindex: '-1', 'aria-labelledby': 'tab-4'}}, [ h('div.seeks-table', [h('div.seeks-wrapper', [h('div.auto-pairing', [ h('div.auto-pairing-actions'), + h('div.auto-rating-range'), h('div.timecontrols'), h('div.variants'), ])])]) diff --git a/server/auto_pair.py b/server/auto_pair.py index 516ba806b..3f30ca891 100644 --- a/server/auto_pair.py +++ b/server/auto_pair.py @@ -45,6 +45,7 @@ async def auto_pair(app_state, user, auto_variant_tc, other_user=None, matching_ def find_matching_user(app_state, user, variant_tc): """Return first compatible user from app_state.auto_pairing_users if there is any, else None""" + variant, chess960, _, _, _ = variant_tc return next( ( user_candidate @@ -56,7 +57,7 @@ def find_matching_user(app_state, user, variant_tc): and user.ready_for_auto_pairing and auto_pairing_user.ready_for_auto_pairing ) - if user.compatible_with_other_user(user_candidate) + if user.auto_compatible_with_other_user(user_candidate, variant, chess960) ), None, ) @@ -80,7 +81,7 @@ def find_matching_seek(app_state, user, variant_tc): and seek.color == "r" and seek.fen == "" ) - if user.compatible_with_seek(seek_candidate) + if user.auto_compatible_with_seek(seek_candidate) ), None, ) diff --git a/server/pychess_global_app_state.py b/server/pychess_global_app_state.py index 65f98098a..14e564507 100644 --- a/server/pychess_global_app_state.py +++ b/server/pychess_global_app_state.py @@ -98,7 +98,7 @@ def __init__(self, app: web.Application): self.sent_lichess_team_msg: List[date] = [] self.seeks: dict[str, Seek] = {} - self.auto_pairing_users: set = set() + self.auto_pairing_users: dict[User, (int, int)] = {} self.auto_pairings: dict[str, set] = {} self.games: dict[str, Game] = {} self.invites: dict[str, Seek] = {} @@ -243,10 +243,11 @@ async def init_from_db(self): if variant_tc not in self.auto_pairings: self.auto_pairings[variant_tc] = set() - for username in doc["users"]: + for username, rrange in doc["users"]: user = await self.users.get(username) - self.auto_pairing_users.add(user) self.auto_pairings[variant_tc].add(user) + if user not in self.auto_pairing_users: + self.auto_pairing_users[user] = rrange # Load seeks from database async for doc in self.db.seek.find(): @@ -467,7 +468,10 @@ async def server_shutdown(self): auto_pairings = [ { "variant_tc": variant_tc, - "users": [user.username for user in self.auto_pairings[variant_tc]], + "users": [ + (user.username, self.auto_pairing_users[user]) + for user in self.auto_pairings[variant_tc] + ], } for variant_tc in self.auto_pairings ] diff --git a/server/seek.py b/server/seek.py index e0b537171..dacb10fec 100644 --- a/server/seek.py +++ b/server/seek.py @@ -42,7 +42,7 @@ def __init__( self.color = color self.fen = "" if fen is None else fen self.rated = rated - self.rating = creator.get_rating(variant, chess960).rating_prov[0] + self.rating = creator.get_rating_value(variant, chess960) self.rrmin = rrmin if (rrmin is not None and rrmin != -1000) else -10000 self.rrmax = rrmax if (rrmax is not None and rrmax != 1000) else 10000 self.base = base diff --git a/server/user.py b/server/user.py index c4cca071c..daaf89dc3 100644 --- a/server/user.py +++ b/server/user.py @@ -61,6 +61,7 @@ def __init__( self.username = username self.seeks: dict[int, Seek] = {} + self.ready_for_auto_pairing = False self.lobby_sockets: Set[WebSocketResponse] = set() self.tournament_sockets: dict[str, WebSocketResponse] = {} # {tournamentId: set()} @@ -269,7 +270,7 @@ async def clear_seeks(self): def remove_from_auto_pairings(self): try: - self.app_state.auto_pairing_users.remove(self) + del self.app_state.auto_pairing_users[self] except KeyError: pass [ @@ -371,14 +372,43 @@ def remove_ws_for_game(self, game_id, ws) -> bool: else: return False - def compatible_with_other_user(self, other_user): + def auto_compatible_with_other_user(self, other_user, variant, chess960): + """Users are compatible when their auto pairing rating ranges are overlapped + and the users are not blocked by each other""" + + rating = self.get_rating_value(variant, chess960) + rr = self.app_state.auto_pairing_users[self] + a = (rating + rr[0], rating + rr[1]) + + rating = other_user.get_rating_value(variant, chess960) + rr = self.app_state.auto_pairing_users[other_user] + b = (rating + rr[0], rating + rr[1]) + return (other_user.username not in self.blocked) and ( - self.username not in other_user.blocked + self.username not in other_user.blocked and max(a[0], b[0]) <= min(a[1], b[1]) + ) + + def auto_compatible_with_seek(self, seek): + """Seek is auto pairing compatible when the rating ranges are overlapped + and the users are not blocked by each other""" + + rating = self.get_rating_value(seek.variant, seek.chess960) + rr = self.app_state.auto_pairing_users[self] + a = (rating + rr[0], rating + rr[1]) + + other_user = seek.creator + rating = other_user.get_rating_value(seek.variant, seek.chess960) + b = (rating + seek.rrmin, rating + seek.rrmax) + + return (other_user.username not in self.blocked) and ( + self.username not in other_user.blocked and max(a[0], b[0]) <= min(a[1], b[1]) ) def compatible_with_seek(self, seek): + """Seek is compatible when my rating is inside the seek rating range + and the users are not blocked by each other""" + self_rating = self.get_rating_value(seek.variant, seek.chess960) - print(self.username, seek.variant, seek.chess960, self_rating) seek_user = self.app_state.users[seek.creator.username] return ( (seek_user.username not in self.blocked) diff --git a/server/wsl.py b/server/wsl.py index 39ef59b05..94aa96258 100644 --- a/server/wsl.py +++ b/server/wsl.py @@ -75,6 +75,7 @@ async def finally_logic(app_state: PychessGlobalAppState, ws, user): async def process_message(app_state: PychessGlobalAppState, user, ws, data): + print(user.username, data) if data["type"] == "create_ai_challenge": await handle_create_ai_challenge(app_state, ws, user, data) elif data["type"] == "create_seek": @@ -375,6 +376,12 @@ async def handle_create_auto_pairing(app_state, ws, user, data): matching_user = None matching_seek = None + rrmin = data["rrmin"] + rrmax = data["rrmax"] + rrmin = rrmin if (rrmin != -1000) else -10000 + rrmax = rrmax if (rrmax != 1000) else 10000 + app_state.auto_pairing_users[user] = (rrmin, rrmax) + for variant_tc in product(data["variants"], data["tcs"]): variant_tc = ( variant_tc[0][0], @@ -410,15 +417,13 @@ async def handle_create_auto_pairing(app_state, ws, user, data): ) if not auto_paired: - app_state.auto_pairing_users.add(user) user.ready_for_auto_pairing = True for user_ws in user.lobby_sockets: await ws_send_json(user_ws, {"type": "auto_pairing_on"}) - -# print("AUTO_PAIRING USERS", [user.username for user in app_state.auto_pairing_users]) -# for key, value in app_state.auto_pairings.items(): -# print(key, [user.username for user in app_state.auto_pairings[key]]) + # print("AUTO_PAIRING USERS", [item for item in app_state.auto_pairing_users.items()]) + # for key, value in app_state.auto_pairings.items(): + # print(key, [user.username for user in app_state.auto_pairings[key]]) async def send_game_in_progress_if_any(app_state: PychessGlobalAppState, user, ws): diff --git a/static/lobby.css b/static/lobby.css index 5b8e87ab5..6fcd8b4f0 100644 --- a/static/lobby.css +++ b/static/lobby.css @@ -135,10 +135,17 @@ div.rating-range .slider { margin: 0; } div.tc-block, div#rating-range-setting, div.timecontrols { - padding: 1em; - background: var(--bg-color2); - border-top: 1px solid var(--border-color); - border-bottom: 1px solid var(--border-color); + padding: 1em; + background: var(--bg-color2); + border-top: 1px solid var(--border-color); + border-bottom: 1px solid var(--border-color); +} +div.timecontrols { + padding: 1em 0 1em 0; +} +div.auto-rating-range { + display: flex; + padding: 8px; } .rating-range { justify-content: center; @@ -146,9 +153,10 @@ div.tc-block, div#rating-range-setting, div.timecontrols { flex-flow: row nowrap; align-items: center; } -.rating-range .rating-min, .rating-range .rating-max { - font-weight: inherit; - flex: 0 0 7ch; +.auto-rating-min, .rating-min, .auto-rating-max, .rating-max { + font-weight: normal; + padding: 0 1ch 0 1ch; + width: 7ch; } div.auto-pairing { display: grid;